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 12 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
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: 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.
4 changes: 4 additions & 0 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default {
};
```

## Config options

Besides the config options shown above, the Vercel adapter also supports the [`runtime`](https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration), [`regions`](https://vercel.com/docs/concepts/edge-network/regions), [`maxDuration`](https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration) and [`memory`](https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration) options for serverless functions and the [`regions`](https://vercel.com/docs/concepts/edge-network/regions) and [`envVarsInUse`](https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration) options for edge functions. You can set defaults through the adapter options and override them inside `+page/layout(.server).js` files using the [config](/docs/page-options#config) export.

## 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 Down
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
32 changes: 31 additions & 1 deletion packages/adapter-vercel/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ type Options = {
edge?: boolean;
external?: string[];
split?: boolean;
};
} & Config;

export default function plugin(options?: Options): Adapter;

export interface Config {
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
/**
* To which runtime to deploy the app. Can be one of:
* - `edge`: https://vercel.com/docs/concepts/functions/edge-functions
* - `serverless` or a string specifying the serverless runtime (like `node16`): https://vercel.com/docs/concepts/functions/serverless-functions
* @default 'serverless'
*/
runtime?: 'serverless' | 'edge' | (string & {});
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
/**
* To which regions to deploy the app. A list of regions or `'all'` for edge functions.
* More info: https://vercel.com/docs/concepts/edge-network/regions
*/
regions?: string[] | 'all';
/**
* List of environment variable names that will be available for the Edge Function to utilize.
* Edge only.
*/
envVarsInUse?: string[];
/**
* Maximum execution duration (in seconds) that will be allowed for the Serverless Function.
* Serverless only.
*/
maxDuration?: number;
/**
* Amount of memory (RAM in MB) that will be allocated to the Serverless Function.
* Serverless only.
*/
memory?: number;
}
87 changes: 69 additions & 18 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { nodeFileTrace } from '@vercel/nft';
import esbuild from 'esbuild';

/** @type {import('.').default} **/
const plugin = function ({ external = [], edge, split } = {}) {
const plugin = function ({ external = [], edge, split, ...default_config } = {}) {
return {
name: '@sveltejs/adapter-vercel',

Expand All @@ -25,16 +25,17 @@ const plugin = function ({ external = [], edge, split } = {}) {
functions: `${dir}/functions`
};

const config = static_vercel_config(builder);
const static_config = static_vercel_config(builder);

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

/**
* @param {string} name
* @param {string} pattern
* @param {import('.').Config | undefined} config
* @param {(options: { relativePath: string }) => string} generate_manifest
*/
async function generate_serverless_function(name, pattern, generate_manifest) {
async function generate_serverless_function(name, pattern, config, generate_manifest) {
const relativePath = path.posix.relative(tmp, builder.getServerDirectory());

builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
Expand All @@ -53,18 +54,20 @@ const plugin = function ({ external = [], edge, split } = {}) {
builder,
`${tmp}/index.js`,
`${dirs.functions}/${name}.func`,
`nodejs${node_version.major}.x`
`nodejs${node_version.major}.x`,
config
);

config.routes.push({ src: pattern, dest: `/${name}` });
static_config.routes.push({ src: pattern, dest: `/${name}` });
}

/**
* @param {string} name
* @param {string} pattern
* @param {import('.').Config | undefined} config
* @param {(options: { relativePath: string }) => string} generate_manifest
*/
async function generate_edge_function(name, pattern, generate_manifest) {
async function generate_edge_function(name, pattern, config, generate_manifest) {
const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`);
const relativePath = path.posix.relative(tmp, builder.getServerDirectory());

Expand Down Expand Up @@ -95,22 +98,24 @@ const plugin = function ({ external = [], edge, split } = {}) {
write(
`${dirs.functions}/${name}.func/.vc-config.json`,
JSON.stringify({
...config,
runtime: 'edge',
entrypoint: 'index.js'
// TODO expose envVarsInUse
})
);

config.routes.push({ src: pattern, dest: `/${name}` });
static_config.routes.push({ src: pattern, dest: `/${name}` });
}

const generate_function = edge ? generate_edge_function : generate_serverless_function;

if (split) {
if (split || builder.hasRouteLevelConfig) {
await builder.createEntries((route) => {
const route_config = { ...default_config, ...route.config };
return {
id: route.pattern.toString(), // TODO is `id` necessary?
filter: (other) => route.pattern.toString() === other.pattern.toString(),
filter: (other) =>
split
? route.pattern.toString() === other.pattern.toString()
: can_group(route_config, { ...default_config, ...other.config }),
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
complete: async (entry) => {
let sliced_pattern = route.pattern
.toString()
Expand All @@ -126,12 +131,22 @@ const plugin = function ({ external = [], edge, split } = {}) {

const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes

await generate_function(route.id.slice(1) || 'index', src, entry.generateManifest);
const generate_function =
edge && (!route_config.runtime || route_config.runtime === 'edge')
? generate_edge_function
: generate_serverless_function;
await generate_function(
route.id.slice(1) || 'index',
src,
route_config,
entry.generateManifest
);
}
};
});
} else {
await generate_function('render', '/.*', builder.generateManifest);
const generate_function = edge ? generate_edge_function : generate_serverless_function;
await generate_function('render', '/.*', default_config, builder.generateManifest);
}

builder.log.minor('Copying assets...');
Expand All @@ -141,11 +156,44 @@ const plugin = function ({ external = [], edge, split } = {}) {

builder.log.minor('Writing routes...');

write(`${dir}/config.json`, JSON.stringify(config, null, ' '));
write(`${dir}/config.json`, JSON.stringify(static_config, null, ' '));
}
};
};

/**
* @param {import('.').Config | undefined} config_a
* @param {import('.').Config | undefined} config_b
*/
function can_group(config_a, config_b) {
if (config_a === config_b) return true;
if (!config_a || !config_b) return false;

if (config_a.runtime !== config_b.runtime) return false;
if (config_a.maxDuration !== config_b.maxDuration) return false;
if (config_a.memory !== config_b.memory) return false;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
if (arrays_different(config_a.envVarsInUse, config_b.envVarsInUse)) return false;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

const regions_a = config_a.regions === 'all' ? ['all'] : config_a.regions;
const regions_b = config_b.regions === 'all' ? ['all'] : config_b.regions;
if (arrays_different(regions_a, regions_b)) return false;

return true;
}

/**
*
* @param {any[] | undefined} a
* @param {any[] | undefined} b
* @returns
*/
function arrays_different(a, b) {
if (a === b) return false;
if (!a || !b) return true;
if (a.length !== b.length) return true;
return a.every((e) => b.includes(e));
}

/**
* @param {string} file
* @param {string} data
Expand Down Expand Up @@ -228,8 +276,9 @@ function static_vercel_config(builder) {
* @param {string} entry
* @param {string} dir
* @param {string} runtime
* @param {import('.').Config | undefined} config
*/
async function create_function_bundle(builder, entry, dir, runtime) {
async function create_function_bundle(builder, entry, dir, runtime, config) {
fs.rmSync(dir, { force: true, recursive: true });

let base = entry;
Expand Down Expand Up @@ -257,7 +306,7 @@ async function create_function_bundle(builder, entry, dir, runtime) {
resolution_failures.set(importer, []);
}

resolution_failures.get(importer).push(module);
/** @type {string[]} */ (resolution_failures.get(importer)).push(module);
} else {
throw error;
}
Expand All @@ -278,7 +327,8 @@ async function create_function_bundle(builder, entry, dir, runtime) {
}

// find common ancestor directory
let common_parts;
/** @type {string[]} */
let common_parts = [];

for (const file of traced.fileList) {
if (common_parts) {
Expand Down Expand Up @@ -324,6 +374,7 @@ async function create_function_bundle(builder, entry, dir, runtime) {
`${dir}/.vc-config.json`,
JSON.stringify({
runtime,
...config,
handler: path.relative(base + ancestor, entry),
launcherType: 'Nodejs'
})
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-vercel/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"noEmit": true,
"noImplicitAny": true,
"module": "es2022",
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export function create_builder({ config, build_data, server_metadata, routes, pr

config,
prerendered,
hasRouteLevelConfig:
!!server_metadata.routes &&
!![...server_metadata.routes.values()].some((route) => route.config),

async compress(directory) {
if (!existsSync(directory)) {
Expand All @@ -57,6 +60,7 @@ export function create_builder({ config, build_data, server_metadata, routes, pr
const methods =
/** @type {import('types').HttpMethod[]} */
(server_metadata.routes.get(route.id)?.methods);
const config = server_metadata.routes.get(route.id)?.config;

return {
id: route.id,
Expand All @@ -66,7 +70,8 @@ export function create_builder({ config, build_data, server_metadata, routes, pr
content: segment
})),
pattern: route.pattern,
methods
methods,
config
};
});

Expand Down
Loading