Skip to content

Commit

Permalink
[breaking] Prerendering overhaul (#6392)
Browse files Browse the repository at this point in the history
Closes #4287
Main part of #6356
  • Loading branch information
Rich-Harris committed Aug 30, 2022
1 parent 9592539 commit 863a546
Show file tree
Hide file tree
Showing 34 changed files with 287 additions and 116 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-laws-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-static': patch
---

[breaking] require all routes to be prerenderable when not using fallback option
5 changes: 5 additions & 0 deletions .changeset/spicy-taxis-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] add `prerender = 'auto'` option, and extend `prerender` option to endpoints
15 changes: 11 additions & 4 deletions documentation/docs/12-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,29 @@ export const hydrate = false;
### prerender

It's likely that at least some pages of your app can be represented as a simple HTML file generated at build time. These pages can be [_prerendered_](/docs/appendix#prerendering).
It's likely that at least some routes of your app can be represented as a simple HTML file generated at build time. These routes can be [_prerendered_](/docs/appendix#prerendering).

Prerendering happens automatically for any page with the `prerender` annotation:
Prerendering happens automatically for any `+page` or `+server` file with the `prerender` annotation:

```js
/// file: +page.js/+page.server.js
/// file: +page.js/+page.server.js/+server.js
export const prerender = true;
```

Alternatively, you can set [`config.kit.prerender.default`](/docs/configuration#prerender) to `true` and prerender everything except pages that are explicitly marked as _not_ prerenderable:

```js
/// file: +page.js/+page.server.js
/// file: +page.js/+page.server.js/+server.js
export const prerender = false;
```

Routes with `prerender = true` will be excluded from manifests used for dynamic SSR, making your server (or serverless/edge functions) smaller. In some cases you might want to prerender a route but also include it in the manifest (for example, you want to prerender your most recent/popular content but server-render the long tail) — for these cases, there's a third option, 'auto':

```js
/// file: +page.js/+page.server.js/+server.js
export const prerender = 'auto';
```

> If your entire app is suitable for prerendering, you can use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will output files suitable for use with any static webserver.
The prerenderer will start at the root of your app and generate HTML for any prerenderable pages it finds. Each page is scanned for `<a>` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with the `entries` option in the [prerender configuration](/docs/configuration#prerender).
Expand Down
32 changes: 28 additions & 4 deletions packages/adapter-static/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path';
import { platforms } from './platforms.js';

/** @type {import('.').default} */
Expand All @@ -6,10 +7,33 @@ export default function (options) {
name: '@sveltejs/adapter-static',

async adapt(builder) {
if (!options?.fallback && !builder.config.kit.prerender.default) {
throw Error(
'adapter-static requires `config.kit.prerender.default` to be `true` unless you set the `fallback: true` option to create a single-page app. See https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more information'
);
if (!options?.fallback) {
/** @type {string[]} */
const dynamic_routes = [];

// this is a bit of a hack — it allows us to know whether there are dynamic
// (i.e. prerender = false/'auto') routes without having dedicated API
// surface area for it
builder.createEntries((route) => {
dynamic_routes.push(route.id);

return {
id: '',
filter: () => false,
complete: () => {}
};
});

if (dynamic_routes.length > 0) {
const prefix = path.relative('.', builder.config.kit.files.routes);
builder.log.error(
`@sveltejs/adapter-static: cannot have dynamic routes unless using the 'fallback' option. See https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more information`
);
builder.log.error(
dynamic_routes.map((id) => ` - ${path.posix.join(prefix, id)}`).join('\n')
);
throw new Error('Encountered dynamic routes');
}
}

const platform = platforms.find((platform) => platform.test());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const res = await fetch('/endpoint/implicit.json');
return await res.json();
}
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<h1>This page was prerendered</h1>
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>

<h1>This page was prerendered</h1>
<p>answer: {data.answer}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';

export const prerender = true;

/** @type {import('./$types').RequestHandler} */
export function GET() {
return json({ answer: 42 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { json } from '@sveltejs/kit';

// no export const prerender here, it should be prerendered by virtue
// of being fetched from a prerendered page

/** @type {import('./$types').RequestHandler} */
export function GET() {
return json({ answer: 42 });
}
11 changes: 10 additions & 1 deletion packages/adapter-static/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ run('prerendered', (test) => {
assert.ok(fs.existsSync(`${cwd}/build/index.html`));
});

test('prerenders content', async ({ base, page }) => {
test('prerenders a page', async ({ base, page }) => {
await page.goto(base);
assert.equal(await page.textContent('h1'), 'This page was prerendered');
assert.equal(await page.textContent('p'), 'answer: 42');
});

test('prerenders an unreferenced endpoint with explicit `prerender` setting', async ({ cwd }) => {
assert.ok(fs.existsSync(`${cwd}/build/endpoint/explicit.json`));
});

test('prerenders a referenced endpoint with implicit `prerender` setting', async ({ cwd }) => {
assert.ok(fs.existsSync(`${cwd}/build/endpoint/implicit.json`));
});
});

Expand Down
1 change: 0 additions & 1 deletion packages/adapter-static/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function run(app, callback) {
console.error(`---\nstdout:\n${e.stdout}`);
console.error(`---\nstderr:\n${e.stderr}`);
console.groupEnd();
assert.unreachable(e.message);
}

context.cwd = cwd;
Expand Down
72 changes: 28 additions & 44 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,21 @@ import { pipeline } from 'stream';
import { promisify } from 'util';
import { copy, rimraf, mkdirp } from '../../utils/filesystem.js';
import { generate_manifest } from '../generate_manifest/index.js';
import { get_path } from '../../utils/routing.js';

const pipe = promisify(pipeline);

/**
* Creates the Builder which is passed to adapters for building the application.
* @param {{
* config: import('types').ValidatedConfig;
* build_data: import('types').BuildData;
* routes: import('types').RouteData[];
* prerendered: import('types').Prerendered;
* log: import('types').Logger;
* }} opts
* @returns {import('types').Builder}
*/
export function create_builder({ config, build_data, prerendered, log }) {
/** @type {Set<string>} */
const prerendered_paths = new Set(prerendered.paths);

/** @param {import('types').RouteData} route */
// TODO routes should come pre-filtered
function not_prerendered(route) {
const path = route.page && get_path(route.id);
if (path) {
return !prerendered_paths.has(path) && !prerendered_paths.has(path + '/');
}

return true;
}

const pipe = promisify(pipeline);

/**
* @param {string} file
* @param {'gz' | 'br'} format
*/
async function compress_file(file, format = 'gz') {
const compress =
format == 'br'
? zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size
}
})
: zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION });

const source = createReadStream(file);
const destination = createWriteStream(`${file}.${format}`);

await pipe(source, compress, destination);
}

export function create_builder({ config, build_data, routes, prerendered, log }) {
return {
log,
rimraf,
Expand All @@ -66,8 +30,6 @@ export function create_builder({ config, build_data, prerendered, log }) {
prerendered,

async createEntries(fn) {
const { routes } = build_data.manifest_data;

/** @type {import('types').RouteDefinition[]} */
const facades = routes.map((route) => {
const methods = new Set();
Expand Down Expand Up @@ -113,7 +75,7 @@ export function create_builder({ config, build_data, prerendered, log }) {
}
}

const filtered = new Set(group.filter(not_prerendered));
const filtered = new Set(group);

// heuristic: if /foo/[bar] is included, /foo/[bar].json should
// also be included, since the page likely needs the endpoint
Expand Down Expand Up @@ -146,7 +108,7 @@ export function create_builder({ config, build_data, prerendered, log }) {
return generate_manifest({
build_data,
relative_path: relativePath,
routes: build_data.manifest_data.routes.filter(not_prerendered),
routes,
format
});
},
Expand Down Expand Up @@ -221,3 +183,25 @@ export function create_builder({ config, build_data, prerendered, log }) {
}
};
}

/**
* @param {string} file
* @param {'gz' | 'br'} format
*/
async function compress_file(file, format = 'gz') {
const compress =
format == 'br'
? zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size
}
})
: zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION });

const source = createReadStream(file);
const destination = createWriteStream(`${file}.${format}`);

await pipe(source, compress, destination);
}
1 change: 1 addition & 0 deletions packages/kit/src/core/adapt/builder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ test('copy files', () => {
config: /** @type {import('types').ValidatedConfig} */ (mocked),
// @ts-expect-error
build_data: {},
routes: [],
// @ts-expect-error
prerendered: {
paths: []
Expand Down
16 changes: 14 additions & 2 deletions packages/kit/src/core/adapt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import { create_builder } from './builder.js';
* @param {import('types').ValidatedConfig} config
* @param {import('types').BuildData} build_data
* @param {import('types').Prerendered} prerendered
* @param {import('types').PrerenderMap} prerender_map
* @param {{ log: import('types').Logger }} opts
*/
export async function adapt(config, build_data, prerendered, { log }) {
export async function adapt(config, build_data, prerendered, prerender_map, { log }) {
const { name, adapt } = config.kit.adapter;

console.log(colors.bold().cyan(`\n> Using ${name}`));

const builder = create_builder({ config, build_data, prerendered, log });
const builder = create_builder({
config,
build_data,
routes: build_data.manifest_data.routes.filter((route) => {
if (!route.page && !route.endpoint) return false;

const prerender = prerender_map.get(route.id);
return prerender === false || prerender === undefined || prerender === 'auto';
}),
prerendered,
log
});
await adapt(builder);

log.success('done');
Expand Down
Loading

0 comments on commit 863a546

Please sign in to comment.