@netlify/edge-bundler 8.16.4 → 8.17.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.
@@ -149,6 +149,7 @@ test('Loads function paths from the in-source `config` function', async () => {
149
149
  const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
150
150
  basePath,
151
151
  configPath: join(internalDirectory, 'config.json'),
152
+ featureFlags: { edge_functions_path_urlpattern: true },
152
153
  });
153
154
  const generatedFiles = await fs.readdir(distPath);
154
155
  expect(result.functions.length).toBe(7);
@@ -165,7 +166,7 @@ test('Loads function paths from the in-source `config` function', async () => {
165
166
  expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$', excluded_patterns: [] });
166
167
  expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$', excluded_patterns: [] });
167
168
  expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$', excluded_patterns: [] });
168
- expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5/.*/?$', excluded_patterns: [] });
169
+ expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5(?:/(.*))/?$', excluded_patterns: [] });
169
170
  expect(postCacheRoutes.length).toBe(1);
170
171
  expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$', excluded_patterns: [] });
171
172
  expect(Object.keys(functionConfig)).toHaveLength(1);
@@ -1,10 +1,12 @@
1
1
  declare const defaultFlags: {
2
2
  edge_functions_fail_unsupported_regex: boolean;
3
+ edge_functions_path_urlpattern: boolean;
3
4
  };
4
5
  type FeatureFlag = keyof typeof defaultFlags;
5
6
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
6
7
  declare const getFlags: (input?: Record<string, boolean>, flags?: {
7
8
  edge_functions_fail_unsupported_regex: boolean;
9
+ edge_functions_path_urlpattern: boolean;
8
10
  }) => FeatureFlags;
9
11
  export { defaultFlags, getFlags };
10
12
  export type { FeatureFlag, FeatureFlags };
@@ -1,5 +1,6 @@
1
1
  const defaultFlags = {
2
2
  edge_functions_fail_unsupported_regex: false,
3
+ edge_functions_path_urlpattern: false,
3
4
  };
4
5
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
5
6
  ...result,
@@ -4,6 +4,7 @@ import globToRegExp from 'glob-to-regexp';
4
4
  import { parsePattern } from './declaration.js';
5
5
  import { getPackageVersion } from './package_json.js';
6
6
  import { nonNullable } from './utils/non_nullable.js';
7
+ import { ExtendedURLPattern } from './utils/urlpattern.js';
7
8
  const removeEmptyConfigValues = (functionConfig) => Object.entries(functionConfig).reduce((acc, [key, value]) => {
8
9
  if (value && !(Array.isArray(value) && value.length === 0)) {
9
10
  return { ...acc, [key]: value };
@@ -25,10 +26,10 @@ const sanitizeEdgeFunctionConfig = (config) => {
25
26
  }
26
27
  return newConfig;
27
28
  };
28
- const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
29
+ const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath, featureFlags) => {
29
30
  if (excludedPath) {
30
31
  const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
31
- const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern);
32
+ const excludedPatterns = paths.map((path) => pathToRegularExpression(path, featureFlags)).map(serializePattern);
32
33
  manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
33
34
  }
34
35
  };
@@ -41,7 +42,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
41
42
  if (manifestFunctionConfig[name] === undefined) {
42
43
  continue;
43
44
  }
44
- addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
45
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath, featureFlags);
45
46
  manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError };
46
47
  }
47
48
  for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
@@ -49,7 +50,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
49
50
  if (manifestFunctionConfig[name] === undefined) {
50
51
  continue;
51
52
  }
52
- addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
53
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath, featureFlags);
53
54
  manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest };
54
55
  }
55
56
  declarations.forEach((declaration) => {
@@ -57,8 +58,8 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
57
58
  if (func === undefined) {
58
59
  return;
59
60
  }
60
- const pattern = getRegularExpression(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
61
- const excludedPattern = getExcludedRegularExpressions(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
61
+ const pattern = getRegularExpression(declaration, featureFlags);
62
+ const excludedPattern = getExcludedRegularExpressions(declaration, featureFlags);
62
63
  const route = {
63
64
  function: func.name,
64
65
  pattern: serializePattern(pattern),
@@ -86,7 +87,18 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
86
87
  };
87
88
  return manifest;
88
89
  };
89
- const pathToRegularExpression = (path) => {
90
+ const pathToRegularExpression = (path, featureFlags) => {
91
+ if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_path_urlpattern) {
92
+ const pattern = new ExtendedURLPattern({ pathname: path });
93
+ // Removing the `^` and `$` delimiters because we'll need to modify what's
94
+ // between them.
95
+ const source = pattern.regexp.pathname.source.slice(1, -1);
96
+ // Wrapping the expression source with `^` and `$`. Also, adding an optional
97
+ // trailing slash, so that a declaration of `path: "/foo"` matches requests
98
+ // for both `/foo` and `/foo/`.
99
+ const normalizedSource = `^${source}\\/?$`;
100
+ return normalizedSource;
101
+ }
90
102
  // We use the global flag so that `globToRegExp` will not wrap the expression
91
103
  // with `^` and `$`. We'll do that ourselves.
92
104
  const regularExpression = globToRegExp(path, { flags: 'g' });
@@ -96,23 +108,23 @@ const pathToRegularExpression = (path) => {
96
108
  const normalizedSource = `^${regularExpression.source}\\/?$`;
97
109
  return normalizedSource;
98
110
  };
99
- const getRegularExpression = (declaration, failUnsupportedRegex = false) => {
111
+ const getRegularExpression = (declaration, featureFlags) => {
100
112
  if ('pattern' in declaration) {
101
113
  try {
102
114
  return parsePattern(declaration.pattern);
103
115
  }
104
116
  catch (error) {
105
117
  // eslint-disable-next-line max-depth
106
- if (failUnsupportedRegex) {
118
+ if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex) {
107
119
  throw new Error(`Could not parse path declaration of function '${declaration.function}': ${error.message}`);
108
120
  }
109
121
  console.warn(`Function '${declaration.function}' uses an unsupported regular expression and will not be invoked: ${error.message}`);
110
122
  return declaration.pattern;
111
123
  }
112
124
  }
113
- return pathToRegularExpression(declaration.path);
125
+ return pathToRegularExpression(declaration.path, featureFlags);
114
126
  };
115
- const getExcludedRegularExpressions = (declaration, failUnsupportedRegex = false) => {
127
+ const getExcludedRegularExpressions = (declaration, featureFlags) => {
116
128
  if ('excludedPattern' in declaration && declaration.excludedPattern) {
117
129
  const excludedPatterns = Array.isArray(declaration.excludedPattern)
118
130
  ? declaration.excludedPattern
@@ -122,7 +134,7 @@ const getExcludedRegularExpressions = (declaration, failUnsupportedRegex = false
122
134
  return parsePattern(excludedPattern);
123
135
  }
124
136
  catch (error) {
125
- if (failUnsupportedRegex) {
137
+ if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex) {
126
138
  throw new Error(`Could not parse path declaration of function '${declaration.function}': ${error.message}`);
127
139
  }
128
140
  console.warn(`Function '${declaration.function}' uses an unsupported regular expression and will therefore not be invoked: ${error.message}`);
@@ -132,7 +144,7 @@ const getExcludedRegularExpressions = (declaration, failUnsupportedRegex = false
132
144
  }
133
145
  if ('path' in declaration && declaration.excludedPath) {
134
146
  const paths = Array.isArray(declaration.excludedPath) ? declaration.excludedPath : [declaration.excludedPath];
135
- return paths.map(pathToRegularExpression);
147
+ return paths.map((path) => pathToRegularExpression(path, featureFlags));
136
148
  }
137
149
  return [];
138
150
  };
@@ -34,8 +34,14 @@ test('Generates a manifest with display names', () => {
34
34
  name: 'Display Name',
35
35
  },
36
36
  };
37
- const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
38
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$', excluded_patterns: [] }];
37
+ const manifest = generateManifest({
38
+ bundles: [],
39
+ declarations,
40
+ functions,
41
+ internalFunctionConfig,
42
+ featureFlags: { edge_functions_path_urlpattern: true },
43
+ });
44
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
39
45
  expect(manifest.function_config).toEqual({
40
46
  'func-1': { name: 'Display Name' },
41
47
  });
@@ -50,8 +56,14 @@ test('Generates a manifest with a generator field', () => {
50
56
  generator: '@netlify/fake-plugin@1.0.0',
51
57
  },
52
58
  };
53
- const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
54
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$', excluded_patterns: [] }];
59
+ const manifest = generateManifest({
60
+ bundles: [],
61
+ declarations,
62
+ functions,
63
+ internalFunctionConfig,
64
+ featureFlags: { edge_functions_path_urlpattern: true },
65
+ });
66
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
55
67
  const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
56
68
  expect(manifest.routes).toEqual(expectedRoutes);
57
69
  expect(manifest.function_config).toEqual(expectedFunctionConfig);
@@ -65,14 +77,23 @@ test('Generates a manifest with excluded paths and patterns', () => {
65
77
  ];
66
78
  const declarations = [
67
79
  { function: 'func-1', path: '/f1/*', excludedPath: '/f1/exclude' },
68
- { function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
80
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excludedPattern: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
69
81
  { function: 'func-3', path: '/*', excludedPath: '/**/*.html' },
70
82
  ];
71
- const manifest = generateManifest({ bundles: [], declarations, functions });
83
+ const manifest = generateManifest({
84
+ bundles: [],
85
+ declarations,
86
+ functions,
87
+ featureFlags: { edge_functions_path_urlpattern: true },
88
+ });
72
89
  const expectedRoutes = [
73
- { function: 'func-1', pattern: '^/f1/.*/?$', excluded_patterns: ['^/f1/exclude/?$'] },
74
- { function: 'func-2', pattern: '^/f2/.*/?$', excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
75
- { function: 'func-3', pattern: '^/.*/?$', excluded_patterns: ['^/.*/.*\\.html/?$'] },
90
+ { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'] },
91
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
92
+ {
93
+ function: 'func-3',
94
+ pattern: '^(?:/(.*))/?$',
95
+ excluded_patterns: ['^(?:/((?:.*)(?:/(?:.*))*))?(?:/(.*))\\.html/?$'],
96
+ },
76
97
  ];
77
98
  expect(manifest.routes).toEqual(expectedRoutes);
78
99
  expect(manifest.function_config).toEqual({});
@@ -87,8 +108,14 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
87
108
  const userFunctionConfig = {
88
109
  'func-1': { excludedPath: '/f1/exclude' },
89
110
  };
90
- const manifest = generateManifest({ bundles: [], declarations, functions, userFunctionConfig });
91
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$', excluded_patterns: [] }];
111
+ const manifest = generateManifest({
112
+ bundles: [],
113
+ declarations,
114
+ functions,
115
+ userFunctionConfig,
116
+ featureFlags: { edge_functions_path_urlpattern: true },
117
+ });
118
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
92
119
  expect(manifest.routes).toEqual(expectedRoutes);
93
120
  expect(manifest.function_config).toEqual({
94
121
  'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
@@ -102,7 +129,7 @@ test('Filters out internal in-source configurations in user created functions',
102
129
  ];
103
130
  const declarations = [
104
131
  { function: 'func-1', path: '/f1/*' },
105
- { function: 'func-2', pattern: '^/f2/.*/?$' },
132
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$' },
106
133
  ];
107
134
  const userFunctionConfig = {
108
135
  'func-1': {
@@ -163,22 +190,23 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
163
190
  functions,
164
191
  userFunctionConfig,
165
192
  internalFunctionConfig,
193
+ featureFlags: { edge_functions_path_urlpattern: true },
166
194
  });
167
195
  expect(manifest.routes).toEqual([
168
196
  {
169
197
  function: 'customisation',
170
- pattern: '^/showcases/.*/?$',
198
+ pattern: '^/showcases(?:/(.*))/?$',
171
199
  excluded_patterns: [],
172
200
  },
173
201
  {
174
202
  function: 'customisation',
175
- pattern: '^/checkout/.*/?$',
176
- excluded_patterns: ['^/.*/terms-and-conditions/?$'],
203
+ pattern: '^/checkout(?:/(.*))/?$',
204
+ excluded_patterns: ['^(?:/(.*))/terms-and-conditions/?$'],
177
205
  },
178
206
  ]);
179
207
  expect(manifest.function_config).toEqual({
180
208
  customisation: {
181
- excluded_patterns: ['^/.*\\.css/?$', '^/.*\\.jpg/?$'],
209
+ excluded_patterns: ['^(?:/(.*))\\.css/?$', '^(?:/(.*))\\.jpg/?$'],
182
210
  },
183
211
  });
184
212
  const matcher = getRouteMatcher(manifest);
@@ -195,7 +223,7 @@ test('Includes failure modes in manifest', () => {
195
223
  ];
196
224
  const declarations = [
197
225
  { function: 'func-1', path: '/f1/*' },
198
- { function: 'func-2', pattern: '^/f2/.*/?$' },
226
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$' },
199
227
  ];
200
228
  const userFunctionConfig = {
201
229
  'func-1': {
@@ -292,8 +320,8 @@ test('Generates a manifest with layers', () => {
292
320
  { function: 'func-2', path: '/f2/*' },
293
321
  ];
294
322
  const expectedRoutes = [
295
- { function: 'func-1', pattern: '^/f1/.*/?$', excluded_patterns: [] },
296
- { function: 'func-2', pattern: '^/f2/.*/?$', excluded_patterns: [] },
323
+ { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] },
324
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [] },
297
325
  ];
298
326
  const layers = [
299
327
  {
@@ -301,8 +329,19 @@ test('Generates a manifest with layers', () => {
301
329
  flag: 'edge_functions_onion_layer',
302
330
  },
303
331
  ];
304
- const manifest1 = generateManifest({ bundles: [], declarations, functions });
305
- const manifest2 = generateManifest({ bundles: [], declarations, functions, layers });
332
+ const manifest1 = generateManifest({
333
+ bundles: [],
334
+ declarations,
335
+ functions,
336
+ featureFlags: { edge_functions_path_urlpattern: true },
337
+ });
338
+ const manifest2 = generateManifest({
339
+ bundles: [],
340
+ declarations,
341
+ functions,
342
+ layers,
343
+ featureFlags: { edge_functions_path_urlpattern: true },
344
+ });
306
345
  expect(manifest1.routes).toEqual(expectedRoutes);
307
346
  expect(manifest1.layers).toEqual([]);
308
347
  expect(manifest2.routes).toEqual(expectedRoutes);
@@ -0,0 +1,4 @@
1
+ import { URLPattern } from 'urlpattern-polyfill';
2
+ export declare class ExtendedURLPattern extends URLPattern {
3
+ regexp: Record<string, RegExp>;
4
+ }
@@ -0,0 +1,3 @@
1
+ import { URLPattern } from 'urlpattern-polyfill';
2
+ export class ExtendedURLPattern extends URLPattern {
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "8.16.4",
3
+ "version": "8.17.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -93,6 +93,7 @@
93
93
  "regexp-tree": "^0.1.24",
94
94
  "semver": "^7.3.8",
95
95
  "tmp-promise": "^3.0.3",
96
+ "urlpattern-polyfill": "8.0.2",
96
97
  "uuid": "^9.0.0"
97
98
  }
98
99
  }