Skip to content
Open
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/brown-eggs-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Fixed prerendering pipeline bug found in sveltejs/kit#15620 and sveltejs/kit#10735
7 changes: 4 additions & 3 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down
86 changes: 56 additions & 30 deletions packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,55 +213,65 @@ 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)
);
}

/**
* @param {string} decoded
* @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<string, import('types').PrerenderDependency>} */
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);
Expand Down Expand Up @@ -355,7 +365,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
/** @type {Set<string>} */ (expected_hashlinks.get(key)).add(decoded);
}

void enqueue(decoded, decode_uri(pathname), pathname);
void enqueue(decoded, decode_uri(pathname), pathname, undefined, true);
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,11 @@ export interface ServerMetadataRoute {
config: any;
api: {
methods: Array<HttpMethod | '*'>;
prerender?: boolean | 'auto';
};
page: {
methods: Array<'GET' | 'POST'>;
prerender?: boolean | 'auto';
};
methods: Array<HttpMethod | '*'>;
prerender: PrerenderOption | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>prerendered page with server endpoint</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function GET() {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' }
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function POST() {
return new Response('OK', { status: 200 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import { resolve } from '$app/paths';
</script>

<a href={resolve('/linked-api/my-awesome-endpoint.json')}>My Awesome Endpoint</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const prerender = true;

export function GET() {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' }
});
}
14 changes: 14 additions & 0 deletions packages/kit/test/prerendering/basics/test/tests.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<p id="a">path with spaces</p>');
Expand Down
Loading