From 165ab337934871f6c6f46d254e7db529c7539ee1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 14:35:50 -0500 Subject: [PATCH 01/10] Revert "this feels clunky" This reverts commit b3b1b6ef89769d9d29369073b214b01ad6635a69. --- packages/adapter-vercel/index.js | 59 ++++++++++++-------------- packages/kit/src/core/adapt/builder.js | 33 ++++---------- packages/kit/types/private.d.ts | 20 ++------- 3 files changed, 38 insertions(+), 74 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index eb1fdb9a81f8..2034d006d7c7 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -31,11 +31,11 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) /** * @param {string} name - * @param {string[]} patterns + * @param {string} pattern * @param {import('.').Config | undefined} config * @param {(options: { relativePath: string }) => string} generate_manifest */ - async function generate_serverless_function(name, patterns, config, 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`, { @@ -58,18 +58,16 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) config ); - for (const pattern of patterns) { - static_config.routes.push({ src: pattern, dest: `/${name}` }); - } + static_config.routes.push({ src: pattern, dest: `/${name}` }); } /** * @param {string} name - * @param {string[]} patterns + * @param {string} pattern * @param {import('.').Config | undefined} config * @param {(options: { relativePath: string }) => string} generate_manifest */ - async function generate_edge_function(name, patterns, config, 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()); @@ -106,9 +104,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) }) ); - for (const pattern of patterns) { - static_config.routes.push({ src: pattern, dest: `/${name}` }); - } + static_config.routes.push({ src: pattern, dest: `/${name}` }); } if (split || builder.hasRouteLevelConfig) { @@ -117,34 +113,31 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) return { id: route.pattern.toString(), // TODO is `id` necessary? filter: (other) => - (!split && !builder.hasRouteLevelConfig) || - route.pattern.toString() === other.pattern.toString(), - group: (other) => can_group(route_config, { ...default_config, ...other.config }), + split + ? route.pattern.toString() === other.pattern.toString() + : can_group(route_config, { ...default_config, ...other.config }), complete: async (entry) => { - const patterns = entry.routes.map((route) => { - let sliced_pattern = route.pattern - .toString() - // remove leading / and trailing $/ - .slice(1, -2) - // replace escaped \/ with / - .replace(/\\\//g, '/'); - - // replace the root route "^/" with "^/?" - if (sliced_pattern === '^/') { - sliced_pattern = '^/?'; - } - - return `${sliced_pattern}(?:/__data.json)?$`; - }); + let sliced_pattern = route.pattern + .toString() + // remove leading / and trailing $/ + .slice(1, -2) + // replace escaped \/ with / + .replace(/\\\//g, '/'); + + // replace the root route "^/" with "^/?" + if (sliced_pattern === '^/') { + sliced_pattern = '^/?'; + } + + const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes const generate_function = - (edge && !route_config.runtime) || route_config.runtime === 'edge' + edge && (!route_config.runtime || route_config.runtime === 'edge') ? generate_edge_function : generate_serverless_function; - await generate_function( route.id.slice(1) || 'index', - patterns, + src, route_config, entry.generateManifest ); @@ -153,7 +146,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) }); } else { const generate_function = edge ? generate_edge_function : generate_serverless_function; - await generate_function('render', ['/.*'], default_config, builder.generateManifest); + await generate_function('render', '/.*', default_config, builder.generateManifest); } builder.log.minor('Copying assets...'); @@ -380,8 +373,8 @@ async function create_function_bundle(builder, entry, dir, runtime, config) { write( `${dir}/.vc-config.json`, JSON.stringify({ + runtime, ...config, - runtime: config?.runtime === 'serverless' || !config?.runtime ? runtime : config.runtime, handler: path.relative(base + ancestor, entry), launcherType: 'Nodejs' }) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index c601dbc11399..44cadc8e50ad 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -76,57 +76,40 @@ export function create_builder({ config, build_data, server_metadata, routes, pr }); const seen = new Set(); - const grouped = new Set(); - let ungrouped = facades.map((f, i) => ({ r: routes[i], f })); for (let i = 0; i < routes.length; i += 1) { const route = routes[i]; - const { id, filter, complete, group } = fn(facades[i]); + const { id, filter, complete } = fn(facades[i]); - if (seen.has(id) || grouped.has(route)) continue; + if (seen.has(id)) continue; seen.add(id); - const filtered = new Set([route]); - const route_definitions = new Set([facades[i]]); - - if (group) { - ungrouped = ungrouped.filter((candidate) => { - if (group(candidate.f)) { - filtered.add(candidate.r); - route_definitions.add(candidate.f); - grouped.add(candidate.r); - return false; - } - return true; - }); - } + const group = [route]; // figure out which lower priority routes should be considered fallbacks for (let j = i + 1; j < routes.length; j += 1) { - if (!group && filter(facades[j])) { - filtered.add(routes[j]); - route_definitions.add(facades[j]); + if (filter(facades[j])) { + group.push(routes[j]); } } + 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 // TODO is this still necessary, given the new way of doing things? filtered.forEach((route) => { if (route.page) { - const idx = routes.findIndex((candidate) => candidate.id === route.id + '.json'); - const endpoint = routes[idx]; + const endpoint = routes.find((candidate) => candidate.id === route.id + '.json'); if (endpoint) { filtered.add(endpoint); - route_definitions.add(facades[idx]); } } }); if (filtered.size > 0) { await complete({ - routes: Array.from(route_definitions), generateManifest: ({ relativePath }) => generate_manifest({ build_data, diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts index 6391d12f42eb..395d582cbdf3 100644 --- a/packages/kit/types/private.d.ts +++ b/packages/kit/types/private.d.ts @@ -10,33 +10,21 @@ export interface AdapterEntry { */ id: string; - /** - * A function that compares the lower candidate route with the current route to determine - * if it should be grouped with the current route. Has no effect when `group` is set. - * - * Use cases: - * - Fallback pages: `/foo/[c]` is a fallback for `/foo/a-[b]`, and `/[...catchall]` is a fallback for all routes - */ - filter(route: RouteDefinition): boolean; - /** * A function that compares the candidate route with the current route to determine - * if it should be grouped with the current route. In contrast to `filter`, this - * results in the other route not being invoked by `createEntries` again, if grouped. + * if it should be grouped with the current route. * * Use cases: + * - Fallback pages: `/foo/[c]` is a fallback for `/foo/a-[b]`, and `/[...catchall]` is a fallback for all routes * - Grouping routes that share a common `config`: `/foo` should be deployed to the edge, `/bar` and `/baz` should be deployed to a serverless function */ - group?(route: RouteDefinition): boolean; + filter(route: RouteDefinition): boolean; /** * A function that is invoked once the entry has been created. This is where you * should write the function to the filesystem and generate redirect manifests. */ - complete(entry: { - routes: RouteDefinition[]; - generateManifest(opts: { relativePath: string }): string; - }): MaybePromise; + complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise; } // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts From f378118a0237e1027dabb5a499331d86d405de74 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 16:52:27 -0500 Subject: [PATCH 02/10] alternative approach to route-level config --- packages/adapter-vercel/index.js | 174 ++++++++++++++------ packages/kit/src/core/adapt/builder.js | 84 ++++++---- packages/kit/src/core/adapt/builder.spec.js | 2 +- packages/kit/src/core/adapt/index.js | 2 +- packages/kit/types/index.d.ts | 6 +- 5 files changed, 180 insertions(+), 88 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 2034d006d7c7..7731294abfa3 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -4,6 +4,13 @@ import { fileURLToPath } from 'url'; import { nodeFileTrace } from '@vercel/nft'; import esbuild from 'esbuild'; +const DEFAULTS = { + runtime: 'node18.x', + regions: ['iad1'], + memory: 128, + maxDuration: 30 // TODO check what the defaults actually are +}; + /** @type {import('.').default} **/ const plugin = function ({ external = [], edge, split, ...default_config } = {}) { return { @@ -31,11 +38,10 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) /** * @param {string} name - * @param {string} pattern - * @param {import('.').Config | undefined} config - * @param {(options: { relativePath: string }) => string} generate_manifest + * @param {import('.').Config} config + * @param {typeof builder.routes} routes */ - async function generate_serverless_function(name, pattern, config, generate_manifest) { + async function generate_serverless_function(name, config, routes) { const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, { @@ -47,30 +53,34 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) write( `${tmp}/manifest.js`, - `export const manifest = ${generate_manifest({ relativePath })};\n` + `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` ); await create_function_bundle( builder, `${tmp}/index.js`, `${dirs.functions}/${name}.func`, - `nodejs${node_version.major}.x`, + `nodejs${node_version.major}.x`, // TODO use function config config ); - - 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 + * @param {import('.').Config} config + * @param {typeof builder.routes} routes */ - async function generate_edge_function(name, pattern, config, generate_manifest) { + async function generate_edge_function(name, config, routes) { const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`); const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); + const envVarsInUse = new Set(); + routes.forEach((route) => { + route.config?.envVarsInUse?.forEach((x) => { + envVarsInUse.add(x); + }); + }); + builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, { replace: { SERVER: `${relativePath}/index.js`, @@ -80,7 +90,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) write( `${tmp}/manifest.js`, - `export const manifest = ${generate_manifest({ relativePath })};\n` + `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` ); await esbuild.build({ @@ -99,54 +109,105 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) `${dirs.functions}/${name}.func/.vc-config.json`, JSON.stringify({ ...config, - runtime: 'edge', + envVarsInUse: [...envVarsInUse], entrypoint: 'index.js' }) ); + } + + /** @type {Map} */ + const groups = new Map(); + + /** @type {Map} */ + const conflicts = new Map(); - static_config.routes.push({ src: pattern, dest: `/${name}` }); + /** @type {Map} */ + const functions = new Map(); + + // group routes by config + for (const route of builder.routes) { + const pattern = route.pattern.toString(); + const config = { ...DEFAULTS, ...default_config, ...route.config }; + const hash = hash_config(config); + + // first, check there are no routes with incompatible configs that will be merged + const existing = conflicts.get(pattern); + if (existing) { + if (existing.hash !== hash) { + throw new Error( + `The ${route.id} and ${existing.route_id} routes must be merged into a single function that matches the ${route.pattern} regex, but they have incompatible configs. You must either rename one of the routes, or make their configs match.` + ); + } + } else { + conflicts.set(pattern, { hash, route_id: route.id }); + } + + // then, create a group for each config + let group = groups.get(hash); + if (!group) { + group = { i: groups.size, config, routes: [] }; + groups.set(hash, group); + } + + group.routes.push(route); } - 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) => - split - ? route.pattern.toString() === other.pattern.toString() - : can_group(route_config, { ...default_config, ...other.config }), - complete: async (entry) => { - let sliced_pattern = route.pattern - .toString() - // remove leading / and trailing $/ - .slice(1, -2) - // replace escaped \/ with / - .replace(/\\\//g, '/'); - - // replace the root route "^/" with "^/?" - if (sliced_pattern === '^/') { - sliced_pattern = '^/?'; - } - - const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes - - 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 - ); + for (const group of groups.values()) { + const generate_function = + group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function; + + if (split) { + // generate individual functions + /** @type {Map} */ + const merged = new Map(); + + for (const route of group.routes) { + const pattern = route.pattern.toString(); + const existing = merged.get(pattern); + if (existing) { + existing.push(route); + } else { + merged.set(pattern, [route]); } - }; - }); - } else { - const generate_function = edge ? generate_edge_function : generate_serverless_function; - await generate_function('render', '/.*', default_config, builder.generateManifest); + } + + let i = 0; + + for (const [pattern, routes] of merged) { + const name = `fn-${group.i}-${i++}`; + functions.set(pattern, name); + await generate_function(name, group.config, routes); + } + } else { + // generate one function for the group + const name = `fn-${group.i}`; + await generate_function(name, group.config, group.routes); + + for (const route of group.routes) { + functions.set(route.pattern.toString(), name); + } + } + } + + for (const route of builder.routes) { + const pattern = route.pattern.toString(); + + let src = pattern + // remove leading / and trailing $/ + .slice(1, -2) + // replace escaped \/ with / + .replace(/\\\//g, '/'); + + // replace the root route "^/" with "^/?" + if (src === '^/') { + src = '^/?'; + } + + const name = functions.get(pattern); + if (name) { + static_config.routes.push({ src, dest: `/${name}` }); + functions.delete(pattern); + } } builder.log.minor('Copying assets...'); @@ -161,6 +222,11 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) }; }; +/** @param {import('.').Config} config */ +function hash_config(config) { + return [config.runtime, config.regions, config.memory, config.maxDuration].join('/'); +} + /** * @param {import('.').Config | undefined} config_a * @param {import('.').Config | undefined} config_b diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 44cadc8e50ad..f0ac7c81fdd9 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -18,13 +18,51 @@ const pipe = promisify(pipeline); * config: import('types').ValidatedConfig; * build_data: import('types').BuildData; * server_metadata: import('types').ServerMetadata; - * routes: import('types').RouteData[]; + * route_data: import('types').RouteData[]; * prerendered: import('types').Prerendered; * log: import('types').Logger; * }} opts * @returns {import('types').Builder} */ -export function create_builder({ config, build_data, server_metadata, routes, prerendered, log }) { +export function create_builder({ + config, + build_data, + server_metadata, + route_data, + prerendered, + log +}) { + /** @type {Map} */ + const lookup = new Map(); + + /** + * Rather than exposing the internal `RouteData` type, which is subject to change, + * we expose a stable type that adapters can use to group/filter routes + */ + const routes = route_data.map((route) => { + const methods = + /** @type {import('types').HttpMethod[]} */ + (server_metadata.routes.get(route.id)?.methods); + const config = server_metadata.routes.get(route.id)?.config; + + /** @type {import('types').RouteDefinition} */ + const facade = { + id: route.id, + segments: get_route_segments(route.id).map((segment) => ({ + dynamic: segment.includes('['), + rest: segment.includes('[...'), + content: segment + })), + pattern: route.pattern, + methods, + config + }; + + lookup.set(facade, route); + + return facade; + }); + return { log, rimraf, @@ -33,6 +71,8 @@ export function create_builder({ config, build_data, server_metadata, routes, pr config, prerendered, + routes, + hasRouteLevelConfig: !!server_metadata.routes && !![...server_metadata.routes.values()].some((route) => route.config), @@ -55,31 +95,11 @@ export function create_builder({ config, build_data, server_metadata, routes, pr }, async createEntries(fn) { - /** @type {import('types').RouteDefinition[]} */ - const facades = routes.map((route) => { - 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, - segments: get_route_segments(route.id).map((segment) => ({ - dynamic: segment.includes('['), - rest: segment.includes('[...'), - content: segment - })), - pattern: route.pattern, - methods, - config - }; - }); - const seen = new Set(); - for (let i = 0; i < routes.length; i += 1) { - const route = routes[i]; - const { id, filter, complete } = fn(facades[i]); + for (let i = 0; i < route_data.length; i += 1) { + const route = route_data[i]; + const { id, filter, complete } = fn(routes[i]); if (seen.has(id)) continue; seen.add(id); @@ -87,9 +107,9 @@ export function create_builder({ config, build_data, server_metadata, routes, pr const group = [route]; // figure out which lower priority routes should be considered fallbacks - for (let j = i + 1; j < routes.length; j += 1) { - if (filter(facades[j])) { - group.push(routes[j]); + for (let j = i + 1; j < route_data.length; j += 1) { + if (filter(routes[j])) { + group.push(route_data[j]); } } @@ -100,7 +120,7 @@ export function create_builder({ config, build_data, server_metadata, routes, pr // TODO is this still necessary, given the new way of doing things? filtered.forEach((route) => { if (route.page) { - const endpoint = routes.find((candidate) => candidate.id === route.id + '.json'); + const endpoint = route_data.find((candidate) => candidate.id === route.id + '.json'); if (endpoint) { filtered.add(endpoint); @@ -148,11 +168,13 @@ export function create_builder({ config, build_data, server_metadata, routes, pr }); }, - generateManifest: ({ relativePath }) => { + generateManifest: ({ relativePath, routes: subset }) => { return generate_manifest({ build_data, relative_path: relativePath, - routes + routes: subset + ? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) + : route_data }); }, diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index 2f10ff4b22f1..983b37e75b05 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -31,7 +31,7 @@ test('copy files', () => { build_data: {}, // @ts-expect-error server_metadata: {}, - routes: [], + route_data: [], // @ts-expect-error prerendered: { paths: [] diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 430054830748..0dd8dd116565 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -18,7 +18,7 @@ export async function adapt(config, build_data, server_metadata, prerendered, pr config, build_data, server_metadata, - routes: build_data.manifest_data.routes.filter((route) => { + route_data: build_data.manifest_data.routes.filter((route) => { if (!route.page && !route.endpoint) return false; const prerender = prerender_map.get(route.id); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3d60e591b091..2b7ff7733b2a 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -83,12 +83,16 @@ export interface Builder { config: ValidatedConfig; /** Information about prerendered pages and assets, if any. */ prerendered: Prerendered; + /** An array of dynamic (not prerendered) routes */ + routes: RouteDefinition[]; + /** `true` if one or more routes have `export const config = ..` in it */ hasRouteLevelConfig: boolean; /** * Create separate functions that map to one or more routes of your app. * @param fn A function that groups a set of routes into an entry point + * @deprecated Use `builder.routes` instead */ createEntries(fn: (route: RouteDefinition) => AdapterEntry): Promise; @@ -101,7 +105,7 @@ export interface Builder { * Generate a server-side manifest to initialise the SvelteKit [server](https://kit.svelte.dev/docs/types#public-types-server) with. * @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated */ - generateManifest(opts: { relativePath: string }): string; + generateManifest(opts: { relativePath: string; routes?: RouteDefinition[] }): string; /** * Resolve a path to the `name` directory inside `outDir`, e.g. `/path/to/.svelte-kit/my-adapter`. From 60162580980c2e9d348b13debf30a3747d1f7a3c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 16:58:17 -0500 Subject: [PATCH 03/10] remove hasRouteLevelConfig --- packages/kit/src/core/adapt/builder.js | 4 ---- packages/kit/types/index.d.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index f0ac7c81fdd9..f5d24636c75e 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -73,10 +73,6 @@ export function create_builder({ prerendered, routes, - hasRouteLevelConfig: - !!server_metadata.routes && - !![...server_metadata.routes.values()].some((route) => route.config), - async compress(directory) { if (!existsSync(directory)) { return; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 2b7ff7733b2a..e667291290a2 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -86,9 +86,6 @@ export interface Builder { /** An array of dynamic (not prerendered) routes */ routes: RouteDefinition[]; - /** `true` if one or more routes have `export const config = ..` in it */ - hasRouteLevelConfig: boolean; - /** * Create separate functions that map to one or more routes of your app. * @param fn A function that groups a set of routes into an entry point From e165e0b8094c15d1b5fd16ef5243fc3f2743c006 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 16:59:33 -0500 Subject: [PATCH 04/10] handle data requests --- packages/adapter-vercel/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 7731294abfa3..bd99c768ed84 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -203,6 +203,8 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) src = '^/?'; } + src += '(?:/__data.json)?$'; + const name = functions.get(pattern); if (name) { static_config.routes.push({ src, dest: `/${name}` }); From 947bb22484a942ca8592859590a4765878438fc2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:01:19 -0500 Subject: [PATCH 05/10] simplify --- packages/adapter-static/index.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 8f859dabcd8e..a57e7b6fee60 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -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.length > 0 && 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 ${ @@ -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. From 0cbbdba37dbe06ae81de854884e5936250acc3fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:12:07 -0500 Subject: [PATCH 06/10] fix types --- packages/adapter-vercel/index.js | 8 ++++---- packages/kit/types/index.d.ts | 11 ++++++++++- packages/kit/types/private.d.ts | 10 ++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index bd99c768ed84..8aecd8ce5ac2 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -39,7 +39,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) /** * @param {string} name * @param {import('.').Config} config - * @param {typeof builder.routes} routes + * @param {import('@sveltejs/kit').RouteDefinition[]} routes */ async function generate_serverless_function(name, config, routes) { const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); @@ -68,7 +68,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) /** * @param {string} name * @param {import('.').Config} config - * @param {typeof builder.routes} routes + * @param {import('@sveltejs/kit').RouteDefinition[]} routes */ async function generate_edge_function(name, config, routes) { const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`); @@ -115,7 +115,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) ); } - /** @type {Map} */ + /** @type {Map[] }>} */ const groups = new Map(); /** @type {Map} */ @@ -158,7 +158,7 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) if (split) { // generate individual functions - /** @type {Map} */ + /** @type {Map[]>} */ const merged = new Map(); for (const route of group.routes) { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index e667291290a2..ed7571680ce2 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -7,13 +7,14 @@ import { CompileOptions } from 'svelte/types/compiler/interfaces'; import { AdapterEntry, CspDirectives, + HttpMethod, Logger, MaybePromise, Prerendered, PrerenderHttpErrorHandlerValue, PrerenderMissingIdHandlerValue, RequestOptions, - RouteDefinition, + RouteSegment, UniqueInterface } from './private.js'; import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js'; @@ -959,6 +960,14 @@ export interface ResolveOptions { preload?(input: { type: 'font' | 'css' | 'js' | 'asset'; path: string }): boolean; } +export interface RouteDefinition { + id: string; + pattern: RegExp; + segments: RouteSegment[]; + methods: HttpMethod[]; + config: Config; +} + export class Server { constructor(manifest: SSRManifest); init(options: ServerInitOptions): Promise; diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts index 395d582cbdf3..29152ae4ffab 100644 --- a/packages/kit/types/private.d.ts +++ b/packages/kit/types/private.d.ts @@ -2,6 +2,8 @@ // but which cannot be imported from `@sveltejs/kit`. Care should // be taken to avoid breaking changes when editing this file +import { RouteDefinition } from './index.js'; + export interface AdapterEntry { /** * A string that uniquely identifies an HTTP service (e.g. serverless function) and is used for deduplication. @@ -215,14 +217,6 @@ export interface RequestOptions { platform?: App.Platform; } -export interface RouteDefinition { - id: string; - pattern: RegExp; - segments: RouteSegment[]; - methods: HttpMethod[]; - config: Config; -} - export interface RouteSegment { content: string; dynamic: boolean; From 2b17a7ecd68c6b0c84577d9e6c24a5af5be5f4c0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:23:29 -0500 Subject: [PATCH 07/10] remove unused code --- packages/adapter-vercel/index.js | 33 -------------------------------- 1 file changed, 33 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 8aecd8ce5ac2..f403226ff9e6 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -229,39 +229,6 @@ function hash_config(config) { return [config.runtime, config.regions, config.memory, config.maxDuration].join('/'); } -/** - * @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; - if (arrays_different(config_a.envVarsInUse, config_b.envVarsInUse)) return false; - - 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 From 0c279e37ea75e40201c81df6308a7300a923b953 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:33:33 -0500 Subject: [PATCH 08/10] fix site --- documentation/docs/20-core-concepts/40-page-options.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md index ab5f57c16e65..e5114b775560 100644 --- a/documentation/docs/20-core-concepts/40-page-options.md +++ b/documentation/docs/20-core-concepts/40-page-options.md @@ -139,11 +139,11 @@ declare module 'some-adapter' { } // @filename: index.js ----cut--- +// ---cut--- /// file: src/routes/+page.js /** @type {import('some-adapter').Config} */ -export const config: Config = { - runtime: 'edge'; +export const config = { + runtime: 'edge' }; ``` From fb0c2e066e310e9fd97a86a1a5c2c52e6d7d059c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:38:04 -0500 Subject: [PATCH 09/10] fix default runtime --- packages/adapter-vercel/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index f403226ff9e6..5d59ccfdec4f 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -5,7 +5,7 @@ import { nodeFileTrace } from '@vercel/nft'; import esbuild from 'esbuild'; const DEFAULTS = { - runtime: 'node18.x', + runtime: 'nodejs18.x', regions: ['iad1'], memory: 128, maxDuration: 30 // TODO check what the defaults actually are From c9efb67dc27f93b6674b320b3f5e010d15ce7fc2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Feb 2023 17:51:22 -0500 Subject: [PATCH 10/10] validate region, default to all for edge --- packages/adapter-vercel/index.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 5d59ccfdec4f..ef58f79982fb 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -4,9 +4,12 @@ import { fileURLToPath } from 'url'; import { nodeFileTrace } from '@vercel/nft'; import esbuild from 'esbuild'; +const VALID_RUNTIMES = ['edge', 'nodejs16.x', 'nodejs18.x']; + +const DEFAULT_RUNTIME = 'nodejs18.x'; +const DEFAULT_REGION = 'iad1'; + const DEFAULTS = { - runtime: 'nodejs18.x', - regions: ['iad1'], memory: 128, maxDuration: 30 // TODO check what the defaults actually are }; @@ -127,7 +130,20 @@ const plugin = function ({ external = [], edge, split, ...default_config } = {}) // group routes by config for (const route of builder.routes) { const pattern = route.pattern.toString(); - const config = { ...DEFAULTS, ...default_config, ...route.config }; + + const runtime = route.config?.runtime ?? default_config?.runtime ?? DEFAULT_RUNTIME; + if (!VALID_RUNTIMES.includes(runtime)) { + throw new Error( + `Invalid runtime '${runtime}' for route ${ + route.id + }. Valid runtimes are ${VALID_RUNTIMES.join(', ')}` + ); + } + + const regions = runtime === 'edge' ? ['all'] : [DEFAULT_REGION]; + + const config = { runtime, regions, ...DEFAULTS, ...default_config, ...route.config }; + const hash = hash_config(config); // first, check there are no routes with incompatible configs that will be merged