@netlify/edge-bundler 8.18.0 → 8.19.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.
@@ -1,4 +1,5 @@
1
- export const PUBLIC_SPECIFIER = 'netlify:edge'
1
+ export const LEGACY_PUBLIC_SPECIFIER = 'netlify:edge'
2
+ export const PUBLIC_SPECIFIER = '@netlify/edge-functions'
2
3
  export const STAGE1_SPECIFIER = 'netlify:bootstrap-stage1'
3
4
  export const STAGE2_SPECIFIER = 'netlify:bootstrap-stage2'
4
5
  export const virtualRoot = 'file:///root/'
@@ -4,7 +4,7 @@ import * as path from 'https://deno.land/std@0.177.0/path/mod.ts'
4
4
 
5
5
  import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts'
6
6
  import { importMapSpecifier, virtualRoot } from '../../shared/consts.ts'
7
- import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
7
+ import { LEGACY_PUBLIC_SPECIFIER, PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
8
8
  import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
9
9
 
10
10
  interface FunctionReference {
@@ -75,7 +75,12 @@ const stage2Loader = (basePath: string, functions: InputFunction[], externals: S
75
75
  return inlineModule(specifier, importMapData)
76
76
  }
77
77
 
78
- if (specifier === PUBLIC_SPECIFIER || externals.has(specifier) || specifier.startsWith('node:')) {
78
+ if (
79
+ specifier === LEGACY_PUBLIC_SPECIFIER ||
80
+ specifier === PUBLIC_SPECIFIER ||
81
+ externals.has(specifier) ||
82
+ specifier.startsWith('node:')
83
+ ) {
79
84
  return {
80
85
  kind: 'external',
81
86
  specifier,
@@ -6,6 +6,7 @@ export declare const enum Cache {
6
6
  Off = "off",
7
7
  Manual = "manual"
8
8
  }
9
+ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
9
10
  export type Path = `/${string}`;
10
11
  export type OnError = 'fail' | 'bypass' | Path;
11
12
  export declare const isValidOnError: (value: unknown) => value is OnError;
@@ -16,6 +17,7 @@ export interface FunctionConfig {
16
17
  onError?: OnError;
17
18
  name?: string;
18
19
  generator?: string;
20
+ method?: HTTPMethod | HTTPMethod[];
19
21
  }
20
22
  export declare const getFunctionConfig: ({ func, importMap, deno, bootstrapURL, log, }: {
21
23
  func: EdgeFunction;
@@ -151,12 +151,12 @@ test('Loads function paths from the in-source `config` function', async () => {
151
151
  {
152
152
  function: 'user-func2',
153
153
  path: '/user-func2',
154
+ method: ['PATCH'],
154
155
  },
155
156
  ];
156
157
  const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
157
158
  basePath,
158
159
  configPath: join(internalDirectory, 'config.json'),
159
- featureFlags: { edge_functions_path_urlpattern: true },
160
160
  });
161
161
  const generatedFiles = await fs.readdir(distPath);
162
162
  expect(result.functions.length).toBe(7);
@@ -179,6 +179,7 @@ test('Loads function paths from the in-source `config` function', async () => {
179
179
  pattern: '^/user-func2/?$',
180
180
  excluded_patterns: [],
181
181
  path: '/user-func2',
182
+ methods: ['PATCH'],
182
183
  });
183
184
  expect(routes[2]).toEqual({
184
185
  function: 'framework-func1',
@@ -203,6 +204,7 @@ test('Loads function paths from the in-source `config` function', async () => {
203
204
  pattern: '^/user-func5(?:/(.*))/?$',
204
205
  excluded_patterns: [],
205
206
  path: '/user-func5/*',
207
+ methods: ['GET'],
206
208
  });
207
209
  expect(postCacheRoutes.length).toBe(1);
208
210
  expect(postCacheRoutes[0]).toEqual({
@@ -210,6 +212,7 @@ test('Loads function paths from the in-source `config` function', async () => {
210
212
  pattern: '^/user-func4/?$',
211
213
  excluded_patterns: [],
212
214
  path: '/user-func4',
215
+ methods: ['POST', 'PUT'],
213
216
  });
214
217
  expect(Object.keys(functionConfig)).toHaveLength(1);
215
218
  expect(functionConfig['user-func5']).toEqual({
@@ -1,8 +1,9 @@
1
- import { FunctionConfig, Path } from './config.js';
1
+ import { FunctionConfig, HTTPMethod, Path } from './config.js';
2
2
  import { FeatureFlags } from './feature_flags.js';
3
3
  interface BaseDeclaration {
4
4
  cache?: string;
5
5
  function: string;
6
+ method?: HTTPMethod | HTTPMethod[];
6
7
  name?: string;
7
8
  generator?: string;
8
9
  }
@@ -48,7 +48,7 @@ const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsV
48
48
  const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited) => {
49
49
  const declarations = [];
50
50
  for (const name in functionConfigs) {
51
- const { cache, path } = functionConfigs[name];
51
+ const { cache, path, method } = functionConfigs[name];
52
52
  // If we have a path specified, create a declaration for each path.
53
53
  if (!functionsVisited.has(name) && path) {
54
54
  const paths = Array.isArray(path) ? path : [path];
@@ -57,6 +57,9 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
57
57
  if (cache) {
58
58
  declaration.cache = cache;
59
59
  }
60
+ if (method) {
61
+ declaration.method = method;
62
+ }
60
63
  declarations.push(declaration);
61
64
  });
62
65
  }
@@ -1,12 +1,10 @@
1
1
  declare const defaultFlags: {
2
2
  edge_functions_fail_unsupported_regex: boolean;
3
- edge_functions_path_urlpattern: boolean;
4
3
  };
5
4
  type FeatureFlag = keyof typeof defaultFlags;
6
5
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
7
6
  declare const getFlags: (input?: Record<string, boolean>, flags?: {
8
7
  edge_functions_fail_unsupported_regex: boolean;
9
- edge_functions_path_urlpattern: boolean;
10
8
  }) => FeatureFlags;
11
9
  export { defaultFlags, getFlags };
12
10
  export type { FeatureFlag, FeatureFlags };
@@ -1,6 +1,5 @@
1
1
  const defaultFlags = {
2
2
  edge_functions_fail_unsupported_regex: false,
3
- edge_functions_path_urlpattern: false,
4
3
  };
5
4
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
6
5
  ...result,
@@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { parse } from '@import-maps/resolve';
6
6
  import { isFileNotFoundError } from './utils/error.js';
7
7
  const INTERNAL_IMPORTS = {
8
+ '@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
8
9
  'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
9
10
  };
10
11
  // ImportMap can take several import map files and merge them into a final
@@ -69,6 +69,7 @@ describe('Returns the fully resolved import map', () => {
69
69
  specifier3: 'file:///different/full/path/file3.js',
70
70
  specifier2: 'file:///some/full/path/file2.js',
71
71
  specifier1: 'file:///some/full/path/file.js',
72
+ '@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
72
73
  'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
73
74
  });
74
75
  expect(scopes).toStrictEqual({
@@ -90,6 +91,7 @@ describe('Returns the fully resolved import map', () => {
90
91
  specifier3: 'file:///vendor/full/path/file3.js',
91
92
  specifier2: 'file:///root/full/path/file2.js',
92
93
  specifier1: 'file:///root/full/path/file.js',
94
+ '@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
93
95
  'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
94
96
  });
95
97
  expect(scopes).toStrictEqual({
@@ -9,6 +9,7 @@ interface Route {
9
9
  pattern: string;
10
10
  excluded_patterns: string[];
11
11
  path?: string;
12
+ methods?: string[];
12
13
  }
13
14
  interface EdgeFunctionConfig {
14
15
  excluded_patterns: string[];
@@ -1,6 +1,5 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { join } from 'path';
3
- import globToRegExp from 'glob-to-regexp';
4
3
  import { wrapBundleError } from './bundle_error.js';
5
4
  import { parsePattern } from './declaration.js';
6
5
  import { getPackageVersion } from './package_json.js';
@@ -27,13 +26,26 @@ const sanitizeEdgeFunctionConfig = (config) => {
27
26
  }
28
27
  return newConfig;
29
28
  };
30
- const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath, featureFlags) => {
29
+ const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
31
30
  if (excludedPath) {
32
31
  const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
33
- const excludedPatterns = paths.map((path) => pathToRegularExpression(path, featureFlags)).map(serializePattern);
32
+ const excludedPatterns = paths.map((path) => pathToRegularExpression(path)).map(serializePattern);
34
33
  manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
35
34
  }
36
35
  };
36
+ /**
37
+ * Normalizes method names into arrays of uppercase strings.
38
+ * (e.g. "get" becomes ["GET"])
39
+ */
40
+ const normalizeMethods = (method, name) => {
41
+ const methods = Array.isArray(method) ? method : [method];
42
+ return methods.map((method) => {
43
+ if (typeof method !== 'string') {
44
+ throw new TypeError(`Could not parse method declaration of function '${name}'. Expecting HTTP Method, got ${method}`);
45
+ }
46
+ return method.toUpperCase();
47
+ });
48
+ };
37
49
  const generateManifest = ({ bundles = [], declarations = [], featureFlags, functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], }) => {
38
50
  const preCacheRoutes = [];
39
51
  const postCacheRoutes = [];
@@ -43,7 +55,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
43
55
  if (manifestFunctionConfig[name] === undefined) {
44
56
  continue;
45
57
  }
46
- addExcludedPatterns(name, manifestFunctionConfig, excludedPath, featureFlags);
58
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
47
59
  manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError };
48
60
  }
49
61
  for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
@@ -51,7 +63,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
51
63
  if (manifestFunctionConfig[name] === undefined) {
52
64
  continue;
53
65
  }
54
- addExcludedPatterns(name, manifestFunctionConfig, excludedPath, featureFlags);
66
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
55
67
  manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest };
56
68
  }
57
69
  declarations.forEach((declaration) => {
@@ -66,6 +78,9 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
66
78
  pattern: serializePattern(pattern),
67
79
  excluded_patterns: excludedPattern.map(serializePattern),
68
80
  };
81
+ if ('method' in declaration) {
82
+ route.methods = normalizeMethods(declaration.method, func.name);
83
+ }
69
84
  if ('path' in declaration) {
70
85
  route.path = declaration.path;
71
86
  }
@@ -91,31 +106,21 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
91
106
  };
92
107
  return manifest;
93
108
  };
94
- const pathToRegularExpression = (path, featureFlags) => {
95
- if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_path_urlpattern) {
96
- try {
97
- const pattern = new ExtendedURLPattern({ pathname: path });
98
- // Removing the `^` and `$` delimiters because we'll need to modify what's
99
- // between them.
100
- const source = pattern.regexp.pathname.source.slice(1, -1);
101
- // Wrapping the expression source with `^` and `$`. Also, adding an optional
102
- // trailing slash, so that a declaration of `path: "/foo"` matches requests
103
- // for both `/foo` and `/foo/`.
104
- const normalizedSource = `^${source}\\/?$`;
105
- return normalizedSource;
106
- }
107
- catch (error) {
108
- throw wrapBundleError(error);
109
- }
109
+ const pathToRegularExpression = (path) => {
110
+ try {
111
+ const pattern = new ExtendedURLPattern({ pathname: path });
112
+ // Removing the `^` and `$` delimiters because we'll need to modify what's
113
+ // between them.
114
+ const source = pattern.regexp.pathname.source.slice(1, -1);
115
+ // Wrapping the expression source with `^` and `$`. Also, adding an optional
116
+ // trailing slash, so that a declaration of `path: "/foo"` matches requests
117
+ // for both `/foo` and `/foo/`.
118
+ const normalizedSource = `^${source}\\/?$`;
119
+ return normalizedSource;
120
+ }
121
+ catch (error) {
122
+ throw wrapBundleError(error);
110
123
  }
111
- // We use the global flag so that `globToRegExp` will not wrap the expression
112
- // with `^` and `$`. We'll do that ourselves.
113
- const regularExpression = globToRegExp(path, { flags: 'g' });
114
- // Wrapping the expression source with `^` and `$`. Also, adding an optional
115
- // trailing slash, so that a declaration of `path: "/foo"` matches requests
116
- // for both `/foo` and `/foo/`.
117
- const normalizedSource = `^${regularExpression.source}\\/?$`;
118
- return normalizedSource;
119
124
  };
120
125
  const getRegularExpression = (declaration, featureFlags) => {
121
126
  if ('pattern' in declaration) {
@@ -131,7 +136,7 @@ const getRegularExpression = (declaration, featureFlags) => {
131
136
  return declaration.pattern;
132
137
  }
133
138
  }
134
- return pathToRegularExpression(declaration.path, featureFlags);
139
+ return pathToRegularExpression(declaration.path);
135
140
  };
136
141
  const getExcludedRegularExpressions = (declaration, featureFlags) => {
137
142
  if ('excludedPattern' in declaration && declaration.excludedPattern) {
@@ -153,7 +158,7 @@ const getExcludedRegularExpressions = (declaration, featureFlags) => {
153
158
  }
154
159
  if ('path' in declaration && declaration.excludedPath) {
155
160
  const paths = Array.isArray(declaration.excludedPath) ? declaration.excludedPath : [declaration.excludedPath];
156
- return paths.map((path) => pathToRegularExpression(path, featureFlags));
161
+ return paths.map((path) => pathToRegularExpression(path));
157
162
  }
158
163
  return [];
159
164
  };
@@ -40,7 +40,6 @@ test('Generates a manifest with display names', () => {
40
40
  declarations,
41
41
  functions,
42
42
  internalFunctionConfig,
43
- featureFlags: { edge_functions_path_urlpattern: true },
44
43
  });
45
44
  const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
46
45
  expect(manifest.function_config).toEqual({
@@ -62,7 +61,6 @@ test('Generates a manifest with a generator field', () => {
62
61
  declarations,
63
62
  functions,
64
63
  internalFunctionConfig,
65
- featureFlags: { edge_functions_path_urlpattern: true },
66
64
  });
67
65
  const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
68
66
  const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
@@ -85,7 +83,6 @@ test('Generates a manifest with excluded paths and patterns', () => {
85
83
  bundles: [],
86
84
  declarations,
87
85
  functions,
88
- featureFlags: { edge_functions_path_urlpattern: true },
89
86
  });
90
87
  const expectedRoutes = [
91
88
  { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'], path: '/f1/*' },
@@ -119,7 +116,6 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
119
116
  declarations,
120
117
  functions,
121
118
  userFunctionConfig,
122
- featureFlags: { edge_functions_path_urlpattern: true },
123
119
  });
124
120
  const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
125
121
  expect(manifest.routes).toEqual(expectedRoutes);
@@ -196,7 +192,6 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
196
192
  functions,
197
193
  userFunctionConfig,
198
194
  internalFunctionConfig,
199
- featureFlags: { edge_functions_path_urlpattern: true },
200
195
  });
201
196
  expect(manifest.routes).toEqual([
202
197
  {
@@ -235,7 +230,6 @@ test('URLPattern named groups are supported', () => {
235
230
  functions,
236
231
  userFunctionConfig,
237
232
  internalFunctionConfig,
238
- featureFlags: { edge_functions_path_urlpattern: true },
239
233
  });
240
234
  expect(manifest.routes).toEqual([
241
235
  {
@@ -259,7 +253,6 @@ test('Invalid Path patterns throw bundling errors', () => {
259
253
  functions,
260
254
  userFunctionConfig,
261
255
  internalFunctionConfig,
262
- featureFlags: { edge_functions_path_urlpattern: true },
263
256
  })).toThrowError(BundleError);
264
257
  });
265
258
  test('Includes failure modes in manifest', () => {
@@ -381,14 +374,12 @@ test('Generates a manifest with layers', () => {
381
374
  bundles: [],
382
375
  declarations,
383
376
  functions,
384
- featureFlags: { edge_functions_path_urlpattern: true },
385
377
  });
386
378
  const manifest2 = generateManifest({
387
379
  bundles: [],
388
380
  declarations,
389
381
  functions,
390
382
  layers,
391
- featureFlags: { edge_functions_path_urlpattern: true },
392
383
  });
393
384
  expect(manifest1.routes).toEqual(expectedRoutes);
394
385
  expect(manifest1.layers).toEqual([]);
@@ -50,6 +50,13 @@ declare const edgeManifestSchema: {
50
50
  path: {
51
51
  type: string;
52
52
  };
53
+ methods: {
54
+ type: string;
55
+ items: {
56
+ type: string;
57
+ enum: string[];
58
+ };
59
+ };
53
60
  };
54
61
  additionalProperties: boolean;
55
62
  };
@@ -85,6 +92,13 @@ declare const edgeManifestSchema: {
85
92
  path: {
86
93
  type: string;
87
94
  };
95
+ methods: {
96
+ type: string;
97
+ items: {
98
+ type: string;
99
+ enum: string[];
100
+ };
101
+ };
88
102
  };
89
103
  additionalProperties: boolean;
90
104
  };
@@ -29,6 +29,10 @@ const routesSchema = {
29
29
  excluded_patterns: excludedPatternsSchema,
30
30
  generator: { type: 'string' },
31
31
  path: { type: 'string' },
32
+ methods: {
33
+ type: 'array',
34
+ items: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] },
35
+ },
32
36
  },
33
37
  additionalProperties: false,
34
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "8.18.0",
3
+ "version": "8.19.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -82,7 +82,6 @@
82
82
  "execa": "^6.0.0",
83
83
  "find-up": "^6.3.0",
84
84
  "get-port": "^6.1.2",
85
- "glob-to-regexp": "^0.4.1",
86
85
  "is-path-inside": "^4.0.0",
87
86
  "jsonc-parser": "^3.2.0",
88
87
  "node-fetch": "^3.1.1",