@netlify/edge-bundler 8.12.3 → 8.13.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.
@@ -1,6 +1,5 @@
1
1
  import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
2
2
  import { Declaration } from './declaration.js';
3
- import { EdgeFunction } from './edge_function.js';
4
3
  import { FeatureFlags } from './feature_flags.js';
5
4
  import { LogFunction } from './logger.js';
6
5
  interface BundleOptions {
@@ -17,23 +16,8 @@ interface BundleOptions {
17
16
  internalSrcFolder?: string;
18
17
  }
19
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<{
20
- functions: EdgeFunction[];
19
+ functions: import("./edge_function.js").EdgeFunction[];
21
20
  manifest: import("./manifest.js").Manifest;
22
21
  }>;
23
- export declare const addGeneratorFieldIfMissing: (declaration: Declaration, functions: EdgeFunction[], internalFunctionsPath?: string) => {
24
- generator: string | undefined;
25
- cache?: string | undefined;
26
- function: string;
27
- name?: string | undefined;
28
- path: `/${string}`;
29
- excludedPath?: `/${string}` | undefined;
30
- } | {
31
- generator: string | undefined;
32
- cache?: string | undefined;
33
- function: string;
34
- name?: string | undefined;
35
- pattern: string;
36
- excludedPattern?: string | undefined;
37
- };
38
22
  export { bundle };
39
23
  export type { BundleOptions };
@@ -1,7 +1,6 @@
1
1
  import { promises as fs } from 'fs';
2
- import { join, resolve } from 'path';
2
+ import { join } from 'path';
3
3
  import commonPathPrefix from 'common-path-prefix';
4
- import isPathInside from 'is-path-inside';
5
4
  import { v4 as uuidv4 } from 'uuid';
6
5
  import { importMapSpecifier } from '../shared/consts.js';
7
6
  import { DenoBridge } from './bridge.js';
@@ -25,7 +24,6 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
25
24
  onAfterDownload,
26
25
  onBeforeDownload,
27
26
  };
28
- const internalFunctionsPath = internalSrcFolder && resolve(internalSrcFolder);
29
27
  if (cacheDirectory !== undefined) {
30
28
  options.denoDir = join(cacheDirectory, 'deno_dir');
31
29
  }
@@ -42,9 +40,12 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
42
40
  // Layers are marked as externals in the ESZIP, so that those specifiers are
43
41
  // not actually included in the bundle.
44
42
  const externals = deployConfig.layers.map((layer) => layer.name);
43
+ const userSourceDirectories = sourceDirectories.filter((dir) => dir !== internalSrcFolder);
45
44
  const importMap = new ImportMap();
46
45
  await importMap.addFiles([deployConfig === null || deployConfig === void 0 ? void 0 : deployConfig.importMap, ...importMapPaths], logger);
47
- const functions = await findFunctions(sourceDirectories);
46
+ const userFunctions = userSourceDirectories.length === 0 ? [] : await findFunctions(userSourceDirectories);
47
+ const internalFunctions = internalSrcFolder ? await findFunctions([internalSrcFolder]) : [];
48
+ const functions = [...internalFunctions, ...userFunctions];
48
49
  const functionBundle = await bundleESZIP({
49
50
  basePath,
50
51
  buildID,
@@ -61,24 +62,27 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
61
62
  // rename the bundles to their permanent names.
62
63
  await createFinalBundles([functionBundle], distDirectory, buildID);
63
64
  // Retrieving a configuration object for each function.
64
- const functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger, featureFlags)));
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, featureFlags)]);
67
+ const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger, featureFlags)]);
65
68
  // Creating a hash of function names to configuration objects.
66
- const functionsWithConfig = functions.reduce((acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }), {});
69
+ const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
70
+ const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
67
71
  // Creating a final declarations array by combining the TOML file with the
68
72
  // deploy configuration API and the in-source configuration.
69
- const declarationsFromConfig = mergeDeclarations(tomlDeclarations, functionsWithConfig, deployConfig.declarations);
70
- // If any declarations are autogenerated and are missing the generator field
71
- // add a default string.
72
- const declarations = internalFunctionsPath
73
- ? declarationsFromConfig.map((declaration) => addGeneratorFieldIfMissing(declaration, functions, internalFunctionsPath))
74
- : declarationsFromConfig;
73
+ const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations, featureFlags);
74
+ const internalFunctionConfig = createFunctionConfig({
75
+ internalFunctionsWithConfig,
76
+ declarations,
77
+ });
75
78
  const manifest = await writeManifest({
76
79
  bundles: [functionBundle],
77
80
  declarations,
78
81
  distDirectory,
79
82
  featureFlags,
80
83
  functions,
81
- functionConfig: functionsWithConfig,
84
+ userFunctionConfig: userFunctionsWithConfig,
85
+ internalFunctionConfig,
82
86
  importMap: importMapSpecifier,
83
87
  layers: deployConfig.layers,
84
88
  });
@@ -107,12 +111,26 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
107
111
  }
108
112
  return commonPathPrefix(sourceDirectories);
109
113
  };
110
- export const addGeneratorFieldIfMissing = (declaration, functions, internalFunctionsPath) => {
111
- var _a;
112
- const fullFuncPath = (_a = functions === null || functions === void 0 ? void 0 : functions.find((func) => func.name === declaration.function)) === null || _a === void 0 ? void 0 : _a.path;
113
- // If function path is in the internalFunctionsPath, we assume it is autogenerated.
114
- const isInternal = Boolean(internalFunctionsPath && fullFuncPath && isPathInside(fullFuncPath, internalFunctionsPath));
115
- const generatorFallback = isInternal ? 'internalFunc' : undefined;
116
- return { ...declaration, generator: declaration.generator || generatorFallback };
114
+ // We used to allow the `name` and `generator` fields to be defined at the
115
+ // declaration level. We want these properties to live at the function level
116
+ // in their config object, so we translate that for backwards-compatibility.
117
+ const mergeWithDeclarationConfig = ({ functionName, config, declarations }) => {
118
+ const declaration = declarations === null || declarations === void 0 ? void 0 : declarations.find((decl) => decl.function === functionName);
119
+ return {
120
+ ...config,
121
+ name: (declaration === null || declaration === void 0 ? void 0 : declaration.name) || config.name,
122
+ generator: (declaration === null || declaration === void 0 ? void 0 : declaration.generator) || config.generator,
123
+ };
117
124
  };
125
+ const addGeneratorFallback = (config) => ({
126
+ ...config,
127
+ generator: config.generator || 'internalFunc',
128
+ });
129
+ const createFunctionConfig = ({ internalFunctionsWithConfig, declarations }) => Object.entries(internalFunctionsWithConfig).reduce((acc, [functionName, config]) => {
130
+ const mergedConfigFields = mergeWithDeclarationConfig({ functionName, config, declarations });
131
+ return {
132
+ ...acc,
133
+ [functionName]: addGeneratorFallback(mergedConfigFields),
134
+ };
135
+ }, {});
118
136
  export { bundle };
@@ -264,7 +264,7 @@ test('Processes a function that imports a custom layer', async () => {
264
264
  expect(layers).toEqual([layer]);
265
265
  await cleanup();
266
266
  });
267
- test('Loads declarations and import maps from the deploy configuration', async () => {
267
+ test('Loads declarations and import maps from the deploy configuration and in-source config', async () => {
268
268
  const { basePath, cleanup, distPath } = await useFixture('with_deploy_config');
269
269
  const declarations = [
270
270
  {
@@ -283,16 +283,22 @@ test('Loads declarations and import maps from the deploy configuration', async (
283
283
  expect(generatedFiles.length).toBe(2);
284
284
  const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
285
285
  const manifest = JSON.parse(manifestFile);
286
- const { routes, bundles, function_config: functionConfig } = manifest;
286
+ const { bundles, function_config: functionConfig } = manifest;
287
287
  expect(bundles.length).toBe(1);
288
288
  expect(bundles[0].format).toBe('eszip2');
289
289
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
290
- expect(routes[0].generator).toBeUndefined();
291
- expect(routes[1].name).toBe('Function two');
292
- expect(routes[1].generator).toBe('@netlify/fake-plugin@1.0.0');
293
- expect(routes[2].generator).toBe('internalFunc');
294
290
  // respects excludedPath from deploy config
295
- expect(functionConfig.func2).toEqual({ excluded_patterns: ['^/func2/skip/?$'] });
291
+ expect(functionConfig.func2).toEqual({
292
+ excluded_patterns: ['^/func2/skip/?$'],
293
+ name: 'Function two',
294
+ generator: '@netlify/fake-plugin@1.0.0',
295
+ });
296
+ // respects in-source config
297
+ expect(functionConfig.func3).toEqual({
298
+ name: 'in-config-function',
299
+ on_error: 'bypass',
300
+ generator: 'internalFunc',
301
+ });
296
302
  await cleanup();
297
303
  });
298
304
  test("Ignores entries in `importMapPaths` that don't point to an existing import map file", async () => {
@@ -8,12 +8,14 @@ export declare const enum Cache {
8
8
  Manual = "manual"
9
9
  }
10
10
  export type Path = `/${string}`;
11
- export type OnError = 'fail' | 'bypass' | `/${string}`;
11
+ export type OnError = 'fail' | 'bypass' | Path;
12
12
  export declare const isValidOnError: (value: unknown) => value is OnError;
13
13
  export interface FunctionConfig {
14
14
  cache?: Cache;
15
15
  path?: Path | Path[];
16
16
  excludedPath?: Path | Path[];
17
17
  onError?: OnError;
18
+ name?: string;
19
+ generator?: string;
18
20
  }
19
21
  export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger, featureFlags: FeatureFlags) => Promise<FunctionConfig>;
@@ -117,6 +117,25 @@ const functions = [
117
117
  export const config = { path: "/home" }
118
118
  `,
119
119
  },
120
+ {
121
+ testName: 'config with path, generator, name and onError`',
122
+ expectedConfig: {
123
+ path: '/home',
124
+ generator: '@netlify/fake-plugin@1.0.0',
125
+ name: 'a displayName',
126
+ onError: 'bypass',
127
+ },
128
+ name: 'func6',
129
+ source: `
130
+ export default async () => new Response("Hello from function three")
131
+
132
+ export const config = { path: "/home",
133
+ generator: '@netlify/fake-plugin@1.0.0',
134
+ name: 'a displayName',
135
+ onError: 'bypass',
136
+ }
137
+ `,
138
+ },
120
139
  ];
121
140
  describe('`getFunctionConfig` extracts configuration properties from function file', () => {
122
141
  test.each(functions)('$testName', async (func) => {
@@ -1,4 +1,5 @@
1
1
  import { FunctionConfig, Path } from './config.js';
2
+ import { FeatureFlags } from './feature_flags.js';
2
3
  interface BaseDeclaration {
3
4
  cache?: string;
4
5
  function: string;
@@ -14,6 +15,6 @@ type DeclarationWithPattern = BaseDeclaration & {
14
15
  excludedPattern?: string;
15
16
  };
16
17
  export type Declaration = DeclarationWithPath | DeclarationWithPattern;
17
- export declare const mergeDeclarations: (tomlDeclarations: Declaration[], functionsConfig: Record<string, FunctionConfig>, deployConfigDeclarations: Declaration[]) => Declaration[];
18
+ export declare const mergeDeclarations: (tomlDeclarations: Declaration[], userFunctionsConfig: Record<string, FunctionConfig>, internalFunctionsConfig: Record<string, FunctionConfig>, deployConfigDeclarations: Declaration[], featureFlags?: FeatureFlags) => Declaration[];
18
19
  export declare const parsePattern: (pattern: string) => string;
19
20
  export {};
@@ -1,14 +1,39 @@
1
1
  import regexpAST from 'regexp-tree';
2
- export const mergeDeclarations = (tomlDeclarations, functionsConfig, deployConfigDeclarations) => {
2
+ export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, internalFunctionsConfig, deployConfigDeclarations, featureFlags = {}) => {
3
+ const functionsVisited = new Set();
4
+ let declarations = getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited);
5
+ // eslint-disable-next-line unicorn/prefer-ternary
6
+ if (featureFlags.edge_functions_correct_order) {
7
+ declarations = [
8
+ // INTEGRATIONS
9
+ // 1. Declarations from the integrations deploy config
10
+ ...getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited),
11
+ // 2. Declarations from the integrations ISC
12
+ ...createDeclarationsFromFunctionConfigs(internalFunctionsConfig, functionsVisited),
13
+ // USER
14
+ // 3. Declarations from the users toml config
15
+ ...getDeclarationsFromInput(tomlDeclarations, userFunctionsConfig, functionsVisited),
16
+ // 4. Declarations from the users ISC
17
+ ...createDeclarationsFromFunctionConfigs(userFunctionsConfig, functionsVisited),
18
+ ];
19
+ }
20
+ else {
21
+ declarations = [
22
+ ...getDeclarationsFromInput(tomlDeclarations, userFunctionsConfig, functionsVisited),
23
+ ...getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited),
24
+ ...createDeclarationsFromFunctionConfigs(internalFunctionsConfig, functionsVisited),
25
+ ...createDeclarationsFromFunctionConfigs(userFunctionsConfig, functionsVisited),
26
+ ];
27
+ }
28
+ return declarations;
29
+ };
30
+ const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsVisited) => {
3
31
  var _a;
4
32
  const declarations = [];
5
- const functionsVisited = new Set();
6
- // We start by iterating over all the declarations in the TOML file and in
7
- // the deploy configuration file. For any declaration for which we also have
8
- // a function configuration object, we replace the path because that object
9
- // takes precedence.
10
- for (const declaration of [...tomlDeclarations, ...deployConfigDeclarations]) {
11
- const config = functionsConfig[declaration.function];
33
+ // For any declaration for which we also have a function configuration object,
34
+ // we replace the path because that object takes precedence.
35
+ for (const declaration of inputDeclarations) {
36
+ const config = functionConfigs[declaration.function];
12
37
  if (!config) {
13
38
  // If no config is found, add the declaration as is.
14
39
  declarations.push(declaration);
@@ -28,15 +53,21 @@ export const mergeDeclarations = (tomlDeclarations, functionsConfig, deployConfi
28
53
  }
29
54
  functionsVisited.add(declaration.function);
30
55
  }
31
- // Finally, we must create declarations for functions that are not declared
32
- // in the TOML at all.
33
- for (const name in functionsConfig) {
34
- const { cache, path } = functionsConfig[name];
56
+ return declarations;
57
+ };
58
+ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited) => {
59
+ const declarations = [];
60
+ for (const name in functionConfigs) {
61
+ const { cache, path } = functionConfigs[name];
35
62
  // If we have a path specified, create a declaration for each path.
36
63
  if (!functionsVisited.has(name) && path) {
37
64
  const paths = Array.isArray(path) ? path : [path];
38
65
  paths.forEach((singlePath) => {
39
- declarations.push({ cache, function: name, path: singlePath });
66
+ const declaration = { function: name, path: singlePath };
67
+ if (cache) {
68
+ declaration.cache = cache;
69
+ }
70
+ declarations.push(declaration);
40
71
  });
41
72
  }
42
73
  }
@@ -1,13 +1,54 @@
1
1
  import { test, expect } from 'vitest';
2
2
  import { mergeDeclarations } from './declaration.js';
3
- // TODO: Add tests with the deploy config.
4
3
  const deployConfigDeclarations = [];
4
+ test('Ensure the order of edge functions with FF', () => {
5
+ const deployConfigDeclarations = [
6
+ { function: 'framework-manifest-a', path: '/path1' },
7
+ { function: 'framework-manifest-c', path: '/path3' },
8
+ { function: 'framework-manifest-b', path: '/path2' },
9
+ ];
10
+ const tomlConfig = [
11
+ { function: 'user-toml-a', path: '/path1' },
12
+ { function: 'user-toml-c', path: '/path3' },
13
+ { function: 'user-toml-b', path: '/path2' },
14
+ ];
15
+ const userFuncConfig = {
16
+ 'user-isc-c': { path: ['/path1', '/path2'] },
17
+ };
18
+ const internalFuncConfig = {
19
+ 'framework-isc-c': { path: ['/path1', '/path2'] },
20
+ };
21
+ expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations, {
22
+ edge_functions_correct_order: true,
23
+ })).toMatchSnapshot();
24
+ });
25
+ test('Ensure the order of edge functions without FF', () => {
26
+ const deployConfigDeclarations = [
27
+ { function: 'framework-manifest-a', path: '/path1' },
28
+ { function: 'framework-manifest-c', path: '/path3' },
29
+ { function: 'framework-manifest-b', path: '/path2' },
30
+ ];
31
+ const tomlConfig = [
32
+ { function: 'user-toml-a', path: '/path1' },
33
+ { function: 'user-toml-c', path: '/path3' },
34
+ { function: 'user-toml-b', path: '/path2' },
35
+ ];
36
+ const userFuncConfig = {
37
+ 'user-isc-c': { path: ['/path1', '/path2'] },
38
+ };
39
+ const internalFuncConfig = {
40
+ 'framework-isc-c': { path: ['/path1', '/path2'] },
41
+ };
42
+ expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations, {
43
+ edge_functions_correct_order: false,
44
+ })).toMatchSnapshot();
45
+ });
5
46
  test('In-source config takes precedence over netlify.toml config', () => {
6
47
  const tomlConfig = [
7
48
  { function: 'geolocation', path: '/geo', cache: 'off' },
8
49
  { function: 'json', path: '/json', cache: 'manual' },
9
50
  ];
10
- const funcConfig = {
51
+ const userFuncConfig = {
11
52
  geolocation: { path: ['/geo-isc', '/*'], cache: 'manual' },
12
53
  json: { path: '/json', cache: 'off' },
13
54
  };
@@ -16,7 +57,7 @@ test('In-source config takes precedence over netlify.toml config', () => {
16
57
  { function: 'geolocation', path: '/*', cache: 'manual' },
17
58
  { function: 'json', path: '/json', cache: 'off' },
18
59
  ];
19
- const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations);
60
+ const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
20
61
  expect(declarations).toEqual(expectedDeclarations);
21
62
  });
22
63
  test("Declarations don't break if no in-source config is provided", () => {
@@ -24,7 +65,7 @@ test("Declarations don't break if no in-source config is provided", () => {
24
65
  { function: 'geolocation', path: '/geo', cache: 'off' },
25
66
  { function: 'json', path: '/json', cache: 'manual' },
26
67
  ];
27
- const funcConfig = {
68
+ const userFuncConfig = {
28
69
  geolocation: { path: ['/geo-isc'], cache: 'manual' },
29
70
  json: {},
30
71
  };
@@ -32,7 +73,7 @@ test("Declarations don't break if no in-source config is provided", () => {
32
73
  { function: 'geolocation', path: '/geo-isc', cache: 'manual' },
33
74
  { function: 'json', path: '/json', cache: 'manual' },
34
75
  ];
35
- const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations);
76
+ const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
36
77
  expect(declarations).toEqual(expectedDeclarations);
37
78
  });
38
79
  test('In-source config works independent of the netlify.toml file if a path is defined and otherwise if no path is set', () => {
@@ -49,9 +90,9 @@ test('In-source config works independent of the netlify.toml file if a path is d
49
90
  { function: 'json', path: '/json-isc', cache: 'off' },
50
91
  ];
51
92
  const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
52
- const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, deployConfigDeclarations);
93
+ const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, {}, deployConfigDeclarations);
53
94
  expect(declarationsWithISCPath).toEqual(expectedDeclarationsWithISCPath);
54
- const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, deployConfigDeclarations);
95
+ const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, {}, deployConfigDeclarations);
55
96
  expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath);
56
97
  });
57
98
  test('In-source config works if only the cache config property is set', () => {
@@ -60,7 +101,7 @@ test('In-source config works if only the cache config property is set', () => {
60
101
  geolocation: { cache: 'manual' },
61
102
  };
62
103
  const expectedDeclarations = [{ function: 'geolocation', path: '/geo', cache: 'manual' }];
63
- expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
104
+ expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
64
105
  });
65
106
  test("In-source config path property works if it's not an array", () => {
66
107
  const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
@@ -68,7 +109,7 @@ test("In-source config path property works if it's not an array", () => {
68
109
  json: { path: '/json', cache: 'manual' },
69
110
  };
70
111
  const expectedDeclarations = [{ function: 'json', path: '/json', cache: 'manual' }];
71
- expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
112
+ expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
72
113
  });
73
114
  test("In-source config path property works if it's not an array and it's not present in toml or deploy config", () => {
74
115
  const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
@@ -79,7 +120,7 @@ test("In-source config path property works if it's not an array and it's not pre
79
120
  { function: 'geolocation', path: '/geo', cache: 'off' },
80
121
  { function: 'json', path: '/json-isc', cache: 'manual' },
81
122
  ];
82
- expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
123
+ expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
83
124
  });
84
125
  test('In-source config works if path property is an empty array with cache value specified', () => {
85
126
  const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
@@ -87,12 +128,12 @@ test('In-source config works if path property is an empty array with cache value
87
128
  json: { path: [], cache: 'manual' },
88
129
  };
89
130
  const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }];
90
- expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
131
+ expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
91
132
  });
92
133
  test('netlify.toml-defined excludedPath are respected', () => {
93
134
  const tomlConfig = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
94
135
  const funcConfig = {};
95
136
  const expectedDeclarations = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
96
- const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations);
137
+ const declarations = mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations);
97
138
  expect(declarations).toEqual(expectedDeclarations);
98
139
  });
@@ -4,10 +4,20 @@ import { PassThrough } from 'stream';
4
4
  import { execa } from 'execa';
5
5
  import nock from 'nock';
6
6
  import tmp from 'tmp-promise';
7
- import { beforeEach, afterEach, test, expect } from 'vitest';
7
+ import { beforeEach, afterEach, test, expect, vi } from 'vitest';
8
8
  import { fixturesDir, testLogger } from '../test/util.js';
9
9
  import { download } from './downloader.js';
10
10
  import { getPlatformTarget } from './platform.js';
11
+ // This changes the defaults for p-retry
12
+ // minTimeout 1000 -> 10
13
+ // factor 2 -> 1
14
+ // This reduces the wait time in the tests from `2s, 4s, 8s` to `10ms, 10ms, 10ms` for 3 retries
15
+ vi.mock('p-retry', async (importOriginal) => {
16
+ const pRetry = (await importOriginal());
17
+ return {
18
+ default: (func, options) => pRetry.default(func, { minTimeout: 10, factor: 1, ...options }),
19
+ };
20
+ });
11
21
  const streamError = () => {
12
22
  const stream = new PassThrough();
13
23
  setTimeout(() => stream.emit('data', 'zipcontent'), 100);
@@ -1,14 +1,14 @@
1
1
  declare const defaultFlags: {
2
+ edge_functions_correct_order: boolean;
2
3
  edge_functions_fail_unsupported_regex: boolean;
3
4
  edge_functions_invalid_config_throw: boolean;
4
- edge_functions_manifest_validate_slash: boolean;
5
5
  };
6
6
  type FeatureFlag = keyof typeof defaultFlags;
7
7
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
8
8
  declare const getFlags: (input?: Record<string, boolean>, flags?: {
9
+ edge_functions_correct_order: boolean;
9
10
  edge_functions_fail_unsupported_regex: boolean;
10
11
  edge_functions_invalid_config_throw: boolean;
11
- edge_functions_manifest_validate_slash: boolean;
12
12
  }) => FeatureFlags;
13
13
  export { defaultFlags, getFlags };
14
14
  export type { FeatureFlag, FeatureFlags };
@@ -1,7 +1,7 @@
1
1
  const defaultFlags = {
2
+ edge_functions_correct_order: false,
2
3
  edge_functions_fail_unsupported_regex: false,
3
4
  edge_functions_invalid_config_throw: false,
4
- edge_functions_manifest_validate_slash: false,
5
5
  };
6
6
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
7
7
  ...result,
@@ -1,3 +1,4 @@
1
1
  import { EdgeFunction } from './edge_function.js';
2
+ export declare const removeDuplicatesByExtension: (functions: string[]) => string[];
2
3
  declare const findFunctions: (directories: string[]) => Promise<EdgeFunction[]>;
3
4
  export { findFunctions };
@@ -1,12 +1,24 @@
1
1
  import { promises as fs } from 'fs';
2
- import { basename, extname, join } from 'path';
2
+ import { basename, extname, join, parse } from 'path';
3
3
  import { nonNullable } from './utils/non_nullable.js';
4
- const ALLOWED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
4
+ // the order of the allowed extensions is also the order we remove duplicates
5
+ // with a lower index meaning a higher precedence over the others
6
+ const ALLOWED_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
7
+ export const removeDuplicatesByExtension = (functions) => {
8
+ const seen = new Map();
9
+ return Object.values(functions.reduce((acc, path) => {
10
+ const { ext, name } = parse(path);
11
+ const extIndex = ALLOWED_EXTENSIONS.indexOf(ext);
12
+ if (!seen.has(name) || seen.get(name) > extIndex) {
13
+ seen.set(name, extIndex);
14
+ return { ...acc, [name]: path };
15
+ }
16
+ return acc;
17
+ }, {}));
18
+ };
5
19
  const findFunctionInDirectory = async (directory) => {
6
20
  const name = basename(directory);
7
- const candidatePaths = [...ALLOWED_EXTENSIONS]
8
- .flatMap((extension) => [`${name}${extension}`, `index${extension}`])
9
- .map((filename) => join(directory, filename));
21
+ const candidatePaths = ALLOWED_EXTENSIONS.flatMap((extension) => [`${name}${extension}`, `index${extension}`]).map((filename) => join(directory, filename));
10
22
  let functionPath;
11
23
  for (const candidatePath of candidatePaths) {
12
24
  try {
@@ -35,14 +47,14 @@ const findFunctionInPath = async (path) => {
35
47
  return findFunctionInDirectory(path);
36
48
  }
37
49
  const extension = extname(path);
38
- if (ALLOWED_EXTENSIONS.has(extension)) {
50
+ if (ALLOWED_EXTENSIONS.includes(extension)) {
39
51
  return { name: basename(path, extension), path };
40
52
  }
41
53
  };
42
54
  const findFunctionsInDirectory = async (baseDirectory) => {
43
55
  let items = [];
44
56
  try {
45
- items = await fs.readdir(baseDirectory);
57
+ items = await fs.readdir(baseDirectory).then(removeDuplicatesByExtension);
46
58
  }
47
59
  catch {
48
60
  // no-op
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { test, expect } from 'vitest';
2
+ import { removeDuplicatesByExtension } from './finder.js';
3
+ test('filters out any duplicate files based on the extension', () => {
4
+ const functions = [
5
+ 'file1.js',
6
+ 'file1.ts',
7
+ 'file2.tsx',
8
+ 'file2.jsx',
9
+ 'file3.tsx',
10
+ 'file3.js',
11
+ 'file4.ts',
12
+ 'file5.ts',
13
+ 'file5.tsx',
14
+ ];
15
+ const expected = ['file1.js', 'file2.jsx', 'file3.js', 'file4.ts', 'file5.ts'];
16
+ expect(removeDuplicatesByExtension(functions)).toStrictEqual(expected);
17
+ });
@@ -6,13 +6,13 @@ import { FeatureFlags } from './feature_flags.js';
6
6
  import { Layer } from './layer.js';
7
7
  interface Route {
8
8
  function: string;
9
- name?: string;
10
9
  pattern: string;
11
- generator?: string;
12
10
  }
13
11
  interface EdgeFunctionConfig {
14
12
  excluded_patterns: string[];
15
13
  on_error?: string;
14
+ generator?: string;
15
+ name?: string;
16
16
  }
17
17
  interface Manifest {
18
18
  bundler_version: string;
@@ -34,11 +34,12 @@ interface GenerateManifestOptions {
34
34
  declarations?: Declaration[];
35
35
  featureFlags?: FeatureFlags;
36
36
  functions: EdgeFunction[];
37
- functionConfig?: Record<string, FunctionConfig>;
38
37
  importMap?: string;
38
+ internalFunctionConfig?: Record<string, FunctionConfig>;
39
39
  layers?: Layer[];
40
+ userFunctionConfig?: Record<string, FunctionConfig>;
40
41
  }
41
- declare const generateManifest: ({ bundles, declarations, featureFlags, functions, functionConfig, importMap, layers, }: GenerateManifestOptions) => Manifest;
42
+ declare const generateManifest: ({ bundles, declarations, featureFlags, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, }: GenerateManifestOptions) => Manifest;
42
43
  interface WriteManifestOptions extends GenerateManifestOptions {
43
44
  distDirectory: string;
44
45
  }
@@ -4,6 +4,12 @@ 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
+ const removeEmptyConfigValues = (functionConfig) => Object.entries(functionConfig).reduce((acc, [key, value]) => {
8
+ if (value && !(Array.isArray(value) && value.length === 0)) {
9
+ return { ...acc, [key]: value };
10
+ }
11
+ return acc;
12
+ }, {});
7
13
  // JavaScript regular expressions are converted to strings with leading and
8
14
  // trailing slashes, so any slashes inside the expression itself are escaped
9
15
  // as `//`. This function deserializes that back into a single slash, which
@@ -12,29 +18,39 @@ const serializePattern = (pattern) => pattern.replace(/\\\//g, '/');
12
18
  const sanitizeEdgeFunctionConfig = (config) => {
13
19
  const newConfig = {};
14
20
  for (const [name, functionConfig] of Object.entries(config)) {
15
- if (functionConfig.excluded_patterns.length !== 0 || functionConfig.on_error) {
16
- newConfig[name] = functionConfig;
21
+ const newFunctionConfig = removeEmptyConfigValues(functionConfig);
22
+ if (Object.keys(newFunctionConfig).length !== 0) {
23
+ newConfig[name] = newFunctionConfig;
17
24
  }
18
25
  }
19
26
  return newConfig;
20
27
  };
21
- const generateManifest = ({ bundles = [], declarations = [], featureFlags, functions, functionConfig = {}, importMap, layers = [], }) => {
28
+ const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
29
+ if (excludedPath) {
30
+ const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
31
+ const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern);
32
+ manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
33
+ }
34
+ };
35
+ const generateManifest = ({ bundles = [], declarations = [], featureFlags, functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], }) => {
22
36
  const preCacheRoutes = [];
23
37
  const postCacheRoutes = [];
24
38
  const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
25
- for (const [name, { excludedPath, onError }] of Object.entries(functionConfig)) {
39
+ for (const [name, { excludedPath, onError }] of Object.entries(userFunctionConfig)) {
26
40
  // If the config block is for a function that is not defined, discard it.
27
41
  if (manifestFunctionConfig[name] === undefined) {
28
42
  continue;
29
43
  }
30
- if (excludedPath) {
31
- const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
32
- const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern);
33
- manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
34
- }
35
- if (onError) {
36
- manifestFunctionConfig[name].on_error = onError;
44
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
45
+ manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError };
46
+ }
47
+ for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
48
+ // If the config block is for a function that is not defined, discard it.
49
+ if (manifestFunctionConfig[name] === undefined) {
50
+ continue;
37
51
  }
52
+ addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
53
+ manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest };
38
54
  }
39
55
  declarations.forEach((declaration) => {
40
56
  const func = functions.find(({ name }) => declaration.function === name);
@@ -44,9 +60,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
44
60
  const pattern = getRegularExpression(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
45
61
  const route = {
46
62
  function: func.name,
47
- name: declaration.name,
48
63
  pattern: serializePattern(pattern),
49
- generator: declaration.generator,
50
64
  };
51
65
  const excludedPattern = getExcludedRegularExpression(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
52
66
  if (excludedPattern) {
@@ -26,63 +26,105 @@ test('Generates a manifest with different bundles', () => {
26
26
  expect(manifest.bundler_version).toBe(env.npm_package_version);
27
27
  });
28
28
  test('Generates a manifest with display names', () => {
29
- const functions = [
30
- { name: 'func-1', path: '/path/to/func-1.ts' },
31
- { name: 'func-2', path: '/path/to/func-2.ts' },
32
- ];
33
- const declarations = [
34
- { function: 'func-1', name: 'Display Name', path: '/f1/*' },
35
- { function: 'func-2', path: '/f2/*' },
36
- ];
37
- const manifest = generateManifest({ bundles: [], declarations, functions });
38
- const expectedRoutes = [
39
- { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$' },
40
- { function: 'func-2', pattern: '^/f2/.*/?$' },
41
- ];
29
+ const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
30
+ const declarations = [{ function: 'func-1', path: '/f1/*' }];
31
+ const internalFunctionConfig = {
32
+ 'func-1': {
33
+ name: 'Display Name',
34
+ },
35
+ };
36
+ const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
37
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$' }];
38
+ expect(manifest.function_config).toEqual({
39
+ 'func-1': { name: 'Display Name' },
40
+ });
42
41
  expect(manifest.routes).toEqual(expectedRoutes);
43
42
  expect(manifest.bundler_version).toBe(env.npm_package_version);
44
43
  });
45
44
  test('Generates a manifest with a generator field', () => {
45
+ const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
46
+ const declarations = [{ function: 'func-1', path: '/f1/*' }];
47
+ const internalFunctionConfig = {
48
+ 'func-1': {
49
+ generator: '@netlify/fake-plugin@1.0.0',
50
+ },
51
+ };
52
+ const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
53
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$' }];
54
+ const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
55
+ expect(manifest.routes).toEqual(expectedRoutes);
56
+ expect(manifest.function_config).toEqual(expectedFunctionConfig);
57
+ });
58
+ test('Generates a manifest with excluded paths and patterns', () => {
46
59
  const functions = [
47
60
  { name: 'func-1', path: '/path/to/func-1.ts' },
48
61
  { name: 'func-2', path: '/path/to/func-2.ts' },
49
- { name: 'func-3', path: '/path/to/func-3.ts' },
50
62
  ];
51
63
  const declarations = [
52
- { function: 'func-1', generator: '@netlify/fake-plugin@1.0.0', path: '/f1/*' },
53
- { function: 'func-2', path: '/f2/*' },
54
- { function: 'func-3', generator: '@netlify/fake-plugin@1.0.0', cache: 'manual', path: '/f3' },
64
+ { function: 'func-1', path: '/f1/*', excludedPath: '/f1/exclude' },
65
+ { function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: '^/f2/exclude$' },
55
66
  ];
56
67
  const manifest = generateManifest({ bundles: [], declarations, functions });
57
68
  const expectedRoutes = [
58
- { function: 'func-1', generator: '@netlify/fake-plugin@1.0.0', pattern: '^/f1/.*/?$' },
69
+ { function: 'func-1', pattern: '^/f1/.*/?$' },
59
70
  { function: 'func-2', pattern: '^/f2/.*/?$' },
60
71
  ];
61
- const expectedPostCacheRoutes = [{ function: 'func-3', generator: '@netlify/fake-plugin@1.0.0', pattern: '^/f3/?$' }];
62
72
  expect(manifest.routes).toEqual(expectedRoutes);
63
- expect(manifest.post_cache_routes).toEqual(expectedPostCacheRoutes);
73
+ expect(manifest.function_config).toEqual({
74
+ 'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
75
+ 'func-2': { excluded_patterns: ['^/f2/exclude$'] },
76
+ });
64
77
  expect(manifest.bundler_version).toBe(env.npm_package_version);
65
78
  });
66
- test('Generates a manifest with excluded paths and patterns', () => {
79
+ test('Filters out internal in-source configurations in user created functions', () => {
67
80
  const functions = [
68
81
  { name: 'func-1', path: '/path/to/func-1.ts' },
69
82
  { name: 'func-2', path: '/path/to/func-2.ts' },
70
83
  ];
71
84
  const declarations = [
72
- { function: 'func-1', name: 'Display Name', path: '/f1/*', excludedPath: '/f1/exclude' },
73
- { function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: '^/f2/exclude$' },
74
- ];
75
- const manifest = generateManifest({ bundles: [], declarations, functions });
76
- const expectedRoutes = [
77
- { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$' },
85
+ { function: 'func-1', path: '/f1/*' },
78
86
  { function: 'func-2', pattern: '^/f2/.*/?$' },
79
87
  ];
80
- expect(manifest.routes).toEqual(expectedRoutes);
88
+ const userFunctionConfig = {
89
+ 'func-1': {
90
+ onError: '/custom-error',
91
+ cache: "manual" /* Cache.Manual */,
92
+ excludedPath: '/f1/exclude',
93
+ path: '/path/to/func-1.ts',
94
+ name: 'User function',
95
+ generator: 'fake-generator',
96
+ },
97
+ };
98
+ const internalFunctionConfig = {
99
+ 'func-2': {
100
+ onError: 'bypass',
101
+ cache: "off" /* Cache.Off */,
102
+ excludedPath: '/f2/exclude',
103
+ path: '/path/to/func-2.ts',
104
+ name: 'Internal function',
105
+ generator: 'internal-generator',
106
+ },
107
+ };
108
+ const manifest = generateManifest({
109
+ bundles: [],
110
+ declarations,
111
+ functions,
112
+ userFunctionConfig,
113
+ internalFunctionConfig,
114
+ });
81
115
  expect(manifest.function_config).toEqual({
82
- 'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
83
- 'func-2': { excluded_patterns: ['^/f2/exclude$'] },
116
+ 'func-1': {
117
+ on_error: '/custom-error',
118
+ excluded_patterns: ['^/f1/exclude/?$'],
119
+ },
120
+ 'func-2': {
121
+ on_error: 'bypass',
122
+ cache: "off" /* Cache.Off */,
123
+ name: 'Internal function',
124
+ generator: 'internal-generator',
125
+ excluded_patterns: ['^/f2/exclude/?$'],
126
+ },
84
127
  });
85
- expect(manifest.bundler_version).toBe(env.npm_package_version);
86
128
  });
87
129
  test('Includes failure modes in manifest', () => {
88
130
  const functions = [
@@ -90,17 +132,17 @@ test('Includes failure modes in manifest', () => {
90
132
  { name: 'func-2', path: '/path/to/func-2.ts' },
91
133
  ];
92
134
  const declarations = [
93
- { function: 'func-1', name: 'Display Name', path: '/f1/*' },
135
+ { function: 'func-1', path: '/f1/*' },
94
136
  { function: 'func-2', pattern: '^/f2/.*/?$' },
95
137
  ];
96
- const functionConfig = {
138
+ const userFunctionConfig = {
97
139
  'func-1': {
98
140
  onError: '/custom-error',
99
141
  },
100
142
  };
101
- const manifest = generateManifest({ bundles: [], declarations, functions, functionConfig });
143
+ const manifest = generateManifest({ bundles: [], declarations, functions, userFunctionConfig });
102
144
  expect(manifest.function_config).toEqual({
103
- 'func-1': { excluded_patterns: [], on_error: '/custom-error' },
145
+ 'func-1': { on_error: '/custom-error' },
104
146
  });
105
147
  });
106
148
  test('Excludes functions for which there are function files but no matching config declarations', () => {
@@ -1,4 +1,4 @@
1
1
  import { FeatureFlags } from '../../feature_flags.js';
2
2
  import ManifestValidationError from './error.js';
3
- export declare const validateManifest: (manifestData: unknown, featureFlags?: FeatureFlags) => void;
3
+ export declare const validateManifest: (manifestData: unknown, _featureFlags?: FeatureFlags) => void;
4
4
  export { ManifestValidationError };
@@ -4,13 +4,13 @@ import betterAjvErrors from 'better-ajv-errors';
4
4
  import ManifestValidationError from './error.js';
5
5
  import edgeManifestSchema from './schema.js';
6
6
  let manifestValidator;
7
- const initializeValidator = (featureFlags) => {
7
+ const initializeValidator = () => {
8
8
  if (manifestValidator === undefined) {
9
9
  const ajv = new Ajv({ allErrors: true });
10
10
  ajvErrors(ajv);
11
11
  // regex pattern for manifest route pattern
12
12
  // checks if the pattern string starts with ^ and ends with $
13
- const normalizedPatternRegex = featureFlags.edge_functions_manifest_validate_slash ? /^\^\/.*\$$/ : /^\^.*\$$/;
13
+ const normalizedPatternRegex = /^\^.*\$$/;
14
14
  ajv.addFormat('regexPattern', {
15
15
  validate: (data) => normalizedPatternRegex.test(data),
16
16
  });
@@ -19,8 +19,9 @@ const initializeValidator = (featureFlags) => {
19
19
  return manifestValidator;
20
20
  };
21
21
  // throws on validation error
22
- export const validateManifest = (manifestData, featureFlags = {}) => {
23
- const validate = initializeValidator(featureFlags);
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
+ export const validateManifest = (manifestData, _featureFlags = {}) => {
24
+ const validate = initializeValidator();
24
25
  const valid = validate(manifestData);
25
26
  if (!valid) {
26
27
  let errorOutput;
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { test, expect, describe, beforeEach, vi } from 'vitest';
2
+ import { test, expect, describe } from 'vitest';
3
3
  import { validateManifest, ManifestValidationError } from './index.js';
4
4
  // We need to disable all color outputs for the tests as they are different on different platforms, CI, etc.
5
5
  // This only works if this is the same instance of chalk that better-ajv-errors uses
@@ -92,42 +92,25 @@ describe('bundle', () => {
92
92
  });
93
93
  });
94
94
  describe('route', () => {
95
- let freshValidateManifest;
96
- beforeEach(async () => {
97
- // reset all modules, to get a fresh AJV validator for FF changes
98
- vi.resetModules();
99
- const indexImport = await import('./index.js');
100
- freshValidateManifest = indexImport.validateManifest;
101
- });
102
95
  test('should throw on additional property', () => {
103
96
  const manifest = getBaseManifest();
104
97
  manifest.routes[0].foo = 'bar';
105
- expect(() => freshValidateManifest(manifest)).toThrowErrorMatchingSnapshot();
98
+ expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
106
99
  });
107
100
  test('should throw on invalid pattern', () => {
108
101
  const manifest = getBaseManifest();
109
102
  manifest.routes[0].pattern = '/^/hello/?$/';
110
- expect(() => freshValidateManifest(manifest)).toThrowErrorMatchingSnapshot();
111
- });
112
- test('should not throw on missing beginning slash without FF', () => {
113
- const manifest = getBaseManifest();
114
- manifest.routes[0].pattern = '^hello/?$';
115
- expect(() => freshValidateManifest(manifest, { edge_functions_manifest_validate_slash: false })).not.toThrowError();
116
- });
117
- test('should throw on missing beginning slash with FF', () => {
118
- const manifest = getBaseManifest();
119
- manifest.routes[0].pattern = '^hello/?$';
120
- expect(() => freshValidateManifest(manifest, { edge_functions_manifest_validate_slash: true })).toThrowErrorMatchingSnapshot();
103
+ expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
121
104
  });
122
105
  test('should throw on missing function', () => {
123
106
  const manifest = getBaseManifest();
124
107
  delete manifest.routes[0].function;
125
- expect(() => freshValidateManifest(manifest)).toThrowErrorMatchingSnapshot();
108
+ expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
126
109
  });
127
110
  test('should throw on missing pattern', () => {
128
111
  const manifest = getBaseManifest();
129
112
  delete manifest.routes[0].pattern;
130
- expect(() => freshValidateManifest(manifest)).toThrowErrorMatchingSnapshot();
113
+ expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
131
114
  });
132
115
  });
133
116
  // No tests for post_cache_routes as schema shared with routes
@@ -16,7 +16,7 @@ const routesSchema = {
16
16
  pattern: {
17
17
  type: 'string',
18
18
  format: 'regexPattern',
19
- errorMessage: 'pattern needs to be a regex that starts with ^ followed by / and ends with $ without any additional slashes before and afterwards',
19
+ errorMessage: 'pattern must be a regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)',
20
20
  },
21
21
  generator: { type: 'string' },
22
22
  },
@@ -31,7 +31,7 @@ const functionConfigSchema = {
31
31
  items: {
32
32
  type: 'string',
33
33
  format: 'regexPattern',
34
- errorMessage: 'excluded_patterns needs to be an array of regex that starts with ^ followed by / and ends with $ without any additional slashes before and afterwards',
34
+ errorMessage: 'excluded_patterns must be an array of regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)',
35
35
  },
36
36
  },
37
37
  on_error: { type: 'string' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "8.12.3",
3
+ "version": "8.13.1",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -58,7 +58,7 @@
58
58
  "@types/node": "^14.18.32",
59
59
  "@types/semver": "^7.3.9",
60
60
  "@types/uuid": "^9.0.0",
61
- "@vitest/coverage-c8": "^0.29.2",
61
+ "@vitest/coverage-c8": "^0.30.0",
62
62
  "archiver": "^5.3.1",
63
63
  "chalk": "^4.1.2",
64
64
  "cpy": "^9.0.1",
@@ -66,8 +66,8 @@
66
66
  "husky": "^8.0.0",
67
67
  "nock": "^13.2.4",
68
68
  "tar": "^6.1.11",
69
- "typescript": "^4.5.4",
70
- "vitest": "^0.29.2"
69
+ "typescript": "^5.0.0",
70
+ "vitest": "^0.30.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": "^14.16.0 || >=16.0.0"