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

feat: route level config #8740

Merged
merged 46 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
93aa8f6
route level config wip
dummdidumm Jan 26, 2023
c87e38b
this seems to work
dummdidumm Jan 30, 2023
5866441
proper config
dummdidumm Jan 30, 2023
1897e80
do shallow merge
dummdidumm Jan 30, 2023
e2a5d23
docs
dummdidumm Jan 30, 2023
664985a
use config
dummdidumm Jan 30, 2023
b9201d2
default config
dummdidumm Jan 30, 2023
c758d49
vercel docs
dummdidumm Jan 30, 2023
05c6d6c
fix tests
dummdidumm Jan 30, 2023
2edea52
appease TS
dummdidumm Jan 30, 2023
9668cdc
Merge branch 'master' into route-level-config
dummdidumm Jan 30, 2023
b31c1e8
oops
dummdidumm Jan 30, 2023
5c66287
more docs
dummdidumm Feb 2, 2023
b3b1b6e
this feels clunky
dummdidumm Feb 2, 2023
39f5740
Route level config alt (#8863)
Rich-Harris Feb 2, 2023
a688f2b
merge master
Rich-Harris Feb 2, 2023
b3f04ff
"all" not ["all"]
Rich-Harris Feb 2, 2023
b850246
changesets
Rich-Harris Feb 2, 2023
6a3d59c
make defaultConfig a separate option key
Rich-Harris Feb 2, 2023
59f6cfd
Update documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Rich-Harris Feb 2, 2023
0ceccbf
implement split: true
Rich-Harris Feb 2, 2023
7546293
Merge branch 'route-level-config' of github.com:sveltejs/kit into rou…
Rich-Harris Feb 2, 2023
d309554
tidy up
Rich-Harris Feb 3, 2023
a5fd25e
fix site
Rich-Harris Feb 3, 2023
8adb618
Update packages/adapter-vercel/index.d.ts
Rich-Harris Feb 3, 2023
de3dc0b
tweaks
Rich-Harris Feb 3, 2023
ac16c90
get rid of top-level split option
Rich-Harris Feb 3, 2023
6879b2c
union type
dummdidumm Feb 3, 2023
57288bd
handle common case of one fn for everything separately to not pollute…
dummdidumm Feb 3, 2023
b1d0283
tweak docs a little
dummdidumm Feb 3, 2023
998e1bd
netlify
dummdidumm Feb 3, 2023
a252952
make external a config option and simplify adapter options interface
dummdidumm Feb 3, 2023
8f1bbdb
silence type union error
dummdidumm Feb 3, 2023
835dd9c
use platform defaults
Rich-Harris Feb 3, 2023
691eee9
include everything in builder.routes
Rich-Harris Feb 3, 2023
1c43bf5
implement ISR
Rich-Harris Feb 3, 2023
470ef44
fix some docs stuff
Rich-Harris Feb 3, 2023
4dcf727
clarify multi-region serverless is only for enterprise
Rich-Harris Feb 3, 2023
f893672
clarify memory stuff
Rich-Harris Feb 3, 2023
1c871ea
document ISR
Rich-Harris Feb 3, 2023
c5b9f8b
docs tweaks
Rich-Harris Feb 3, 2023
627cf58
fix site
Rich-Harris Feb 3, 2023
3b1b436
add isr in config hash
Rich-Harris Feb 6, 2023
9e88f8b
Merge branch 'master' into route-level-config
dummdidumm Feb 6, 2023
f6c8160
bump adapter-auto etc
dummdidumm Feb 6, 2023
55aad98
bump peerdeps
dummdidumm Feb 6, 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/chatty-moles-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: support route-level configuration
5 changes: 5 additions & 0 deletions .changeset/cuddly-rats-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: support route-level configuration options
8 changes: 8 additions & 0 deletions .changeset/fresh-lamps-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sveltejs/adapter-auto': major
'@sveltejs/adapter-netlify': major
'@sveltejs/adapter-static': major
'@sveltejs/adapter-vercel': major
---

breaking: bump `@sveltejs/kit` peer dependency
5 changes: 5 additions & 0 deletions .changeset/perfect-penguins-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': patch
---

chore: bump `@sveltejs/kit` and `@sveltejs/adapter-auto` versions
48 changes: 48 additions & 0 deletions documentation/docs/20-core-concepts/40-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,51 @@ export const trailingSlash = 'always';
This option also affects [prerendering](#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions.

> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO.

## config

With the concept of [adapters](/docs/adapters), SvelteKit is able to run on a variety of platforms. Each of these might have specific configuration to further tweak the deployment — for example with Vercel or Netlify you could chose to deploy some parts of your app on the edge and others on serverless environments.
Copy link
Contributor

Choose a reason for hiding this comment

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

choose


`config` is an object with key-value pairs at the top level. Beyond that, the concrete shape is dependent on the adapter you're using. Every adapter should provide a `Config` interface to import for type safety. Consult the documentation of your adapter for more information.

```js
// @filename: ambient.d.ts
declare module 'some-adapter' {
export interface Config { runtime: string }
}

// @filename: index.js
// ---cut---
/// file: src/routes/+page.js
/** @type {import('some-adapter').Config} */
export const config = {
runtime: 'edge'
};
```

`config` objects are merged at the top level (but _not_ deeper levels). This means you don't need to repeat all the values in a `+page.js` if you want to only override some of the values in the upper `+layout.js`. For example this layout configuration...

```js
/// file: src/routes/+layout.js
export const config = {
runtime: 'edge',
regions: 'all',
foo: {
bar: true
}
}
```

...is overridden by this page configuration...

```js
/// file: src/routes/+page.js
export const config = {
regions: ['us1', 'us2'],
foo: {
baz: true
}
}
```

...which results in the config value `{ runtime: 'edge', regions: ['us1', 'us2'], foo: { baz: true } }` for that page.
105 changes: 87 additions & 18 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,103 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter-
Install with `npm i -D @sveltejs/adapter-vercel`, then add the adapter to your `svelte.config.js`:

```js
// @errors: 2307
// @errors: 2307 2345
/// file: svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
kit: {
// default options are shown
adapter: adapter({
// if true, will deploy the app using edge functions
// (https://vercel.com/docs/concepts/functions/edge-functions)
// rather than serverless functions
edge: false,

// an array of dependencies that esbuild should treat
// as external when bundling functions. this only applies
// to edge functions, and should only be used to exclude
// optional dependencies that will not run outside Node
external: [],

// if true, will split your app into multiple functions
// instead of creating a single one for the entire app
split: false
// see the 'Deployment configuration' section below
})
}
};
```

## Environment Variables
## Deployment configuration

To control how your routes are deployed to Vercel as functions, you can specify deployment configuration, either through the option shown above or with [`export const config`](/docs/page-options#config) inside `+server.js`, `+page(.server).js` and `+layout(.server).js` files.

For example you could deploy some parts of your app as [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions)...

```js
/// file: about/+page.js
/** @type {import('@sveltejs/adapter-vercel').Config} */
export const config = {
runtime: 'edge'
};
```

...and others as [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (note that by specifying `config` inside a layout, it applies to all child pages):

```js
/// file: admin/+layout.js
/** @type {import('@sveltejs/adapter-vercel').Config} */
export const config = {
runtime: 'nodejs18.x'
};
```

The following options apply to all functions:

- `runtime`: `'edge'`, `'nodejs16.x'` or `'nodejs18.x'`. By default, the adapter will select `'nodejs16.x'` or `'nodejs18.x'` depending on the Node version your project is configured to use on the Vercel dashboard
- `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]` for serverless functions) or `'all'` if `runtime` is `edge` (its default). Note that multiple regions for serverless functions are only supported on Enterprise plans
- `split`: if `true`, causes a route to be deployed as an individual function. If `split` is set to `true` at the adapter level, all routes will be deployed as individual functions

Additionally, the following options apply to edge functions:
- `envVarsInUse`: an array of environment variables that should be accessible inside the edge function
- `external`: an array of dependencies that esbuild should treat as external when bundling functions. This should only be used to exclude optional dependencies that will not run outside Node

And the following option apply to serverless functions:
- `memory`: the amount of memory available to the function. Defaults to `1024` Mb, and can be decreased to `128` Mb or [increased](https://vercel.com/docs/concepts/limits/overview#serverless-function-memory) in 64Mb increments up to `3008` Mb on Pro or Enterprise accounts
- `maxDuration`: maximum execution duration of the function. Defaults to `10` seconds for Hobby accounts, `60` for Pro and `900` for Enterprise
- `isr`: configuration Incremental Static Regeneration, described below

If your functions need to access data in a specific region, it's recommended that they be deployed in the same region (or close to it) for optimal performance.

## Incremental Static Regeneration

Vercel supports [Incremental Static Regeneration](https://vercel.com/docs/concepts/incremental-static-regeneration/overview) (ISR), which provides the performance and cost advantages of prerendered content with the flexibility of dynamically rendered content.

To add ISR to a route, include the `isr` property in your `config` object:

```js
/// file: blog/[slug]/+page.server.js
// @filename: ambient.d.ts
declare module '$env/static/private' {
export const BYPASS_TOKEN: string;
}

// @filename: index.js
// ---cut---
import { BYPASS_TOKEN } from '$env/static/private';

export const config = {
isr: {
// Expiration time (in seconds) before the cached asset will be re-generated by invoking the Serverless Function.
// Setting the value to `false` means it will never expire.
expiration: 60,

// Option group number of the asset. Assets with the same group number will all be re-validated at the same time.
group: 1,

// Random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset
// with a __prerender_bypass=<token> cookie.
//
// Making a `GET` or `HEAD` request with `x-prerender-revalidate: <token>` will force the asset to be re-validated.
bypassToken: BYPASS_TOKEN,

// List of query string parameter names that will be cached independently.
// If an empty array, query values are not considered for caching.
// If `undefined` each unique query value is cached independently
allowQuery: ['search']
}
};
```

The `expiration` property is required; all others are optional.

## Environment variables

Vercel makes a set of [deployment-specific environment variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) available. Like other environment variables, these are accessible from `$env/static/private` and `$env/dynamic/private` (sometimes — more on that later), and inaccessible from their public counterparts. To access one of these variables from the client:

Expand All @@ -65,7 +134,7 @@ export function load() {
<p>This staging environment was deployed from {data.deploymentGitBranch}.</p>
```

Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend using `$env/static/private` — which will statically replace the variables, enabling optimisations like dead code elimination — rather than `$env/dynamic/private`. If you're deploying with `edge: true` you _must_ use `$env/static/private`, as `$env/dynamic/private` and `$env/dynamic/public` are not currently populated in edge functions on Vercel.
Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend using `$env/static/private` — which will statically replace the variables, enabling optimisations like dead code elimination — rather than `$env/dynamic/private`. If you're deploying with `edge: true` you must either use `$env/static/private` or populate the `envVarsInUse` configuration.

## Notes

Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-auto/adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const adapters = [
name: 'Vercel',
test: () => !!process.env.VERCEL,
module: '@sveltejs/adapter-vercel',
version: '1'
version: '2'
},
{
name: 'Cloudflare Pages',
Expand All @@ -17,7 +17,7 @@ export const adapters = [
name: 'Netlify',
test: () => !!process.env.NETLIFY,
module: '@sveltejs/adapter-netlify',
version: '1'
version: '2'
},
{
name: 'Azure Static Web Apps',
Expand Down
51 changes: 32 additions & 19 deletions packages/adapter-netlify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,17 @@ async function generate_lambda_functions({ builder, publish, split }) {
// Configuring the function to use ESM as the output format.
const fn_config = JSON.stringify({ config: { nodeModuleFormat: 'esm' }, version: 1 });

builder.log.minor('Generating serverless functions...');

if (split) {
builder.log.minor('Generating serverless functions...');
const seen = new Set();

for (let i = 0; i < builder.routes.length; i++) {
const route = builder.routes[i];
if (route.prerender === true) continue;

const routes = [route];

await builder.createEntries((route) => {
const parts = [];
// Netlify's syntax uses '*' and ':param' as "splats" and "placeholders"
// https://docs.netlify.com/routing/redirects/redirect-options/#splats
Expand All @@ -183,27 +190,33 @@ async function generate_lambda_functions({ builder, publish, split }) {
const pattern = `/${parts.join('/')}`;
const name = parts.join('-').replace(/[:.]/g, '_').replace('*', '__rest') || 'index';

return {
id: pattern,
filter: (other) => matches(route.segments, other.segments),
complete: (entry) => {
const manifest = entry.generateManifest({
relativePath: '../server'
});
// skip routes with identical patterns, they were already folded into another function
if (seen.has(pattern)) continue;
seen.add(pattern);

const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`;
// figure out which lower priority routes should be considered fallbacks
for (let j = i + 1; j < builder.routes.length; j += 1) {
if (routes[j].prerender === true) continue;

writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn);
writeFileSync(`.netlify/functions-internal/${name}.json`, fn_config);

redirects.push(`${pattern} /.netlify/functions/${name} 200`);
redirects.push(`${pattern}/__data.json /.netlify/functions/${name} 200`);
if (matches(route.segments, routes[j].segments)) {
routes.push(builder.routes[j]);
}
};
});
} else {
builder.log.minor('Generating serverless functions...');
}

const manifest = builder.generateManifest({
relativePath: '../server',
routes
});

const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`;

writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn);
writeFileSync(`.netlify/functions-internal/${name}.json`, fn_config);

redirects.push(`${pattern} /.netlify/functions/${name} 200`);
redirects.push(`${pattern}/__data.json /.netlify/functions/${name} 200`);
}
} else {
const manifest = builder.generateManifest({
relativePath: '../server'
});
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@
"uvu": "^0.5.6"
},
"peerDependencies": {
"@sveltejs/kit": "^1.0.0"
"@sveltejs/kit": "^1.5.0"
}
}
22 changes: 3 additions & 19 deletions packages/adapter-static/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,9 @@ export default function (options) {

async adapt(builder) {
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 && options?.strict !== false) {
if (builder.routes.some((route) => route.prerender !== true) && options?.strict !== false) {
const prefix = path.relative('.', builder.config.kit.files.routes);
const has_param_routes = dynamic_routes.some((route) => route.includes('['));
const has_param_routes = builder.routes.some((route) => route.id.includes('['));
const config_option =
has_param_routes || JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]'
? ` - adjust the \`prerender.entries\` config option ${
Expand All @@ -38,7 +22,7 @@ export default function (options) {

builder.log.error(
`@sveltejs/adapter-static: all routes must be fully prerenderable, but found the following routes that are dynamic:
${dynamic_routes.map((id) => ` - ${path.posix.join(prefix, id)}`).join('\n')}
${builder.routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')}

You have the following options:
- set the \`fallback\` option — see https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more info.
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-static/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
"vite": "^4.0.4"
},
"peerDependencies": {
"@sveltejs/kit": "^1.0.0"
"@sveltejs/kit": "^1.5.0"
}
}
4 changes: 2 additions & 2 deletions packages/adapter-vercel/files/edge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { manifest } from 'MANIFEST';

const server = new Server(manifest);
const initialized = server.init({
env: process.env
env: /** @type {Record<string, string>} */ (process.env)
});

/**
Expand All @@ -13,7 +13,7 @@ export default async (request) => {
await initialized;
return server.respond(request, {
getClientAddress() {
return request.headers.get('x-forwarded-for');
return /** @type {string} */ (request.headers.get('x-forwarded-for'));
}
});
};
6 changes: 3 additions & 3 deletions packages/adapter-vercel/files/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ installPolyfills();
const server = new Server(manifest);

await server.init({
env: process.env
env: /** @type {Record<string, string>} */ (process.env)
});

/**
Expand All @@ -22,15 +22,15 @@ export default async (req, res) => {
try {
request = await getRequest({ base: `https://${req.headers.host}`, request: req });
} catch (err) {
res.statusCode = err.status || 400;
res.statusCode = /** @type {any} */ (err).status || 400;
return res.end('Invalid request body');
}

setResponse(
res,
await server.respond(request, {
getClientAddress() {
return request.headers.get('x-forwarded-for');
return /** @type {string} */ (request.headers.get('x-forwarded-for'));
}
})
);
Expand Down
Loading