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

Prerendering overhaul #6392

Merged
merged 19 commits into from
Aug 30, 2022
Merged
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/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:
Copy link
Member

Choose a reason for hiding this comment

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

should we add JS here to clarify that you can't add it to +page.svelte?

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I was hoping the code sample would make it clear — adding JS muddies the waters a bit in the case where people are using TypeScript

Copy link
Member

Choose a reason for hiding this comment

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

There are some other places where it's +page/layout with the .js ending. We should probably make that consistent, either adding the .js file always or never (except for when we introduce it).


```js
/// file: +page.js/+page.server.js
/// file: +page.js/+page.server.js/+server.js
Copy link
Member

Choose a reason for hiding this comment

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

I was confused for a bit because I think "what kind of folder names are these?". Maybe instead +page.js, or +page.server.js, or +server.js?

Copy link
Member

Choose a reason for hiding this comment

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

Skipping this for now, seing that it's like this for all others as well.

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':
Copy link
Member

Choose a reason for hiding this comment

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

It took me a little while to understand why this works - why can I prerender AND SSR a route - until I thought "oh yes with [slug]". I don't know how to express this better, but maybe we can come up with something later.


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

Choose a reason for hiding this comment

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

now that I've seen the docs and understand what this option does, I'll throw another name into the ring. how about 'partial'?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I prefer autopartial doesn't have the right connotation to me, it sounds like the page is being partially prerendered

```

> 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