@netlify/edge-bundler 8.16.4 → 8.17.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/deno/config.ts CHANGED
@@ -1,6 +1,9 @@
1
- const [functionURL, collectorURL, rawExitCodes] = Deno.args
1
+ const [functionURL, collectorURL, bootstrapURL, rawExitCodes] = Deno.args
2
2
  const exitCodes = JSON.parse(rawExitCodes)
3
3
 
4
+ const { Netlify } = await import(bootstrapURL)
5
+ globalThis.Netlify = Netlify
6
+
4
7
  let func
5
8
 
6
9
  try {
@@ -14,8 +14,9 @@ interface BundleOptions {
14
14
  onBeforeDownload?: OnBeforeDownloadHook;
15
15
  systemLogger?: LogFunction;
16
16
  internalSrcFolder?: string;
17
+ bootstrapURL?: string;
17
18
  }
18
- declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, }?: BundleOptions) => Promise<{
19
+ declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL, }?: BundleOptions) => Promise<{
19
20
  functions: import("./edge_function.js").EdgeFunction[];
20
21
  manifest: import("./manifest.js").Manifest;
21
22
  }>;
@@ -14,7 +14,7 @@ import { ImportMap } from './import_map.js';
14
14
  import { getLogger } from './logger.js';
15
15
  import { writeManifest } from './manifest.js';
16
16
  import { ensureLatestTypes } from './types.js';
17
- const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, } = {}) => {
17
+ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts', } = {}) => {
18
18
  const logger = getLogger(systemLogger, debug);
19
19
  const featureFlags = getFlags(inputFeatureFlags);
20
20
  const options = {
@@ -63,8 +63,8 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
63
63
  await createFinalBundles([functionBundle], distDirectory, buildID);
64
64
  // Retrieving a configuration object for each function.
65
65
  // Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
66
- const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger)]);
67
- const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger)]);
66
+ const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
67
+ const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
68
68
  // Creating a hash of function names to configuration objects.
69
69
  const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
70
70
  const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
@@ -17,4 +17,10 @@ export interface FunctionConfig {
17
17
  name?: string;
18
18
  generator?: string;
19
19
  }
20
- export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;
20
+ export declare const getFunctionConfig: ({ func, importMap, deno, bootstrapURL, log, }: {
21
+ func: EdgeFunction;
22
+ importMap: ImportMap;
23
+ deno: DenoBridge;
24
+ bootstrapURL: string;
25
+ log: Logger;
26
+ }) => Promise<FunctionConfig>;
@@ -26,7 +26,7 @@ const getConfigExtractor = () => {
26
26
  const configExtractorPath = join(packagePath, 'deno', 'config.ts');
27
27
  return configExtractorPath;
28
28
  };
29
- export const getFunctionConfig = async (func, importMap, deno, log) => {
29
+ export const getFunctionConfig = async ({ func, importMap, deno, bootstrapURL, log, }) => {
30
30
  // The extractor is a Deno script that will import the function and run its
31
31
  // `config` export, if one exists.
32
32
  const extractorPath = getConfigExtractor();
@@ -50,6 +50,7 @@ export const getFunctionConfig = async (func, importMap, deno, log) => {
50
50
  extractorPath,
51
51
  pathToFileURL(func.path).href,
52
52
  pathToFileURL(collector.path).href,
53
+ bootstrapURL,
53
54
  JSON.stringify(ConfigExitCode),
54
55
  ], { rejectOnExitCode: false });
55
56
  if (exitCode !== ConfigExitCode.Success) {
@@ -9,6 +9,7 @@ import { DenoBridge } from './bridge.js';
9
9
  import { bundle } from './bundler.js';
10
10
  import { getFunctionConfig } from './config.js';
11
11
  import { ImportMap } from './import_map.js';
12
+ const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts';
12
13
  const importMapFile = {
13
14
  baseURL: new URL('file:///some/path/import-map.json'),
14
15
  imports: {
@@ -114,9 +115,15 @@ describe('`getFunctionConfig` extracts configuration properties from function fi
114
115
  const path = join(tmpDir, `${func.name}.js`);
115
116
  await fs.writeFile(path, func.source);
116
117
  const funcCall = () => getFunctionConfig({
117
- name: func.name,
118
- path,
119
- }, new ImportMap([importMapFile]), deno, logger);
118
+ func: {
119
+ name: func.name,
120
+ path,
121
+ },
122
+ importMap: new ImportMap([importMapFile]),
123
+ deno,
124
+ log: logger,
125
+ bootstrapURL,
126
+ });
120
127
  if (func.error) {
121
128
  await expect(funcCall()).rejects.toThrowError(func.error);
122
129
  }
@@ -149,6 +156,7 @@ test('Loads function paths from the in-source `config` function', async () => {
149
156
  const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
150
157
  basePath,
151
158
  configPath: join(internalDirectory, 'config.json'),
159
+ featureFlags: { edge_functions_path_urlpattern: true },
152
160
  });
153
161
  const generatedFiles = await fs.readdir(distPath);
154
162
  expect(result.functions.length).toBe(7);
@@ -165,7 +173,7 @@ test('Loads function paths from the in-source `config` function', async () => {
165
173
  expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$', excluded_patterns: [] });
166
174
  expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$', excluded_patterns: [] });
167
175
  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: [] });
176
+ expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5(?:/(.*))/?$', excluded_patterns: [] });
169
177
  expect(postCacheRoutes.length).toBe(1);
170
178
  expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$', excluded_patterns: [] });
171
179
  expect(Object.keys(functionConfig)).toHaveLength(1);
@@ -193,9 +201,15 @@ test('Passes validation if default export exists and is a function', async () =>
193
201
  const path = join(tmpDir, `${func.name}.ts`);
194
202
  await fs.writeFile(path, func.source);
195
203
  await expect(getFunctionConfig({
196
- name: func.name,
197
- path,
198
- }, new ImportMap([importMapFile]), deno, logger)).resolves.not.toThrow();
204
+ func: {
205
+ name: func.name,
206
+ path,
207
+ },
208
+ importMap: new ImportMap([importMapFile]),
209
+ deno,
210
+ log: logger,
211
+ bootstrapURL,
212
+ })).resolves.not.toThrow();
199
213
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
200
214
  });
201
215
  test('Fails validation if default export is not function', async () => {
@@ -217,9 +231,15 @@ test('Fails validation if default export is not function', async () => {
217
231
  const path = join(tmpDir, `${func.name}.ts`);
218
232
  await fs.writeFile(path, func.source);
219
233
  const config = getFunctionConfig({
220
- name: func.name,
221
- path,
222
- }, new ImportMap([importMapFile]), deno, logger);
234
+ func: {
235
+ name: func.name,
236
+ path,
237
+ },
238
+ importMap: new ImportMap([importMapFile]),
239
+ deno,
240
+ log: logger,
241
+ bootstrapURL,
242
+ });
223
243
  await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
224
244
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
225
245
  });
@@ -241,9 +261,15 @@ test('Fails validation if default export is not present', async () => {
241
261
  const path = join(tmpDir, `${func.name}.ts`);
242
262
  await fs.writeFile(path, func.source);
243
263
  const config = getFunctionConfig({
244
- name: func.name,
245
- path,
246
- }, new ImportMap([importMapFile]), deno, logger);
264
+ func: {
265
+ name: func.name,
266
+ path,
267
+ },
268
+ importMap: new ImportMap([importMapFile]),
269
+ deno,
270
+ log: logger,
271
+ bootstrapURL,
272
+ });
247
273
  await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
248
274
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
249
275
  });
@@ -66,7 +66,12 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
66
66
  // Validates and normalizes a pattern so that it's a valid regular expression
67
67
  // in Go, which is the engine used by our edge nodes.
68
68
  export const parsePattern = (pattern) => {
69
- const regexp = new RegExp(pattern);
69
+ let enclosedPattern = pattern;
70
+ if (!pattern.startsWith('^'))
71
+ enclosedPattern = `^${enclosedPattern}`;
72
+ if (!pattern.endsWith('$'))
73
+ enclosedPattern = `${enclosedPattern}$`;
74
+ const regexp = new RegExp(enclosedPattern);
70
75
  const newRegexp = regexpAST.transform(regexp, {
71
76
  Assertion(path) {
72
77
  // Lookaheads are not supported. If we find one, throw an error.
@@ -126,3 +126,9 @@ test('Escapes front slashes in a regex pattern', () => {
126
126
  const actual = parsePattern(regexPattern);
127
127
  expect(actual).toEqual(expected);
128
128
  });
129
+ test('Ensures pattern match on the whole path', () => {
130
+ const regexPattern = '/foo/.*/bar';
131
+ const expected = '^\\/foo\\/.*\\/bar$';
132
+ const actual = parsePattern(regexPattern);
133
+ expect(actual).toEqual(expected);
134
+ });
@@ -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);
@@ -43,7 +43,7 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
43
43
  });
44
44
  let functionsConfig = [];
45
45
  if (options.getFunctionsConfig) {
46
- functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger)));
46
+ functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ func, importMap, deno, bootstrapURL, log: logger })));
47
47
  }
48
48
  const success = await waitForServer(port, processRef.ps);
49
49
  return {
@@ -27,6 +27,10 @@ test('Starts a server and serves requests for edge functions', async () => {
27
27
  name: 'greet',
28
28
  path: join(paths.internal, 'greet.ts'),
29
29
  },
30
+ {
31
+ name: 'global_netlify',
32
+ path: join(paths.user, 'global_netlify.ts'),
33
+ },
30
34
  ];
31
35
  const options = {
32
36
  getFunctionsConfig: true,
@@ -35,7 +39,7 @@ test('Starts a server and serves requests for edge functions', async () => {
35
39
  very_secret_secret: 'i love netlify',
36
40
  }, options);
37
41
  expect(success).toBe(true);
38
- expect(functionsConfig).toEqual([{ path: '/my-function' }, {}]);
42
+ expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
39
43
  for (const key in functions) {
40
44
  const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(
41
45
  // @ts-expect-error TODO: Module graph is currently not typed
@@ -60,4 +64,15 @@ test('Starts a server and serves requests for edge functions', async () => {
60
64
  });
61
65
  expect(response2.status).toBe(200);
62
66
  expect(await response2.text()).toBe('HELLO!');
67
+ const response3 = await fetch(`http://0.0.0.0:${port}/global-netlify`, {
68
+ headers: {
69
+ 'x-nf-edge-functions': 'global_netlify',
70
+ 'x-ef-passthrough': 'passthrough',
71
+ 'X-NF-Request-ID': uuidv4(),
72
+ },
73
+ });
74
+ expect(await response3.json()).toEqual({
75
+ global: 'i love netlify',
76
+ local: 'i love netlify',
77
+ });
63
78
  });
@@ -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.1",
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
  }