@sveltejs/adapter-vercel 1.0.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ If you're using [adapter-auto](https://kit.svelte.dev/docs/adapter-auto), you do
6
6
 
7
7
  ## Docs
8
8
 
9
- [Docs](https://kit.svelte.dev/docs/adapter-cloudflare)
9
+ [Docs](https://kit.svelte.dev/docs/adapter-vercel)
10
10
 
11
11
  ## Changelog
12
12
 
package/files/edge.js CHANGED
@@ -3,7 +3,7 @@ import { manifest } from 'MANIFEST';
3
3
 
4
4
  const server = new Server(manifest);
5
5
  const initialized = server.init({
6
- env: process.env
6
+ env: /** @type {Record<string, string>} */ (process.env)
7
7
  });
8
8
 
9
9
  /**
@@ -13,7 +13,7 @@ export default async (request) => {
13
13
  await initialized;
14
14
  return server.respond(request, {
15
15
  getClientAddress() {
16
- return request.headers.get('x-forwarded-for');
16
+ return /** @type {string} */ (request.headers.get('x-forwarded-for'));
17
17
  }
18
18
  });
19
19
  };
@@ -8,7 +8,7 @@ installPolyfills();
8
8
  const server = new Server(manifest);
9
9
 
10
10
  await server.init({
11
- env: process.env
11
+ env: /** @type {Record<string, string>} */ (process.env)
12
12
  });
13
13
 
14
14
  /**
@@ -22,7 +22,7 @@ export default async (req, res) => {
22
22
  try {
23
23
  request = await getRequest({ base: `https://${req.headers.host}`, request: req });
24
24
  } catch (err) {
25
- res.statusCode = err.status || 400;
25
+ res.statusCode = /** @type {any} */ (err).status || 400;
26
26
  return res.end('Invalid request body');
27
27
  }
28
28
 
@@ -30,7 +30,7 @@ export default async (req, res) => {
30
30
  res,
31
31
  await server.respond(request, {
32
32
  getClientAddress() {
33
- return request.headers.get('x-forwarded-for');
33
+ return /** @type {string} */ (request.headers.get('x-forwarded-for'));
34
34
  }
35
35
  })
36
36
  );
package/index.d.ts CHANGED
@@ -1,9 +1,83 @@
1
1
  import { Adapter } from '@sveltejs/kit';
2
2
 
3
- type Options = {
4
- edge?: boolean;
3
+ export default function plugin(config?: Config): Adapter;
4
+
5
+ export interface ServerlessConfig {
6
+ /**
7
+ * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions)
8
+ * @default 'nodejs18.x'
9
+ */
10
+ runtime?: 'nodejs16.x' | 'nodejs18.x';
11
+ /**
12
+ * To which regions to deploy the app. A list of regions.
13
+ * More info: https://vercel.com/docs/concepts/edge-network/regions
14
+ */
15
+ regions?: string[];
16
+ /**
17
+ * Maximum execution duration (in seconds) that will be allowed for the Serverless Function.
18
+ * Serverless only.
19
+ */
20
+ maxDuration?: number;
21
+ /**
22
+ * Amount of memory (RAM in MB) that will be allocated to the Serverless Function.
23
+ * Serverless only.
24
+ */
25
+ memory?: number;
26
+ /**
27
+ * If `true`, this route will always be deployed as its own separate function
28
+ */
29
+ split?: boolean;
30
+ /**
31
+ * [Incremental Static Regeneration](https://vercel.com/docs/concepts/incremental-static-regeneration/overview) configuration.
32
+ * Serverless only.
33
+ */
34
+ isr?: {
35
+ /**
36
+ * 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.
37
+ */
38
+ expiration?: number | false;
39
+ /**
40
+ * Option group number of the asset. Assets with the same group number will all be re-validated at the same time.
41
+ */
42
+ group?: number;
43
+ /**
44
+ * Random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset
45
+ * with a __prerender_bypass=<token> cookie.
46
+ *
47
+ * Making a `GET` or `HEAD` request with `x-prerender-revalidate: <token>` will force the asset to be re-validated.
48
+ */
49
+ bypassToken?: string;
50
+ /**
51
+ * 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
52
+ */
53
+ allowQuery?: string[] | undefined;
54
+ };
55
+ }
56
+
57
+ export interface EdgeConfig {
58
+ /**
59
+ * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions)
60
+ */
61
+ runtime?: 'edge';
62
+ /**
63
+ * To which regions to deploy the app. A list of regions or `'all'`.
64
+ * More info: https://vercel.com/docs/concepts/edge-network/regions
65
+ */
66
+ regions?: string[] | 'all';
67
+ /**
68
+ * List of environment variable names that will be available for the Edge Function to utilize.
69
+ * Edge only.
70
+ */
71
+ envVarsInUse?: string[];
72
+ /**
73
+ * List of packages that should not be bundled into the Edge Function.
74
+ * Edge only.
75
+ */
5
76
  external?: string[];
77
+ /**
78
+ * If `true`, this route will always be deployed as its own separate function
79
+ */
6
80
  split?: boolean;
7
- };
81
+ }
8
82
 
9
- export default function plugin(options?: Options): Adapter;
83
+ export type Config = EdgeConfig | ServerlessConfig;
package/index.js CHANGED
@@ -1,17 +1,31 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
4
  import { nodeFileTrace } from '@vercel/nft';
5
5
  import esbuild from 'esbuild';
6
6
 
7
+ const VALID_RUNTIMES = ['edge', 'nodejs16.x', 'nodejs18.x'];
8
+
9
+ const get_default_runtime = () => {
10
+ const major = process.version.slice(1).split('.')[0];
11
+ if (major === '16') return 'nodejs16.x';
12
+ if (major === '18') return 'nodejs18.x';
13
+
14
+ throw new Error(
15
+ `Unsupported Node.js version: ${process.version}. Please use Node 16 or Node 18 to build your project, or explicitly specify a runtime in your adapter configuration.`
16
+ );
17
+ };
18
+
7
19
  /** @type {import('.').default} **/
8
- const plugin = function ({ external = [], edge, split } = {}) {
20
+ const plugin = function (defaults = {}) {
21
+ if ('edge' in defaults) {
22
+ throw new Error("{ edge: true } has been removed in favour of { runtime: 'edge' }");
23
+ }
24
+
9
25
  return {
10
26
  name: '@sveltejs/adapter-vercel',
11
27
 
12
28
  async adapt(builder) {
13
- const node_version = get_node_version();
14
-
15
29
  const dir = '.vercel/output';
16
30
  const tmp = builder.getBuildDirectory('vercel-tmp');
17
31
 
@@ -25,16 +39,16 @@ const plugin = function ({ external = [], edge, split } = {}) {
25
39
  functions: `${dir}/functions`
26
40
  };
27
41
 
28
- const config = static_vercel_config(builder);
42
+ const static_config = static_vercel_config(builder);
29
43
 
30
44
  builder.log.minor('Generating serverless function...');
31
45
 
32
46
  /**
33
47
  * @param {string} name
34
- * @param {string} pattern
35
- * @param {(options: { relativePath: string }) => string} generate_manifest
48
+ * @param {import('.').ServerlessConfig} config
49
+ * @param {import('@sveltejs/kit').RouteDefinition<import('.').Config>[]} routes
36
50
  */
37
- async function generate_serverless_function(name, pattern, generate_manifest) {
51
+ async function generate_serverless_function(name, config, routes) {
38
52
  const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
39
53
 
40
54
  builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
@@ -46,28 +60,49 @@ const plugin = function ({ external = [], edge, split } = {}) {
46
60
 
47
61
  write(
48
62
  `${tmp}/manifest.js`,
49
- `export const manifest = ${generate_manifest({ relativePath })};\n`
63
+ `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
50
64
  );
51
65
 
52
66
  await create_function_bundle(
53
67
  builder,
54
68
  `${tmp}/index.js`,
55
69
  `${dirs.functions}/${name}.func`,
56
- `nodejs${node_version.major}.x`
70
+ config
57
71
  );
58
72
 
59
- config.routes.push({ src: pattern, dest: `/${name}` });
73
+ if (config.isr) {
74
+ write(
75
+ `${dirs.functions}/${name}.prerender-config.json`,
76
+ JSON.stringify(
77
+ {
78
+ expiration: config.isr.expiration,
79
+ group: config.isr.group,
80
+ bypassToken: config.isr.bypassToken,
81
+ allowQuery: config.isr.allowQuery
82
+ },
83
+ null,
84
+ '\t'
85
+ )
86
+ );
87
+ }
60
88
  }
61
89
 
62
90
  /**
63
91
  * @param {string} name
64
- * @param {string} pattern
65
- * @param {(options: { relativePath: string }) => string} generate_manifest
92
+ * @param {import('.').EdgeConfig} config
93
+ * @param {import('@sveltejs/kit').RouteDefinition<import('.').EdgeConfig>[]} routes
66
94
  */
67
- async function generate_edge_function(name, pattern, generate_manifest) {
95
+ async function generate_edge_function(name, config, routes) {
68
96
  const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`);
69
97
  const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
70
98
 
99
+ const envVarsInUse = new Set();
100
+ routes.forEach((route) => {
101
+ route.config?.envVarsInUse?.forEach((x) => {
102
+ envVarsInUse.add(x);
103
+ });
104
+ });
105
+
71
106
  builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, {
72
107
  replace: {
73
108
  SERVER: `${relativePath}/index.js`,
@@ -77,7 +112,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
77
112
 
78
113
  write(
79
114
  `${tmp}/manifest.js`,
80
- `export const manifest = ${generate_manifest({ relativePath })};\n`
115
+ `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
81
116
  );
82
117
 
83
118
  await esbuild.build({
@@ -87,51 +122,122 @@ const plugin = function ({ external = [], edge, split } = {}) {
87
122
  bundle: true,
88
123
  platform: 'browser',
89
124
  format: 'esm',
90
- external,
125
+ external: config.external,
91
126
  sourcemap: 'linked',
92
127
  banner: { js: 'globalThis.global = globalThis;' }
93
128
  });
94
129
 
95
130
  write(
96
131
  `${dirs.functions}/${name}.func/.vc-config.json`,
97
- JSON.stringify({
98
- runtime: 'edge',
99
- entrypoint: 'index.js'
100
- // TODO expose envVarsInUse
101
- })
132
+ JSON.stringify(
133
+ {
134
+ runtime: config.runtime,
135
+ regions: config.regions,
136
+ envVarsInUse: [...envVarsInUse],
137
+ entrypoint: 'index.js'
138
+ },
139
+ null,
140
+ '\t'
141
+ )
102
142
  );
143
+ }
144
+
145
+ /** @type {Map<string, { i: number, config: import('.').Config, routes: import('@sveltejs/kit').RouteDefinition<import('.').Config>[] }>} */
146
+ const groups = new Map();
147
+
148
+ /** @type {Map<string, { hash: string, route_id: string }>} */
149
+ const conflicts = new Map();
150
+
151
+ /** @type {Map<string, string>} */
152
+ const functions = new Map();
103
153
 
104
- config.routes.push({ src: pattern, dest: `/${name}` });
154
+ // group routes by config
155
+ for (const route of builder.routes) {
156
+ if (route.prerender === true) continue;
157
+
158
+ const pattern = route.pattern.toString();
159
+
160
+ const runtime = route.config?.runtime ?? defaults?.runtime ?? get_default_runtime();
161
+ if (runtime && !VALID_RUNTIMES.includes(runtime)) {
162
+ throw new Error(
163
+ `Invalid runtime '${runtime}' for route ${
164
+ route.id
165
+ }. Valid runtimes are ${VALID_RUNTIMES.join(', ')}`
166
+ );
167
+ }
168
+
169
+ const config = { runtime, ...defaults, ...route.config };
170
+
171
+ const hash = hash_config(config);
172
+
173
+ // first, check there are no routes with incompatible configs that will be merged
174
+ const existing = conflicts.get(pattern);
175
+ if (existing) {
176
+ if (existing.hash !== hash) {
177
+ throw new Error(
178
+ `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.`
179
+ );
180
+ }
181
+ } else {
182
+ conflicts.set(pattern, { hash, route_id: route.id });
183
+ }
184
+
185
+ // then, create a group for each config
186
+ const id = config.split ? `${hash}-${groups.size}` : hash;
187
+ let group = groups.get(id);
188
+ if (!group) {
189
+ group = { i: groups.size, config, routes: [] };
190
+ groups.set(id, group);
191
+ }
192
+
193
+ group.routes.push(route);
105
194
  }
106
195
 
107
- const generate_function = edge ? generate_edge_function : generate_serverless_function;
108
-
109
- if (split) {
110
- await builder.createEntries((route) => {
111
- return {
112
- id: route.pattern.toString(), // TODO is `id` necessary?
113
- filter: (other) => route.pattern.toString() === other.pattern.toString(),
114
- complete: async (entry) => {
115
- let sliced_pattern = route.pattern
116
- .toString()
117
- // remove leading / and trailing $/
118
- .slice(1, -2)
119
- // replace escaped \/ with /
120
- .replace(/\\\//g, '/');
121
-
122
- // replace the root route "^/" with "^/?"
123
- if (sliced_pattern === '^/') {
124
- sliced_pattern = '^/?';
125
- }
126
-
127
- const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes
128
-
129
- await generate_function(route.id.slice(1) || 'index', src, entry.generateManifest);
130
- }
131
- };
132
- });
133
- } else {
134
- await generate_function('render', '/.*', builder.generateManifest);
196
+ for (const group of groups.values()) {
197
+ const generate_function =
198
+ group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function;
199
+
200
+ // generate one function for the group
201
+ const name = `fn-${group.i}`;
202
+ await generate_function(
203
+ name,
204
+ /** @type {any} */ (group.config),
205
+ /** @type {import('@sveltejs/kit').RouteDefinition<any>[]} */ (group.routes)
206
+ );
207
+
208
+ if (groups.size === 1) {
209
+ // Special case: One function for all routes
210
+ static_config.routes.push({ src: '/.*', dest: `/${name}` });
211
+ } else {
212
+ for (const route of group.routes) {
213
+ functions.set(route.pattern.toString(), name);
214
+ }
215
+ }
216
+ }
217
+
218
+ for (const route of builder.routes) {
219
+ if (route.prerender === true) continue;
220
+
221
+ const pattern = route.pattern.toString();
222
+
223
+ let src = pattern
224
+ // remove leading / and trailing $/
225
+ .slice(1, -2)
226
+ // replace escaped \/ with /
227
+ .replace(/\\\//g, '/');
228
+
229
+ // replace the root route "^/" with "^/?"
230
+ if (src === '^/') {
231
+ src = '^/?';
232
+ }
233
+
234
+ src += '(?:/__data.json)?$';
235
+
236
+ const name = functions.get(pattern);
237
+ if (name) {
238
+ static_config.routes.push({ src, dest: `/${name}` });
239
+ functions.delete(pattern);
240
+ }
135
241
  }
136
242
 
137
243
  builder.log.minor('Copying assets...');
@@ -141,11 +247,26 @@ const plugin = function ({ external = [], edge, split } = {}) {
141
247
 
142
248
  builder.log.minor('Writing routes...');
143
249
 
144
- write(`${dir}/config.json`, JSON.stringify(config, null, ' '));
250
+ write(`${dir}/config.json`, JSON.stringify(static_config, null, '\t'));
145
251
  }
146
252
  };
147
253
  };
148
254
 
255
+ /** @param {import('.').EdgeConfig & import('.').ServerlessConfig} config */
256
+ function hash_config(config) {
257
+ return [
258
+ config.runtime ?? '',
259
+ config.external ?? '',
260
+ config.regions ?? '',
261
+ config.memory ?? '',
262
+ config.maxDuration ?? '',
263
+ config.isr?.expiration ?? '',
264
+ config.isr?.group ?? '',
265
+ config.isr?.bypassToken ?? '',
266
+ config.isr?.allowQuery ?? ''
267
+ ].join('/');
268
+ }
269
+
149
270
  /**
150
271
  * @param {string} file
151
272
  * @param {string} data
@@ -160,19 +281,6 @@ function write(file, data) {
160
281
  fs.writeFileSync(file, data);
161
282
  }
162
283
 
163
- function get_node_version() {
164
- const full = process.version.slice(1); // 'v16.5.0' --> '16.5.0'
165
- const major = parseInt(full.split('.')[0]); // '16.5.0' --> 16
166
-
167
- if (major < 16) {
168
- throw new Error(
169
- `SvelteKit only supports Node.js version 16 or greater (currently using v${full}). Consult the documentation: https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-version`
170
- );
171
- }
172
-
173
- return { major, full };
174
- }
175
-
176
284
  // This function is duplicated in adapter-static
177
285
  /** @param {import('@sveltejs/kit').Builder} builder */
178
286
  function static_vercel_config(builder) {
@@ -193,16 +301,24 @@ function static_vercel_config(builder) {
193
301
  }
194
302
 
195
303
  for (const [path, page] of builder.prerendered.pages) {
196
- if (path.endsWith('/') && path !== '/') {
304
+ let overrides_path = path.slice(1);
305
+
306
+ if (path !== '/') {
307
+ /** @type {string | undefined} */
308
+ let counterpart_route = path + '/';
309
+
310
+ if (path.endsWith('/')) {
311
+ counterpart_route = path.slice(0, -1);
312
+ overrides_path = path.slice(1, -1);
313
+ }
314
+
197
315
  prerendered_redirects.push(
198
- { src: path, dest: path.slice(0, -1) },
199
- { src: path.slice(0, -1), status: 308, headers: { Location: path } }
316
+ { src: path, dest: counterpart_route },
317
+ { src: counterpart_route, status: 308, headers: { Location: path } }
200
318
  );
201
-
202
- overrides[page.file] = { path: path.slice(1, -1) };
203
- } else {
204
- overrides[page.file] = { path: path.slice(1) };
205
319
  }
320
+
321
+ overrides[page.file] = { path: overrides_path };
206
322
  }
207
323
 
208
324
  return {
@@ -227,9 +343,9 @@ function static_vercel_config(builder) {
227
343
  * @param {import('@sveltejs/kit').Builder} builder
228
344
  * @param {string} entry
229
345
  * @param {string} dir
230
- * @param {string} runtime
346
+ * @param {import('.').ServerlessConfig} config
231
347
  */
232
- async function create_function_bundle(builder, entry, dir, runtime) {
348
+ async function create_function_bundle(builder, entry, dir, config) {
233
349
  fs.rmSync(dir, { force: true, recursive: true });
234
350
 
235
351
  let base = entry;
@@ -257,7 +373,7 @@ async function create_function_bundle(builder, entry, dir, runtime) {
257
373
  resolution_failures.set(importer, []);
258
374
  }
259
375
 
260
- resolution_failures.get(importer).push(module);
376
+ /** @type {string[]} */ (resolution_failures.get(importer)).push(module);
261
377
  } else {
262
378
  throw error;
263
379
  }
@@ -278,7 +394,8 @@ async function create_function_bundle(builder, entry, dir, runtime) {
278
394
  }
279
395
 
280
396
  // find common ancestor directory
281
- let common_parts;
397
+ /** @type {string[]} */
398
+ let common_parts = [];
282
399
 
283
400
  for (const file of traced.fileList) {
284
401
  if (common_parts) {
@@ -322,11 +439,18 @@ async function create_function_bundle(builder, entry, dir, runtime) {
322
439
 
323
440
  write(
324
441
  `${dir}/.vc-config.json`,
325
- JSON.stringify({
326
- runtime,
327
- handler: path.relative(base + ancestor, entry),
328
- launcherType: 'Nodejs'
329
- })
442
+ JSON.stringify(
443
+ {
444
+ runtime: config.runtime,
445
+ regions: config.regions,
446
+ memory: config.memory,
447
+ maxDuration: config.maxDuration,
448
+ handler: path.relative(base + ancestor, entry),
449
+ launcherType: 'Nodejs'
450
+ },
451
+ null,
452
+ '\t'
453
+ )
330
454
  );
331
455
 
332
456
  write(`${dir}/package.json`, JSON.stringify({ type: 'module' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/adapter-vercel",
3
- "version": "1.0.5",
3
+ "version": "2.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -29,10 +29,10 @@
29
29
  "devDependencies": {
30
30
  "@types/node": "^16.18.6",
31
31
  "typescript": "^4.9.4",
32
- "@sveltejs/kit": "^1.1.3"
32
+ "@sveltejs/kit": "^1.5.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@sveltejs/kit": "^1.0.0"
35
+ "@sveltejs/kit": "^1.5.0"
36
36
  },
37
37
  "scripts": {
38
38
  "lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",