Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Throw when prerendered routes have unresolvable content negotiation #9994

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cfcc741
fix: Override precedence of `entries`
elliott-with-the-longest-name-on-github May 20, 2023
d0d26c3
fix: Set `text/html` header in `respond` during prerendering
elliott-with-the-longest-name-on-github May 20, 2023
57a21e4
test
elliott-with-the-longest-name-on-github May 20, 2023
97f8ec9
changeset
elliott-with-the-longest-name-on-github May 20, 2023
9c66098
Merge remote-tracking branch 'origin' into elliott/9929-fix-entry-exp…
elliott-with-the-longest-name-on-github May 27, 2023
34e0ef4
change tact; throw on impossible content negotiation and update tests
elliott-with-the-longest-name-on-github May 27, 2023
8213ef6
changeset
elliott-with-the-longest-name-on-github May 30, 2023
df2c721
Merge remote-tracking branch 'origin' into elliott/9929-fix-entry-exp…
elliott-with-the-longest-name-on-github May 30, 2023
13c235d
changeset
elliott-with-the-longest-name-on-github May 30, 2023
118676a
use filenames for more actionable feedback
Jun 1, 2023
4d16b8c
add a +page.svelte, since the route is unrenderable without
Jun 1, 2023
466feec
throw error at dev time
Jun 1, 2023
eb80b81
Merge branch 'master' into elliott/9929-fix-entry-exports
benmccann Jul 27, 2023
a90638e
fix broken lockfile
benmccann Jul 27, 2023
1207d45
merge master
benmccann Aug 11, 2023
e384cce
Merge remote-tracking branch 'origin' into elliott/9929-fix-entry-exp…
elliott-with-the-longest-name-on-github Oct 10, 2023
5190bb3
lockfile
elliott-with-the-longest-name-on-github Oct 10, 2023
da4b0bd
other unprerenderable methods
elliott-with-the-longest-name-on-github Oct 10, 2023
9999c99
fix: Clean up options
elliott-with-the-longest-name-on-github Oct 10, 2023
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/brown-ads-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: precedence of `entries` should be `+page => +page.server => +server`
5 changes: 5 additions & 0 deletions .changeset/polite-months-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: prerendered routes will throw when exposing both a GET handler and a page, preventing impossible content negotiation
53 changes: 38 additions & 15 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ export default forked(import.meta.url, analyse);
/**
* @param {{
* manifest_path: string;
* manifest_data: import('types').ManifestData;
* env: Record<string, string>
* }} opts
*/
async function analyse({ manifest_path, env }) {
async function analyse({ manifest_path, manifest_data, env }) {
/** @type {import('@sveltejs/kit').SSRManifest} */
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;

Expand Down Expand Up @@ -66,31 +67,38 @@ async function analyse({ manifest_path, env }) {

// analyse routes
for (const route of manifest._.routes) {
const route_data = /** @type {import('types').RouteData} */ (
manifest_data.routes.find((r) => r.id === route.id)
);

/** @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;
/** @type {{ endpoint?: import('types').PrerenderOption, page?: import('types').PrerenderOption }} */
const prerender_option = {};
/** @type {{ endpoint?: unknown, page?: unknown }} */
const config_option = {};
/** @type {{ endpoint?: import('types').PrerenderEntryGenerator | undefined, page?: import('types').PrerenderEntryGenerator | undefined }} */
const entries_option = {};

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)) {
if (
mod.prerender &&
(mod.POST || mod.PATCH || mod.PUT || mod.DELETE || mod.HEAD || mod.OPTIONS)
) {
throw new Error(
`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
`Cannot prerender ${route_data.endpoint?.file} as it exposes POST, PATCH, PUT, DELETE, HEAD, or OPTIONS handlers`
);
}

prerender = mod.prerender;
prerender_option.endpoint = mod.prerender;
}

Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => {
Expand All @@ -101,8 +109,8 @@ async function analyse({ manifest_path, env }) {
}
});

config = mod.config;
entries = mod.entries;
config_option.endpoint = mod.config;
entries_option.endpoint = mod.entries;
}

if (route.page) {
Expand Down Expand Up @@ -130,10 +138,25 @@ async function analyse({ manifest_path, env }) {
validate_page_exports(page.universal, page.universal_id);
}

prerender = get_option(nodes, 'prerender') ?? false;
prerender_option.page = get_option(nodes, 'prerender');

config_option.page = get_config(nodes);
entries_option.page = get_option(nodes, 'entries');
}

const prerender = prerender_option.page ?? prerender_option.endpoint ?? false;
const config = config_option.page ?? entries_option.endpoint;
const entries = entries_option.page ?? entries_option.endpoint;

config = get_config(nodes);
entries ??= get_option(nodes, 'entries');
if (prerender === true && route.endpoint && route.page) {
const page = /** @type {string} */ (
route_data.leaf?.component ?? route_data.leaf?.universal ?? route_data.leaf?.server
);
const endpoint = /** @type {string} */ (route_data.endpoint?.file);

throw new Error(
`Cannot prerender both ${page} and ${endpoint}. Disable prerendering for this route, or delete one of the files.`
);
}

metadata.routes.set(route.id, {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export async function dev(vite, vite_config, svelte_config) {

return module.default;
};

result.component_id = node.component;
}

if (node.universal) {
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ function kit({ svelte_config }) {

const metadata = await analyse({
manifest_path,
manifest_data,
env: { ...env.private, ...env.public }
});

Expand Down
19 changes: 19 additions & 0 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,25 @@ export async function respond(request, options, manifest, state) {
/** @type {Response} */
let response;

if (DEV && route.page && route.endpoint) {
const nodes = await Promise.all(
[...route.page.layouts, route.page.leaf].map((n) => {
if (n !== undefined) return manifest._.nodes[n]();
})
);

const mod = await route.endpoint();

if (get_option(nodes, 'prerender') ?? mod.prerender) {
const page = nodes.at(-1)?.component_id;
const endpoint = route.endpoint_id;

throw new Error(
`Cannot prerender both ${page} and ${endpoint}. Disable prerendering for this route, or delete one of the files.`
);
}
}

if (is_data_request) {
response = await render_data(
event,
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export interface SSRNode {
entries?: PrerenderEntryGenerator;
};

component_id: string;
universal_id: string;
server_id: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "prerenderable-incorrect-fragment",
"name": "prerenderable-entry-generator-mismatch",
"private": true,
"version": "0.0.1",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "prerender-impossible-content-negotiation",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && tsc && svelte-check"
},
"devDependencies": {
"@sveltejs/adapter-auto": "workspace:^",
"@sveltejs/kit": "workspace:^",
"svelte": "^3.56.0",
"svelte-check": "^3.0.2",
"typescript": "^4.9.4",
"vite": "^4.3.6"
Comment on lines +12 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should keep these in sync with the other test projects

Suggested change
"@sveltejs/adapter-auto": "workspace:^",
"@sveltejs/kit": "workspace:^",
"svelte": "^3.56.0",
"svelte-check": "^3.0.2",
"typescript": "^4.9.4",
"vite": "^4.3.6"
"@sveltejs/adapter-auto": "workspace:^",
"@sveltejs/kit": "workspace:^",
"svelte": "^4.0.5",
"svelte-check": "^3.4.4",
"typescript": "^4.9.4",
"vite": "^4.4.9"

},
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello world</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const prerender = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function GET() {}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import adapter from '../../../../../adapter-auto/index.js';

/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
}
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "node",
"paths": {
"@sveltejs/kit": ["../../../../types"],
"$lib": ["./src/lib"],
"$lib/*": ["./src/lib/*"]
}
},
"extends": "./.svelte-kit/tsconfig.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as path from 'node:path';
import { sveltekit } from '@sveltejs/kit/vite';

/** @type {import('vite').UserConfig} */
const config = {
build: {
minify: false
},

clearScreen: false,

logLevel: 'silent',

plugins: [sveltekit()],

server: {
fs: {
allow: [path.resolve('../../../../src')]
}
}
};

export default config;
12 changes: 12 additions & 0 deletions packages/kit/test/build-errors/prerender.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ test('entry generators should match their own route', () => {
`Error: The entries export from /[slug]/[notSpecific] generated entry /whatever/specific, which was matched by /[slug]/specific - see the \`handleEntryGeneratorMismatch\` option in https://kit.svelte.dev/docs/configuration#prerender for more info.${EOL}To suppress or handle this error, implement \`handleEntryGeneratorMismatch\` in https://kit.svelte.dev/docs/configuration#prerender`
);
});

test('prerendering a route with +page and +server files should fail due to impossible content negotiation', () => {
assert.throws(
() =>
execSync('pnpm build', {
cwd: path.join(process.cwd(), 'apps/prerender-impossible-content-negotiation'),
stdio: 'pipe',
timeout: 60000
}),
'Detected a prerendered route with both a +server and +page file (route: /). Because content negotiation for static files is impossible, this is not allowed.'
);
});
Loading
Loading