Skip to content

Commit aedb572

Browse files
authored
breaking: stricter route validation, more correct config resolution (#11256)
* fix get_config * oops * fix, changeset * always validate server exports --------- Co-authored-by: Rich Harris <[email protected]>
1 parent c59375c commit aedb572

File tree

4 files changed

+105
-72
lines changed

4 files changed

+105
-72
lines changed

.changeset/eight-pens-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': major
3+
---
4+
5+
breaking: fail if route with +page and +server is marked prerenderable

.changeset/loud-plants-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': major
3+
---
4+
5+
breaking: fail if +page and +server have mismatched config

.changeset/mean-trees-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: resolve route config correctly

packages/kit/src/core/postbuild/analyse.js

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -52,97 +52,54 @@ async function analyse({ manifest_path, env }) {
5252
routes: new Map()
5353
};
5454

55-
// analyse nodes
56-
for (const loader of manifest._.nodes) {
57-
const node = await loader();
55+
const nodes = await Promise.all(manifest._.nodes.map((loader) => loader()));
5856

57+
// analyse nodes
58+
for (const node of nodes) {
5959
metadata.nodes[node.index] = {
6060
has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined
6161
};
6262
}
6363

6464
// analyse routes
6565
for (const route of manifest._.routes) {
66-
/** @type {Array<'GET' | 'POST'>} */
67-
const page_methods = [];
68-
69-
/** @type {(import('types').HttpMethod | '*')[]} */
70-
const api_methods = [];
66+
const page =
67+
route.page &&
68+
analyse_page(
69+
route.page.layouts.map((n) => (n === undefined ? n : nodes[n])),
70+
nodes[route.page.leaf]
71+
);
7172

72-
/** @type {import('types').PrerenderOption | undefined} */
73-
let prerender = undefined;
74-
/** @type {any} */
75-
let config = undefined;
76-
/** @type {import('types').PrerenderEntryGenerator | undefined} */
77-
let entries = undefined;
73+
const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint());
7874

79-
if (route.endpoint) {
80-
const mod = await route.endpoint();
81-
if (mod.prerender !== undefined) {
82-
validate_server_exports(mod, route.id);
75+
if (page?.prerender && endpoint?.prerender) {
76+
throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`);
77+
}
8378

84-
if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
79+
if (page?.config && endpoint?.config) {
80+
for (const key in { ...page.config, ...endpoint.config }) {
81+
if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) {
8582
throw new Error(
86-
`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
83+
`Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any`
8784
);
8885
}
89-
90-
prerender = mod.prerender;
9186
}
92-
93-
Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => {
94-
if (mod[method] && ENDPOINT_METHODS.has(method)) {
95-
api_methods.push(method);
96-
} else if (mod.fallback) {
97-
api_methods.push('*');
98-
}
99-
});
100-
101-
config = mod.config;
102-
entries = mod.entries;
10387
}
10488

105-
if (route.page) {
106-
const nodes = await Promise.all(
107-
[...route.page.layouts, route.page.leaf].map((n) => {
108-
if (n !== undefined) return manifest._.nodes[n]();
109-
})
110-
);
111-
112-
const layouts = nodes.slice(0, -1);
113-
const page = nodes.at(-1);
114-
115-
for (const layout of layouts) {
116-
if (layout) {
117-
validate_layout_server_exports(layout.server, layout.server_id);
118-
validate_layout_exports(layout.universal, layout.universal_id);
119-
}
120-
}
121-
122-
if (page) {
123-
page_methods.push('GET');
124-
if (page.server?.actions) page_methods.push('POST');
125-
126-
validate_page_server_exports(page.server, page.server_id);
127-
validate_page_exports(page.universal, page.universal_id);
128-
}
129-
130-
prerender = get_option(nodes, 'prerender') ?? false;
131-
132-
config = get_config(nodes);
133-
entries ??= get_option(nodes, 'entries');
134-
}
89+
const page_methods = page?.methods ?? [];
90+
const api_methods = endpoint?.methods ?? [];
91+
const entries = page?.entries ?? endpoint?.entries;
13592

13693
metadata.routes.set(route.id, {
137-
config,
94+
config: page?.config ?? endpoint?.config,
13895
methods: Array.from(new Set([...page_methods, ...api_methods])),
13996
page: {
14097
methods: page_methods
14198
},
14299
api: {
143100
methods: api_methods
144101
},
145-
prerender,
102+
prerender: page?.prerender ?? endpoint?.prerender,
146103
entries:
147104
entries && (await entries()).map((entry_object) => resolvePath(route.id, entry_object))
148105
});
@@ -151,20 +108,81 @@ async function analyse({ manifest_path, env }) {
151108
return metadata;
152109
}
153110

111+
/**
112+
* @param {import('types').SSRRoute} route
113+
* @param {import('types').SSREndpoint} mod
114+
*/
115+
function analyse_endpoint(route, mod) {
116+
validate_server_exports(mod, route.id);
117+
118+
if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
119+
throw new Error(
120+
`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
121+
);
122+
}
123+
124+
/** @type {Array<import('types').HttpMethod | '*'>} */
125+
const methods = [];
126+
127+
Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => {
128+
if (mod[method] && ENDPOINT_METHODS.has(method)) {
129+
methods.push(method);
130+
} else if (mod.fallback) {
131+
methods.push('*');
132+
}
133+
});
134+
135+
return {
136+
config: mod.config,
137+
entries: mod.entries,
138+
methods,
139+
prerender: mod.prerender ?? false
140+
};
141+
}
142+
143+
/**
144+
* @param {Array<import('types').SSRNode | undefined>} layouts
145+
* @param {import('types').SSRNode} leaf
146+
*/
147+
function analyse_page(layouts, leaf) {
148+
for (const layout of layouts) {
149+
if (layout) {
150+
validate_layout_server_exports(layout.server, layout.server_id);
151+
validate_layout_exports(layout.universal, layout.universal_id);
152+
}
153+
}
154+
155+
/** @type {Array<'GET' | 'POST'>} */
156+
const methods = ['GET'];
157+
if (leaf.server?.actions) methods.push('POST');
158+
159+
validate_page_server_exports(leaf.server, leaf.server_id);
160+
validate_page_exports(leaf.universal, leaf.universal_id);
161+
162+
return {
163+
config: get_config([...layouts, leaf]),
164+
entries: leaf.universal?.entries ?? leaf.server?.entries,
165+
methods,
166+
prerender: get_option([...layouts, leaf], 'prerender') ?? false
167+
};
168+
}
169+
154170
/**
155171
* Do a shallow merge (first level) of the config object
156172
* @param {Array<import('types').SSRNode | undefined>} nodes
157173
*/
158174
function get_config(nodes) {
175+
/** @type {any} */
159176
let current = {};
177+
160178
for (const node of nodes) {
161-
const config = node?.universal?.config ?? node?.server?.config;
162-
if (config) {
163-
current = {
164-
...current,
165-
...config
166-
};
167-
}
179+
if (!node?.universal?.config && !node?.server?.config) continue;
180+
181+
current = {
182+
...current,
183+
...node?.universal?.config,
184+
...node?.server?.config
185+
};
168186
}
169187

170188
return Object.keys(current).length ? current : undefined;

0 commit comments

Comments
 (0)