Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-monkeys-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: fix resolution order for exports from route files
305 changes: 207 additions & 98 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,126 +46,235 @@ 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((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 }))
);

// 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)
};
}

/**
* Do a shallow merge (first level) of the config object
* @param {Array<import('types').SSRNode | undefined>} nodes
*/
function get_config(nodes) {
let current = {};
for (const node of nodes) {
if (!(node?.server?.config || node?.universal?.config)) continue;
current = {
...current,
...(node?.server?.config ?? {}),
...(node?.universal?.config ?? {})
};
}

// 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;
}

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);
}

prerender = get_option(nodes, 'prerender') ?? false;
return Object.keys(current).length ? current : undefined;
}

config = get_config(nodes);
entries ??= get_option(nodes, 'entries');
}
/**
* @param {Promise<import('types').SSRNode>} 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<import('types').SSRNode>[]}} 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, layout }] = await Promise.all([
endpointResultPromise,
pageResultPromise
]);
const { methods, prerender, config, entries } = merge_route_options({ endpoint, page, layout });
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: EndpointRouteAnalysisResult, page: PageRouteAnalysisResult, layout: LayoutRouteAnalysisResult }} analysis_results
* @returns {MergedRouteAnalysisResult}
*/
function merge_route_options({ endpoint, page, layout }) {
return {
methods: [...new Set([...endpoint.methods, ...page.methods])],
prerender: page.prerender ?? endpoint.prerender ?? layout.prerender,
entries: page.entries ?? endpoint.entries, // layouts can't have entries
config: {
...layout.config,
...endpoint.config,
...page.config
}
};
}

/** @typedef {{
methods: (import('types').HttpMethod | "*")[];
prerender: import('types').PrerenderOption | undefined;
config: any;
entries: import('types').PrerenderEntryGenerator | undefined;
}} 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<import('types').SSREndpoint> | undefined} endpoint_promise
* @param {{file: string}} config
* @returns {Promise<EndpointRouteAnalysisResult>}
*/
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<import('types').SSRNode | undefined>} nodes
* @param {import('types').PageNodeIndexes | null} page_indexes
* @param {{ node_promises: Promise<import('types').SSRNode>[] }} config
* @returns {Promise<{ page: PageRouteAnalysisResult, layout: LayoutRouteAnalysisResult }>}
*/
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 {
page: {
methods: [],
prerender: undefined,
config: undefined,
entries: undefined
},
layout: {
prerender: undefined,
config: 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 {
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)
}
};
}