@netlify/edge-bundler 8.13.0 → 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.
@@ -70,7 +70,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
70
70
  const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
71
71
  // Creating a final declarations array by combining the TOML file with the
72
72
  // deploy configuration API and the in-source configuration.
73
- const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations);
73
+ const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations, featureFlags);
74
74
  const internalFunctionConfig = createFunctionConfig({
75
75
  internalFunctionsWithConfig,
76
76
  declarations,
@@ -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[], userFunctionsConfig: Record<string, FunctionConfig>, internalFunctionsConfig: 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, userFunctionsConfig, internalFunctionsConfig, 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 = userFunctionsConfig[declaration.function] || internalFunctionsConfig[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,10 +53,12 @@ export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, interna
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 { ...internalFunctionsConfig, ...userFunctionsConfig }) {
34
- const { cache, path } = internalFunctionsConfig[name] || userFunctionsConfig[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];
@@ -1,22 +1,47 @@
1
1
  import { test, expect } from 'vitest';
2
2
  import { mergeDeclarations } from './declaration.js';
3
3
  const deployConfigDeclarations = [];
4
- test('Deploy config takes precedence over user config', () => {
4
+ test('Ensure the order of edge functions with FF', () => {
5
5
  const deployConfigDeclarations = [
6
- { function: 'framework-a', path: '/path1' },
7
- { function: 'framework-b', path: '/path2' },
6
+ { function: 'framework-manifest-a', path: '/path1' },
7
+ { function: 'framework-manifest-c', path: '/path3' },
8
+ { function: 'framework-manifest-b', path: '/path2' },
8
9
  ];
9
10
  const tomlConfig = [
10
- { function: 'user-a', path: '/path1' },
11
- { function: 'user-b', path: '/path2' },
11
+ { function: 'user-toml-a', path: '/path1' },
12
+ { function: 'user-toml-c', path: '/path3' },
13
+ { function: 'user-toml-b', path: '/path2' },
12
14
  ];
13
15
  const userFuncConfig = {
14
- 'user-c': { path: ['/path1', '/path2'] },
16
+ 'user-isc-c': { path: ['/path1', '/path2'] },
15
17
  };
16
18
  const internalFuncConfig = {
17
- 'framework-c': { path: ['/path1', '/path2'] },
19
+ 'framework-isc-c': { path: ['/path1', '/path2'] },
18
20
  };
19
- expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations)).toMatchSnapshot();
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();
20
45
  });
21
46
  test('In-source config takes precedence over netlify.toml config', () => {
22
47
  const tomlConfig = [
@@ -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
+ });
@@ -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.13.0",
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.7",
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.7"
69
+ "typescript": "^5.0.0",
70
+ "vitest": "^0.30.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": "^14.16.0 || >=16.0.0"