From 5e8f6d345ea0c85e4126af1dc810cfd19dd32c6b Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 8 Dec 2023 18:29:15 -0700 Subject: [PATCH 1/5] feat: Make analyse concurrent, fix override precedence of entries and config --- packages/kit/src/core/postbuild/analyse.js | 272 +++++++++++++-------- 1 file changed, 175 insertions(+), 97 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 47d859d88bd2..a647a68277e0 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -46,126 +46,204 @@ async function analyse({ manifest_path, env }) { internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix })); internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix })); - /** @type {import('types').ServerMetadata} */ - const metadata = { - nodes: [], - routes: new Map() - }; + const node_promises = manifest._.nodes.map(load_node); + const node_metadata_promise = Promise.all(node_promises.map(analyse_node)); + const route_metadata_promise = Promise.all( + manifest._.routes.map((route) => analyse_route(route, { node_promises })) + ); - // analyse nodes - for (const loader of manifest._.nodes) { - const node = await loader(); + const [nodes, routes] = await Promise.all([node_metadata_promise, route_metadata_promise]); - metadata.nodes[node.index] = { - has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined - }; - } + return { + nodes, + routes: new Map(routes) + }; +} - // analyse routes - for (const route of manifest._.routes) { - /** @type {Array<'GET' | 'POST'>} */ - const page_methods = []; - - /** @type {(import('types').HttpMethod | '*')[]} */ - const api_methods = []; - - /** @type {import('types').PrerenderOption | undefined} */ - let prerender = undefined; - /** @type {any} */ - let config = undefined; - /** @type {import('types').PrerenderEntryGenerator | undefined} */ - let entries = undefined; - - if (route.endpoint) { - const mod = await route.endpoint(); - if (mod.prerender !== undefined) { - validate_server_exports(mod, route.id); - - if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { - throw new Error( - `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` - ); - } - - prerender = mod.prerender; - } - - Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => { - if (mod[method] && ENDPOINT_METHODS.has(method)) { - api_methods.push(method); - } else if (mod.fallback) { - api_methods.push('*'); - } - }); - - config = mod.config; - entries = mod.entries; +/** + * Do a shallow merge (first level) of the config object + * @param {Array} nodes + */ +function get_config(nodes) { + let current = {}; + for (const node of nodes) { + const config = node?.universal?.config ?? node?.server?.config; + if (config) { + current = { + ...current, + ...config + }; } + } - if (route.page) { - const nodes = await Promise.all( - [...route.page.layouts, route.page.leaf].map((n) => { - if (n !== undefined) return manifest._.nodes[n](); - }) - ); - - const layouts = nodes.slice(0, -1); - const page = nodes.at(-1); - - for (const layout of layouts) { - if (layout) { - validate_layout_server_exports(layout.server, layout.server_id); - validate_layout_exports(layout.universal, layout.universal_id); - } - } - - if (page) { - page_methods.push('GET'); - if (page.server?.actions) page_methods.push('POST'); - - validate_page_server_exports(page.server, page.server_id); - validate_page_exports(page.universal, page.universal_id); - } + return Object.keys(current).length ? current : undefined; +} - prerender = get_option(nodes, 'prerender') ?? false; +/** + * @param {import('types').SSRNodeLoader} loader + * @returns {Promise} + */ +function load_node(loader) { + return loader(); +} - config = get_config(nodes); - entries ??= get_option(nodes, 'entries'); - } +/** + * @param {Promise} nodePromise + * @returns {Promise<{ has_server_load: boolean }>} + */ +async function analyse_node(nodePromise) { + const node = await nodePromise; + return { + has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined + }; +} - metadata.routes.set(route.id, { +/** + * + * @param {import('types').SSRRoute} route + * @param {{node_promises: Promise[]}} config + * @returns {Promise<[string, import('types').ServerMetadataRoute]>} + */ +async function analyse_route(route, { node_promises }) { + const endpointResultPromise = analyse_route_endpoint(route.endpoint?.(), { file: route.id }); + const pageResultPromise = analyse_route_page(route.page, { node_promises }); + const [endpoint, page] = await Promise.all([endpointResultPromise, pageResultPromise]); + const { methods, prerender, config, entries } = merge_route_options({ endpoint, page }); + return [ + route.id, + { config, - methods: Array.from(new Set([...page_methods, ...api_methods])), + methods, page: { - methods: page_methods + methods: /** @type {("GET" | "POST")[]} */ (page.methods) }, api: { - methods: api_methods + methods: endpoint.methods }, prerender, entries: entries && (await entries()).map((entry_object) => resolvePath(route.id, entry_object)) - }); + } + ]; +} + +/** + * + * @param {{ endpoint: RouteAnalysisResult, page: RouteAnalysisResult }} analysis_results + * @returns {RouteAnalysisResult} + */ +function merge_route_options({ endpoint, page }) { + return { + methods: [...new Set([...endpoint.methods, ...page.methods])], + prerender: page.prerender ?? endpoint.prerender, + entries: page.entries ?? endpoint.entries, + config: { + ...endpoint.config, + ...page.config + } + }; +} + +/** @typedef {{ + methods: (import('types').HttpMethod | "*")[]; + prerender: import('types').PrerenderOption | undefined; + config: any; + entries: import('types').PrerenderEntryGenerator | undefined; + }} RouteAnalysisResult + */ + +/** + * @param {Promise | undefined} endpoint_promise + * @param {{file: string}} config + * @returns {Promise} + */ +async function analyse_route_endpoint(endpoint_promise, { file }) { + if (!endpoint_promise) { + return { + methods: [], + prerender: undefined, + config: undefined, + entries: undefined + }; + } + const endpoint_module = await endpoint_promise; + if (endpoint_module.prerender !== undefined) { + validate_server_exports(endpoint_module, file); + + if ( + endpoint_module.prerender && + (endpoint_module.POST || + endpoint_module.PATCH || + endpoint_module.PUT || + endpoint_module.DELETE) + ) { + throw new Error(`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${file})`); + } } - return metadata; + /** @type {(import('types').HttpMethod | '*')[]} */ + const methods = []; + Object.values(endpoint_module).forEach((/** @type {import('types').HttpMethod} */ method) => { + if (endpoint_module[method] && ENDPOINT_METHODS.has(method)) { + methods.push(method); + } else if (endpoint_module.fallback) { + methods.push('*'); + } + }); + + return { + methods, + prerender: endpoint_module.prerender, + config: endpoint_module.config, + entries: endpoint_module.entries + }; } /** - * Do a shallow merge (first level) of the config object - * @param {Array} nodes + * @param {import('types').PageNodeIndexes | null} page_indexes + * @param {{ node_promises: Promise[] }} config + * @returns {Promise} */ -function get_config(nodes) { - let current = {}; - for (const node of nodes) { - const config = node?.universal?.config ?? node?.server?.config; - if (config) { - current = { - ...current, - ...config - }; +async function analyse_route_page(page_indexes, { node_promises }) { + if (!page_indexes) { + return { + methods: [], + prerender: undefined, + config: undefined, + entries: undefined + }; + } + + const nodes = await Promise.all( + [...page_indexes.layouts, page_indexes.leaf].map((n) => { + if (n !== undefined) return node_promises[n]; + }) + ); + + const layouts = nodes.slice(0, -1); + const page = nodes.at(-1); + + for (const layout of layouts) { + if (layout) { + validate_layout_server_exports(layout.server, layout.server_id); + validate_layout_exports(layout.universal, layout.universal_id); } } - return Object.keys(current).length ? current : undefined; + /** @type {Array<'GET' | 'POST'>} */ + const methods = []; + if (page) { + methods.push('GET'); + if (page.server?.actions) methods.push('POST'); + + validate_page_server_exports(page.server, page.server_id); + validate_page_exports(page.universal, page.universal_id); + } + + return { + methods, + prerender: get_option(nodes, 'prerender'), + config: get_config(nodes), + entries: get_option(nodes, 'entries') + }; } From cafae9fcd97e4ac96bb9d7a52e1d336d0d782ac4 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 8 Dec 2023 18:33:57 -0700 Subject: [PATCH 2/5] fix: Remove silly function --- packages/kit/src/core/postbuild/analyse.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index a647a68277e0..4e46cf112de9 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -46,7 +46,7 @@ async function analyse({ manifest_path, env }) { internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix })); internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix })); - const node_promises = manifest._.nodes.map(load_node); + const node_promises = manifest._.nodes.map((loader) => loader()); const node_metadata_promise = Promise.all(node_promises.map(analyse_node)); const route_metadata_promise = Promise.all( manifest._.routes.map((route) => analyse_route(route, { node_promises })) @@ -79,14 +79,6 @@ function get_config(nodes) { return Object.keys(current).length ? current : undefined; } -/** - * @param {import('types').SSRNodeLoader} loader - * @returns {Promise} - */ -function load_node(loader) { - return loader(); -} - /** * @param {Promise} nodePromise * @returns {Promise<{ has_server_load: boolean }>} From 1341a3555b4ce53a49d068e93eb7c0fcbfdab447 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 8 Dec 2023 21:21:55 -0700 Subject: [PATCH 3/5] changeset --- .changeset/silver-monkeys-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silver-monkeys-smash.md diff --git a/.changeset/silver-monkeys-smash.md b/.changeset/silver-monkeys-smash.md new file mode 100644 index 000000000000..486032e9d8f1 --- /dev/null +++ b/.changeset/silver-monkeys-smash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: fix resolution order for exports from route files From 574f40de57dd75b3f19ec9e8317c3ebb875878d4 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 8 Dec 2023 21:54:15 -0700 Subject: [PATCH 4/5] fix: Merge `config` more consistently --- packages/kit/src/core/postbuild/analyse.js | 81 ++++++++++++++++------ 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 4e46cf112de9..b04d20fbdfff 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -71,7 +71,8 @@ function get_config(nodes) { if (config) { current = { ...current, - ...config + ...(node?.server?.config ?? {}), + ...(node?.universal?.config ?? {}) }; } } @@ -99,8 +100,11 @@ async function analyse_node(nodePromise) { async function analyse_route(route, { node_promises }) { const endpointResultPromise = analyse_route_endpoint(route.endpoint?.(), { file: route.id }); const pageResultPromise = analyse_route_page(route.page, { node_promises }); - const [endpoint, page] = await Promise.all([endpointResultPromise, pageResultPromise]); - const { methods, prerender, config, entries } = merge_route_options({ endpoint, page }); + const [endpoint, { page, layout }] = await Promise.all([ + endpointResultPromise, + pageResultPromise + ]); + const { methods, prerender, config, entries } = merge_route_options({ endpoint, page, layout }); return [ route.id, { @@ -121,15 +125,16 @@ async function analyse_route(route, { node_promises }) { /** * - * @param {{ endpoint: RouteAnalysisResult, page: RouteAnalysisResult }} analysis_results - * @returns {RouteAnalysisResult} + * @param {{ endpoint: EndpointRouteAnalysisResult, page: PageRouteAnalysisResult, layout: LayoutRouteAnalysisResult }} analysis_results + * @returns {MergedRouteAnalysisResult} */ -function merge_route_options({ endpoint, page }) { +function merge_route_options({ endpoint, page, layout }) { return { methods: [...new Set([...endpoint.methods, ...page.methods])], - prerender: page.prerender ?? endpoint.prerender, - entries: page.entries ?? endpoint.entries, + prerender: page.prerender ?? endpoint.prerender ?? layout.prerender, + entries: page.entries ?? endpoint.entries, // layouts can't have entries config: { + ...layout.config, ...endpoint.config, ...page.config } @@ -141,13 +146,35 @@ function merge_route_options({ endpoint, page }) { prerender: import('types').PrerenderOption | undefined; config: any; entries: import('types').PrerenderEntryGenerator | undefined; - }} RouteAnalysisResult + }} EndpointRouteAnalysisResult + */ + +/** @typedef {{ + methods: ("GET" | "POST")[]; + prerender: import('types').PrerenderOption | undefined; + config: any; + entries: import('types').PrerenderEntryGenerator | undefined; + }} PageRouteAnalysisResult + */ + +/** @typedef {{ + prerender: import('types').PrerenderOption | undefined; + config: any; + }} LayoutRouteAnalysisResult + */ + +/** @typedef {{ + methods: (import('types').HttpMethod | "*")[]; + prerender: import('types').PrerenderOption | undefined; + config: any; + entries: import('types').PrerenderEntryGenerator | undefined; + }} MergedRouteAnalysisResult */ /** * @param {Promise | undefined} endpoint_promise * @param {{file: string}} config - * @returns {Promise} + * @returns {Promise} */ async function analyse_route_endpoint(endpoint_promise, { file }) { if (!endpoint_promise) { @@ -194,15 +221,21 @@ async function analyse_route_endpoint(endpoint_promise, { file }) { /** * @param {import('types').PageNodeIndexes | null} page_indexes * @param {{ node_promises: Promise[] }} config - * @returns {Promise} + * @returns {Promise<{ page: PageRouteAnalysisResult, layout: LayoutRouteAnalysisResult }>} */ async function analyse_route_page(page_indexes, { node_promises }) { if (!page_indexes) { return { - methods: [], - prerender: undefined, - config: undefined, - entries: undefined + page: { + methods: [], + prerender: undefined, + config: undefined, + entries: undefined + }, + layout: { + prerender: undefined, + config: undefined + } }; } @@ -226,16 +259,24 @@ async function analyse_route_page(page_indexes, { node_promises }) { const methods = []; if (page) { methods.push('GET'); - if (page.server?.actions) methods.push('POST'); + if (page.server?.actions) { + methods.push('POST'); + } validate_page_server_exports(page.server, page.server_id); validate_page_exports(page.universal, page.universal_id); } return { - methods, - prerender: get_option(nodes, 'prerender'), - config: get_config(nodes), - entries: get_option(nodes, 'entries') + page: { + methods, + prerender: get_option([page], 'prerender'), + config: get_config([page]), + entries: get_option([page], 'entries') + }, + layout: { + prerender: get_option(layouts, 'prerender'), + config: get_config(layouts) + } }; } From 3e7b838cdb40585c934eaec15f9eb558855da4c4 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 8 Dec 2023 21:57:18 -0700 Subject: [PATCH 5/5] fix: dumb --- packages/kit/src/core/postbuild/analyse.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index b04d20fbdfff..633bf6aa9f62 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -67,14 +67,12 @@ async function analyse({ manifest_path, env }) { function get_config(nodes) { let current = {}; for (const node of nodes) { - const config = node?.universal?.config ?? node?.server?.config; - if (config) { - current = { - ...current, - ...(node?.server?.config ?? {}), - ...(node?.universal?.config ?? {}) - }; - } + if (!(node?.server?.config || node?.universal?.config)) continue; + current = { + ...current, + ...(node?.server?.config ?? {}), + ...(node?.universal?.config ?? {}) + }; } return Object.keys(current).length ? current : undefined;