diff --git a/.changeset/brown-eggs-march.md b/.changeset/brown-eggs-march.md new file mode 100644 index 000000000000..79f946e8e712 --- /dev/null +++ b/.changeset/brown-eggs-march.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Fixed prerendering pipeline bug found in sveltejs/kit#15620 and sveltejs/kit#10735 diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 2b967e555f2b..75678b91187c 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -124,7 +124,6 @@ async function analyse({ const route_config = page?.config ?? endpoint?.config ?? {}; const prerender = page?.prerender ?? endpoint?.prerender; - if (prerender !== true) { for (const feature of list_features( route, @@ -144,10 +143,12 @@ async function analyse({ config: route_config, methods: Array.from(new Set([...page_methods, ...api_methods])), page: { - methods: page_methods + methods: page_methods, + prerender: page?.prerender }, api: { - methods: api_methods + methods: api_methods, + prerender: endpoint?.prerender }, prerender, entries: diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 806952e39268..f0bdf74f4c78 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -213,15 +213,19 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { * @param {string} decoded * @param {string} [encoded] * @param {string} [generated_from_id] + * @param {boolean} [expect_html] */ - function enqueue(referrer, decoded, encoded, generated_from_id) { - if (seen.has(decoded)) return; - seen.add(decoded); + function enqueue(referrer, decoded, encoded, generated_from_id, expect_html) { + const key = expect_html ? decoded + '\x00page' : decoded; + if (seen.has(key)) return; + seen.add(key); const file = decoded.slice(config.paths.base.length + 1); if (files.has(file)) return; - return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id)); + return q.add(() => + visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id, expect_html) + ); } /** @@ -229,39 +233,45 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { * @param {string} encoded * @param {string?} referrer * @param {string} [generated_from_id] + * @param {boolean} [expect_html] */ - async function visit(decoded, encoded, referrer, generated_from_id) { + async function visit(decoded, encoded, referrer, generated_from_id, expect_html) { if (!decoded.startsWith(config.paths.base)) { handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); return; } + const requestHeaders = expect_html ? { Accept: 'text/html' } : undefined; + /** @type {Map} */ const dependencies = new Map(); - const response = await server.respond(new Request(config.prerender.origin + encoded), { - getClientAddress() { - throw new Error('Cannot read clientAddress during prerendering'); - }, - prerendering: { - dependencies, - remote_responses - }, - read: (file) => { - // stuff we just wrote - const filepath = saved.get(file); - if (filepath) return readFileSync(filepath); - - // Static assets emitted during build - if (file.startsWith(config.appDir)) { - return readFileSync(`${out}/server/${file}`); - } + const response = await server.respond( + new Request(config.prerender.origin + encoded, { headers: requestHeaders }), + { + getClientAddress() { + throw new Error('Cannot read clientAddress during prerendering'); + }, + prerendering: { + dependencies, + remote_responses + }, + read: (file) => { + // stuff we just wrote + const filepath = saved.get(file); + if (filepath) return readFileSync(filepath); + + // Static assets emitted during build + if (file.startsWith(config.appDir)) { + return readFileSync(`${out}/server/${file}`); + } - // stuff in `static` - return readFileSync(join(config.files.assets, file)); - }, - emulator - }); + // stuff in `static` + return readFileSync(join(config.files.assets, file)); + }, + emulator + } + ); const encoded_id = response.headers.get('x-sveltekit-routeid'); const decoded_id = encoded_id && decode_uri(encoded_id); @@ -355,7 +365,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { /** @type {Set} */ (expected_hashlinks.get(key)).add(decoded); } - void enqueue(decoded, decode_uri(pathname), pathname); + void enqueue(decoded, decode_uri(pathname), pathname, undefined, true); } } } @@ -534,7 +544,15 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { if (processed_id.includes('[')) continue; const path = `/${get_route_segments(processed_id).join('/')}`; - void enqueue(null, config.paths.base + path); + + const route_data = metadata.routes.get(id); + if (route_data?.page.prerender && route_data?.page.methods.includes('GET')) + void enqueue(null, config.paths.base + path, undefined, undefined, true); + if ( + route_data?.api.prerender && + (route_data?.api.methods.includes('GET') || route_data?.api.methods.includes('*')) + ) + void enqueue(null, config.paths.base + path, undefined, undefined, false); } } } else { @@ -543,8 +561,16 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { } for (const { id, entries } of route_level_entries) { + const route_data = metadata.routes.get(id); + for (const entry of entries) { - void enqueue(null, config.paths.base + entry, undefined, id); + if (route_data?.page.prerender && route_data?.page.methods.includes('GET')) + void enqueue(null, config.paths.base + entry, undefined, id, true); + if ( + route_data?.api.prerender && + (route_data?.api.methods.includes('GET') || route_data?.api.methods.includes('*')) + ) + void enqueue(null, config.paths.base + entry, undefined, id, false); } } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index de22d467faab..80a3462ed8eb 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -377,9 +377,11 @@ export interface ServerMetadataRoute { config: any; api: { methods: Array; + prerender?: boolean | 'auto'; }; page: { methods: Array<'GET' | 'POST'>; + prerender?: boolean | 'auto'; }; methods: Array; prerender: PrerenderOption | undefined; diff --git a/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+page.svelte new file mode 100644 index 000000000000..8c966b6e13b0 --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+page.svelte @@ -0,0 +1 @@ +

prerendered page with server endpoint

diff --git a/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+server.js b/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+server.js new file mode 100644 index 000000000000..7956019cce17 --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/duplicate-get/+server.js @@ -0,0 +1,5 @@ +export function GET() { + return new Response(JSON.stringify({ ok: true }), { + headers: { 'content-type': 'application/json' } + }); +} diff --git a/packages/kit/test/prerendering/basics/src/routes/get-and-post/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/get-and-post/+page.svelte new file mode 100644 index 000000000000..7f4c64a81129 --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/get-and-post/+page.svelte @@ -0,0 +1 @@ +Hello World... diff --git a/packages/kit/test/prerendering/basics/src/routes/get-and-post/+server.ts b/packages/kit/test/prerendering/basics/src/routes/get-and-post/+server.ts new file mode 100644 index 000000000000..0ac0f6ca06d8 --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/get-and-post/+server.ts @@ -0,0 +1,3 @@ +export async function POST() { + return new Response('OK', { status: 200 }); +} diff --git a/packages/kit/test/prerendering/basics/src/routes/linked-api/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/linked-api/+page.svelte new file mode 100644 index 000000000000..bde0a5406dea --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/linked-api/+page.svelte @@ -0,0 +1,5 @@ + + +My Awesome Endpoint diff --git a/packages/kit/test/prerendering/basics/src/routes/linked-api/my-awesome-endpoint.json/+server.js b/packages/kit/test/prerendering/basics/src/routes/linked-api/my-awesome-endpoint.json/+server.js new file mode 100644 index 000000000000..f01a402dd8ed --- /dev/null +++ b/packages/kit/test/prerendering/basics/src/routes/linked-api/my-awesome-endpoint.json/+server.js @@ -0,0 +1,7 @@ +export const prerender = true; + +export function GET() { + return new Response(JSON.stringify({ ok: true }), { + headers: { 'content-type': 'application/json' } + }); +} diff --git a/packages/kit/test/prerendering/basics/test/tests.spec.js b/packages/kit/test/prerendering/basics/test/tests.spec.js index e3972a1d369d..95a648650698 100644 --- a/packages/kit/test/prerendering/basics/test/tests.spec.js +++ b/packages/kit/test/prerendering/basics/test/tests.spec.js @@ -158,6 +158,20 @@ test('does not prerender page with shadow endpoint with non-load handler', () => assert.isFalse(fs.existsSync(`${build}/shadowed-post/__data.json`)); }); +test('prerendering a page that coexists with a GET server endpoint', () => { + assert.isTrue(fs.existsSync(`${build}/duplicate-get.html`)); +}); + +test('prerendering a page that coexists with a POST server endpoint', () => { + assert.isTrue(fs.existsSync(`${build}/get-and-post.html`)); +}); + +test('prerendering a page with a linked GET server endpoint processes properly', () => { + assert.isTrue(fs.existsSync(`${build}/linked-api.html`)); + assert.isTrue(fs.existsSync(`${build}/linked-api/my-awesome-endpoint.json`)); + assert.isFalse(fs.existsSync(`${build}/linked-api/my-awesome-endpoint.html`)); +}); + test('decodes paths when writing files', () => { let content = read('encoding/path with spaces.html'); expect(content).toMatch('

path with spaces

');