@sveltejs/adapter-vercel 1.0.6 → 2.0.1

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/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,16 +1,37 @@
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();
29
+ if (!builder.routes) {
30
+ throw new Error(
31
+ '@sveltejs/adapter-vercel >=2.x (possibly installed through @sveltejs/adapter-auto) requires @sveltejs/kit version 1.5 or higher. ' +
32
+ 'Either downgrade the adapter or upgrade @sveltejs/kit'
33
+ );
34
+ }
14
35
 
15
36
  const dir = '.vercel/output';
16
37
  const tmp = builder.getBuildDirectory('vercel-tmp');
@@ -25,16 +46,16 @@ const plugin = function ({ external = [], edge, split } = {}) {
25
46
  functions: `${dir}/functions`
26
47
  };
27
48
 
28
- const config = static_vercel_config(builder);
49
+ const static_config = static_vercel_config(builder);
29
50
 
30
51
  builder.log.minor('Generating serverless function...');
31
52
 
32
53
  /**
33
54
  * @param {string} name
34
- * @param {string} pattern
35
- * @param {(options: { relativePath: string }) => string} generate_manifest
55
+ * @param {import('.').ServerlessConfig} config
56
+ * @param {import('@sveltejs/kit').RouteDefinition<import('.').Config>[]} routes
36
57
  */
37
- async function generate_serverless_function(name, pattern, generate_manifest) {
58
+ async function generate_serverless_function(name, config, routes) {
38
59
  const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
39
60
 
40
61
  builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
@@ -46,28 +67,49 @@ const plugin = function ({ external = [], edge, split } = {}) {
46
67
 
47
68
  write(
48
69
  `${tmp}/manifest.js`,
49
- `export const manifest = ${generate_manifest({ relativePath })};\n`
70
+ `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
50
71
  );
51
72
 
52
73
  await create_function_bundle(
53
74
  builder,
54
75
  `${tmp}/index.js`,
55
76
  `${dirs.functions}/${name}.func`,
56
- `nodejs${node_version.major}.x`
77
+ config
57
78
  );
58
79
 
59
- config.routes.push({ src: pattern, dest: `/${name}` });
80
+ if (config.isr) {
81
+ write(
82
+ `${dirs.functions}/${name}.prerender-config.json`,
83
+ JSON.stringify(
84
+ {
85
+ expiration: config.isr.expiration,
86
+ group: config.isr.group,
87
+ bypassToken: config.isr.bypassToken,
88
+ allowQuery: config.isr.allowQuery
89
+ },
90
+ null,
91
+ '\t'
92
+ )
93
+ );
94
+ }
60
95
  }
61
96
 
62
97
  /**
63
98
  * @param {string} name
64
- * @param {string} pattern
65
- * @param {(options: { relativePath: string }) => string} generate_manifest
99
+ * @param {import('.').EdgeConfig} config
100
+ * @param {import('@sveltejs/kit').RouteDefinition<import('.').EdgeConfig>[]} routes
66
101
  */
67
- async function generate_edge_function(name, pattern, generate_manifest) {
102
+ async function generate_edge_function(name, config, routes) {
68
103
  const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`);
69
104
  const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
70
105
 
106
+ const envVarsInUse = new Set();
107
+ routes.forEach((route) => {
108
+ route.config?.envVarsInUse?.forEach((x) => {
109
+ envVarsInUse.add(x);
110
+ });
111
+ });
112
+
71
113
  builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, {
72
114
  replace: {
73
115
  SERVER: `${relativePath}/index.js`,
@@ -77,7 +119,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
77
119
 
78
120
  write(
79
121
  `${tmp}/manifest.js`,
80
- `export const manifest = ${generate_manifest({ relativePath })};\n`
122
+ `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
81
123
  );
82
124
 
83
125
  await esbuild.build({
@@ -87,51 +129,122 @@ const plugin = function ({ external = [], edge, split } = {}) {
87
129
  bundle: true,
88
130
  platform: 'browser',
89
131
  format: 'esm',
90
- external,
132
+ external: config.external,
91
133
  sourcemap: 'linked',
92
134
  banner: { js: 'globalThis.global = globalThis;' }
93
135
  });
94
136
 
95
137
  write(
96
138
  `${dirs.functions}/${name}.func/.vc-config.json`,
97
- JSON.stringify({
98
- runtime: 'edge',
99
- entrypoint: 'index.js'
100
- // TODO expose envVarsInUse
101
- })
139
+ JSON.stringify(
140
+ {
141
+ runtime: config.runtime,
142
+ regions: config.regions,
143
+ envVarsInUse: [...envVarsInUse],
144
+ entrypoint: 'index.js'
145
+ },
146
+ null,
147
+ '\t'
148
+ )
149
+ );
150
+ }
151
+
152
+ /** @type {Map<string, { i: number, config: import('.').Config, routes: import('@sveltejs/kit').RouteDefinition<import('.').Config>[] }>} */
153
+ const groups = new Map();
154
+
155
+ /** @type {Map<string, { hash: string, route_id: string }>} */
156
+ const conflicts = new Map();
157
+
158
+ /** @type {Map<string, string>} */
159
+ const functions = new Map();
160
+
161
+ // group routes by config
162
+ for (const route of builder.routes) {
163
+ if (route.prerender === true) continue;
164
+
165
+ const pattern = route.pattern.toString();
166
+
167
+ const runtime = route.config?.runtime ?? defaults?.runtime ?? get_default_runtime();
168
+ if (runtime && !VALID_RUNTIMES.includes(runtime)) {
169
+ throw new Error(
170
+ `Invalid runtime '${runtime}' for route ${
171
+ route.id
172
+ }. Valid runtimes are ${VALID_RUNTIMES.join(', ')}`
173
+ );
174
+ }
175
+
176
+ const config = { runtime, ...defaults, ...route.config };
177
+
178
+ const hash = hash_config(config);
179
+
180
+ // first, check there are no routes with incompatible configs that will be merged
181
+ const existing = conflicts.get(pattern);
182
+ if (existing) {
183
+ if (existing.hash !== hash) {
184
+ throw new Error(
185
+ `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.`
186
+ );
187
+ }
188
+ } else {
189
+ conflicts.set(pattern, { hash, route_id: route.id });
190
+ }
191
+
192
+ // then, create a group for each config
193
+ const id = config.split ? `${hash}-${groups.size}` : hash;
194
+ let group = groups.get(id);
195
+ if (!group) {
196
+ group = { i: groups.size, config, routes: [] };
197
+ groups.set(id, group);
198
+ }
199
+
200
+ group.routes.push(route);
201
+ }
202
+
203
+ for (const group of groups.values()) {
204
+ const generate_function =
205
+ group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function;
206
+
207
+ // generate one function for the group
208
+ const name = `fn-${group.i}`;
209
+ await generate_function(
210
+ name,
211
+ /** @type {any} */ (group.config),
212
+ /** @type {import('@sveltejs/kit').RouteDefinition<any>[]} */ (group.routes)
102
213
  );
103
214
 
104
- config.routes.push({ src: pattern, dest: `/${name}` });
215
+ if (groups.size === 1) {
216
+ // Special case: One function for all routes
217
+ static_config.routes.push({ src: '/.*', dest: `/${name}` });
218
+ } else {
219
+ for (const route of group.routes) {
220
+ functions.set(route.pattern.toString(), name);
221
+ }
222
+ }
105
223
  }
106
224
 
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);
225
+ for (const route of builder.routes) {
226
+ if (route.prerender === true) continue;
227
+
228
+ const pattern = route.pattern.toString();
229
+
230
+ let src = pattern
231
+ // remove leading / and trailing $/
232
+ .slice(1, -2)
233
+ // replace escaped \/ with /
234
+ .replace(/\\\//g, '/');
235
+
236
+ // replace the root route "^/" with "^/?"
237
+ if (src === '^/') {
238
+ src = '^/?';
239
+ }
240
+
241
+ src += '(?:/__data.json)?$';
242
+
243
+ const name = functions.get(pattern);
244
+ if (name) {
245
+ static_config.routes.push({ src, dest: `/${name}` });
246
+ functions.delete(pattern);
247
+ }
135
248
  }
136
249
 
137
250
  builder.log.minor('Copying assets...');
@@ -141,11 +254,26 @@ const plugin = function ({ external = [], edge, split } = {}) {
141
254
 
142
255
  builder.log.minor('Writing routes...');
143
256
 
144
- write(`${dir}/config.json`, JSON.stringify(config, null, ' '));
257
+ write(`${dir}/config.json`, JSON.stringify(static_config, null, '\t'));
145
258
  }
146
259
  };
147
260
  };
148
261
 
262
+ /** @param {import('.').EdgeConfig & import('.').ServerlessConfig} config */
263
+ function hash_config(config) {
264
+ return [
265
+ config.runtime ?? '',
266
+ config.external ?? '',
267
+ config.regions ?? '',
268
+ config.memory ?? '',
269
+ config.maxDuration ?? '',
270
+ config.isr?.expiration ?? '',
271
+ config.isr?.group ?? '',
272
+ config.isr?.bypassToken ?? '',
273
+ config.isr?.allowQuery ?? ''
274
+ ].join('/');
275
+ }
276
+
149
277
  /**
150
278
  * @param {string} file
151
279
  * @param {string} data
@@ -160,19 +288,6 @@ function write(file, data) {
160
288
  fs.writeFileSync(file, data);
161
289
  }
162
290
 
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
291
  // This function is duplicated in adapter-static
177
292
  /** @param {import('@sveltejs/kit').Builder} builder */
178
293
  function static_vercel_config(builder) {
@@ -235,9 +350,9 @@ function static_vercel_config(builder) {
235
350
  * @param {import('@sveltejs/kit').Builder} builder
236
351
  * @param {string} entry
237
352
  * @param {string} dir
238
- * @param {string} runtime
353
+ * @param {import('.').ServerlessConfig} config
239
354
  */
240
- async function create_function_bundle(builder, entry, dir, runtime) {
355
+ async function create_function_bundle(builder, entry, dir, config) {
241
356
  fs.rmSync(dir, { force: true, recursive: true });
242
357
 
243
358
  let base = entry;
@@ -265,7 +380,7 @@ async function create_function_bundle(builder, entry, dir, runtime) {
265
380
  resolution_failures.set(importer, []);
266
381
  }
267
382
 
268
- resolution_failures.get(importer).push(module);
383
+ /** @type {string[]} */ (resolution_failures.get(importer)).push(module);
269
384
  } else {
270
385
  throw error;
271
386
  }
@@ -286,7 +401,8 @@ async function create_function_bundle(builder, entry, dir, runtime) {
286
401
  }
287
402
 
288
403
  // find common ancestor directory
289
- let common_parts;
404
+ /** @type {string[]} */
405
+ let common_parts = [];
290
406
 
291
407
  for (const file of traced.fileList) {
292
408
  if (common_parts) {
@@ -330,11 +446,18 @@ async function create_function_bundle(builder, entry, dir, runtime) {
330
446
 
331
447
  write(
332
448
  `${dir}/.vc-config.json`,
333
- JSON.stringify({
334
- runtime,
335
- handler: path.relative(base + ancestor, entry),
336
- launcherType: 'Nodejs'
337
- })
449
+ JSON.stringify(
450
+ {
451
+ runtime: config.runtime,
452
+ regions: config.regions,
453
+ memory: config.memory,
454
+ maxDuration: config.maxDuration,
455
+ handler: path.relative(base + ancestor, entry),
456
+ launcherType: 'Nodejs'
457
+ },
458
+ null,
459
+ '\t'
460
+ )
338
461
  );
339
462
 
340
463
  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.6",
3
+ "version": "2.0.1",
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.3.4"
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",