@netlify/edge-bundler 7.1.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/deno/config.ts CHANGED
@@ -19,22 +19,12 @@ if (func.config === undefined) {
19
19
  Deno.exit(exitCodes.NoConfig)
20
20
  }
21
21
 
22
- if (typeof func.config !== 'function') {
22
+ if (typeof func.config !== 'object') {
23
23
  Deno.exit(exitCodes.InvalidExport)
24
24
  }
25
25
 
26
- let config
27
-
28
- try {
29
- config = await func.config()
30
- } catch (error) {
31
- console.error(error)
32
-
33
- Deno.exit(exitCodes.RuntimeError)
34
- }
35
-
36
26
  try {
37
- const result = JSON.stringify(config)
27
+ const result = JSON.stringify(func.config)
38
28
 
39
29
  await Deno.writeTextFile(new URL(collectorURL), result)
40
30
  } catch (error) {
@@ -75,6 +75,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
75
75
  declarations,
76
76
  distDirectory,
77
77
  functions,
78
+ functionConfig: functionsWithConfig,
78
79
  importMap: importMapSpecifier,
79
80
  layers: deployConfig.layers,
80
81
  });
@@ -110,7 +110,28 @@ test('Prints a nice error message when user tries importing NPM module', async (
110
110
  }
111
111
  catch (error) {
112
112
  expect(error).toBeInstanceOf(BundleError);
113
- expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported in Deno via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/p-retry"'?`);
113
+ expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/p-retry"'?`);
114
+ }
115
+ finally {
116
+ await cleanup();
117
+ }
118
+ });
119
+ test('Prints a nice error message when user tries importing NPM module with npm: scheme', async () => {
120
+ expect.assertions(2);
121
+ const { basePath, cleanup, distPath } = await useFixture('imports_npm_module_scheme');
122
+ const sourceDirectory = join(basePath, 'functions');
123
+ const declarations = [
124
+ {
125
+ function: 'func1',
126
+ path: '/func1',
127
+ },
128
+ ];
129
+ try {
130
+ await bundle([sourceDirectory], distPath, declarations, { basePath });
131
+ }
132
+ catch (error) {
133
+ expect(error).toBeInstanceOf(BundleError);
134
+ expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/p-retry"'?`);
114
135
  }
115
136
  finally {
116
137
  await cleanup();
@@ -278,12 +299,12 @@ test('Loads declarations and import maps from the deploy configuration', async (
278
299
  expect(generatedFiles.length).toBe(2);
279
300
  const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
280
301
  const manifest = JSON.parse(manifestFile);
281
- const { bundles, routes } = manifest;
302
+ const { bundles, function_config: functionConfig } = manifest;
282
303
  expect(bundles.length).toBe(1);
283
304
  expect(bundles[0].format).toBe('eszip2');
284
305
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
285
306
  // respects excludedPath from deploy config
286
- expect(routes[1].excluded_pattern).toEqual('^/func2/skip/?$');
307
+ expect(functionConfig.func2).toEqual({ excluded_patterns: ['^/func2/skip/?$'] });
287
308
  await cleanup();
288
309
  });
289
310
  test("Ignores entries in `importMapPaths` that don't point to an existing import map file", async () => {
@@ -9,5 +9,6 @@ export declare const enum Cache {
9
9
  export interface FunctionConfig {
10
10
  cache?: Cache;
11
11
  path?: string | string[];
12
+ excludedPath?: string | string[];
12
13
  }
13
14
  export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;
@@ -11,9 +11,8 @@ var ConfigExitCode;
11
11
  ConfigExitCode[ConfigExitCode["ImportError"] = 2] = "ImportError";
12
12
  ConfigExitCode[ConfigExitCode["NoConfig"] = 3] = "NoConfig";
13
13
  ConfigExitCode[ConfigExitCode["InvalidExport"] = 4] = "InvalidExport";
14
- ConfigExitCode[ConfigExitCode["RuntimeError"] = 5] = "RuntimeError";
15
- ConfigExitCode[ConfigExitCode["SerializationError"] = 6] = "SerializationError";
16
- ConfigExitCode[ConfigExitCode["InvalidDefaultExport"] = 7] = "InvalidDefaultExport";
14
+ ConfigExitCode[ConfigExitCode["SerializationError"] = 5] = "SerializationError";
15
+ ConfigExitCode[ConfigExitCode["InvalidDefaultExport"] = 6] = "InvalidDefaultExport";
17
16
  })(ConfigExitCode || (ConfigExitCode = {}));
18
17
  const getConfigExtractor = () => {
19
18
  const packagePath = getPackagePath();
@@ -75,14 +74,10 @@ const logConfigError = (func, exitCode, stderr, log) => {
75
74
  log.system(`No in-source config found for edge function at '${func.path}'`);
76
75
  break;
77
76
  case ConfigExitCode.InvalidExport:
78
- log.user(`'config' export in edge function at '${func.path}' must be a function`);
79
- break;
80
- case ConfigExitCode.RuntimeError:
81
- log.user(`Error while running 'config' function in edge function at '${func.path}'`);
82
- log.user(stderr);
77
+ log.user(`'config' export in edge function at '${func.path}' must be an object`);
83
78
  break;
84
79
  case ConfigExitCode.SerializationError:
85
- log.user(`'config' function in edge function at '${func.path}' must return an object with primitive values only`);
80
+ log.user(`'config' object in edge function at '${func.path}' must contain primitive values only`);
86
81
  break;
87
82
  case ConfigExitCode.InvalidDefaultExport:
88
83
  throw new BundleError(new Error(`Default export in '${func.path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
@@ -35,7 +35,7 @@ test('`getFunctionConfig` extracts configuration properties from function file',
35
35
  source: `
36
36
  export default async () => new Response("Hello from function two")
37
37
 
38
- export const config = () => ({})
38
+ export const config = {}
39
39
  `,
40
40
  },
41
41
  // Config with the wrong type
@@ -45,9 +45,9 @@ test('`getFunctionConfig` extracts configuration properties from function file',
45
45
  source: `
46
46
  export default async () => new Response("Hello from function two")
47
47
 
48
- export const config = {}
48
+ export const config = () => ({})
49
49
  `,
50
- userLog: /^'config' export in edge function at '(.*)' must be a function$/,
50
+ userLog: /^'config' export in edge function at '(.*)' must be an object$/,
51
51
  },
52
52
  // Config with a syntax error
53
53
  {
@@ -60,19 +60,6 @@ test('`getFunctionConfig` extracts configuration properties from function file',
60
60
  `,
61
61
  userLog: /^Could not load edge function at '(.*)'$/,
62
62
  },
63
- // Config that throws
64
- {
65
- expectedConfig: {},
66
- name: 'func5',
67
- source: `
68
- export default async () => new Response("Hello from function two")
69
-
70
- export const config = () => {
71
- throw new Error('uh-oh')
72
- }
73
- `,
74
- userLog: /^Error while running 'config' function in edge function at '(.*)'$/,
75
- },
76
63
  // Config with `path`
77
64
  {
78
65
  expectedConfig: { path: '/home' },
@@ -80,24 +67,9 @@ test('`getFunctionConfig` extracts configuration properties from function file',
80
67
  source: `
81
68
  export default async () => new Response("Hello from function three")
82
69
 
83
- export const config = () => ({ path: "/home" })
70
+ export const config = { path: "/home" }
84
71
  `,
85
72
  },
86
- // Config that prints to stdout
87
- {
88
- expectedConfig: { path: '/home' },
89
- name: 'func7',
90
- source: `
91
- export default async () => new Response("Hello from function three")
92
-
93
- export const config = () => {
94
- console.log("Hello from config!")
95
-
96
- return { path: "/home" }
97
- }
98
- `,
99
- userLog: /^Hello from config!$/,
100
- },
101
73
  ];
102
74
  for (const func of functions) {
103
75
  const logger = {
@@ -130,7 +102,7 @@ test('Ignores function paths from the in-source `config` function if the feature
130
102
  configPath: join(internalDirectory, 'config.json'),
131
103
  });
132
104
  const generatedFiles = await fs.readdir(distPath);
133
- expect(result.functions.length).toBe(6);
105
+ expect(result.functions.length).toBe(7);
134
106
  expect(generatedFiles.length).toBe(2);
135
107
  const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
136
108
  const manifest = JSON.parse(manifestFile);
@@ -163,22 +135,27 @@ test('Loads function paths from the in-source `config` function', async () => {
163
135
  },
164
136
  });
165
137
  const generatedFiles = await fs.readdir(distPath);
166
- expect(result.functions.length).toBe(6);
138
+ expect(result.functions.length).toBe(7);
167
139
  expect(generatedFiles.length).toBe(2);
168
140
  const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
169
141
  const manifest = JSON.parse(manifestFile);
170
- const { bundles, routes, post_cache_routes: postCacheRoutes } = manifest;
142
+ const { bundles, routes, post_cache_routes: postCacheRoutes, function_config: functionConfig } = manifest;
171
143
  expect(bundles.length).toBe(1);
172
144
  expect(bundles[0].format).toBe('eszip2');
173
145
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
174
- expect(routes.length).toBe(5);
146
+ expect(routes.length).toBe(6);
175
147
  expect(routes[0]).toEqual({ function: 'framework-func2', pattern: '^/framework-func2/?$' });
176
148
  expect(routes[1]).toEqual({ function: 'user-func2', pattern: '^/user-func2/?$' });
177
149
  expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$' });
178
150
  expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$' });
179
151
  expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$' });
152
+ expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5/.*/?$' });
180
153
  expect(postCacheRoutes.length).toBe(1);
181
154
  expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$' });
155
+ expect(Object.keys(functionConfig)).toHaveLength(1);
156
+ expect(functionConfig['user-func5']).toEqual({
157
+ excluded_patterns: ['^/user-func5/excluded/?$'],
158
+ });
182
159
  await cleanup();
183
160
  });
184
161
  test('Passes validation if default export exists and is a function', async () => {
@@ -17,13 +17,13 @@ export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig, dep
17
17
  else if ((_a = config.path) === null || _a === void 0 ? void 0 : _a.length) {
18
18
  const paths = Array.isArray(config.path) ? config.path : [config.path];
19
19
  paths.forEach((path) => {
20
- declarations.push({ ...declaration, ...config, path });
20
+ declarations.push({ ...declaration, cache: config.cache, path });
21
21
  });
22
22
  // With an in-source config without a path, add the config to the declaration
23
23
  }
24
24
  else {
25
25
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
26
- const { path, ...rest } = config;
26
+ const { path, excludedPath, ...rest } = config;
27
27
  declarations.push({ ...declaration, ...rest });
28
28
  }
29
29
  functionsVisited.add(declaration.function);
@@ -31,13 +31,12 @@ export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig, dep
31
31
  // Finally, we must create declarations for functions that are not declared
32
32
  // in the TOML at all.
33
33
  for (const name in functionsConfig) {
34
- const { ...config } = functionsConfig[name];
35
- const { path } = functionsConfig[name];
34
+ const { cache, path } = functionsConfig[name];
36
35
  // If we have path specified create a declaration for each path
37
36
  if (!functionsVisited.has(name) && path) {
38
37
  const paths = Array.isArray(path) ? path : [path];
39
38
  paths.forEach((singlePath) => {
40
- declarations.push({ ...config, function: name, path: singlePath });
39
+ declarations.push({ cache, function: name, path: singlePath });
41
40
  });
42
41
  }
43
42
  }
@@ -1,19 +1,15 @@
1
1
  import type { Bundle } from './bundle.js';
2
+ import { FunctionConfig } from './config.js';
2
3
  import type { Declaration } from './declaration.js';
3
4
  import { EdgeFunction } from './edge_function.js';
4
5
  import { Layer } from './layer.js';
5
- interface GenerateManifestOptions {
6
- bundles?: Bundle[];
7
- declarations?: Declaration[];
8
- functions: EdgeFunction[];
9
- importMap?: string;
10
- layers?: Layer[];
11
- }
12
6
  interface Route {
13
7
  function: string;
14
8
  name?: string;
15
9
  pattern: string;
16
- excluded_pattern?: string;
10
+ }
11
+ interface EdgeFunctionConfig {
12
+ excluded_patterns: string[];
17
13
  }
18
14
  interface Manifest {
19
15
  bundler_version: string;
@@ -28,20 +24,24 @@ interface Manifest {
28
24
  }[];
29
25
  routes: Route[];
30
26
  post_cache_routes: Route[];
27
+ function_config: Record<string, EdgeFunctionConfig>;
28
+ }
29
+ interface GenerateManifestOptions {
30
+ bundles?: Bundle[];
31
+ declarations?: Declaration[];
32
+ functions: EdgeFunction[];
33
+ functionConfig?: Record<string, FunctionConfig>;
34
+ importMap?: string;
35
+ layers?: Layer[];
31
36
  }
32
37
  interface Route {
33
38
  function: string;
34
39
  name?: string;
35
40
  pattern: string;
36
41
  }
37
- declare const generateManifest: ({ bundles, declarations, functions, importMap, layers, }: GenerateManifestOptions) => Manifest;
38
- interface WriteManifestOptions {
39
- bundles: Bundle[];
40
- declarations: Declaration[];
42
+ declare const generateManifest: ({ bundles, declarations, functions, functionConfig, importMap, layers, }: GenerateManifestOptions) => Manifest;
43
+ interface WriteManifestOptions extends GenerateManifestOptions {
41
44
  distDirectory: string;
42
- functions: EdgeFunction[];
43
- importMap?: string;
44
- layers?: Layer[];
45
45
  }
46
- declare const writeManifest: ({ bundles, declarations, distDirectory, functions, importMap, layers, }: WriteManifestOptions) => Promise<Manifest>;
46
+ declare const writeManifest: ({ distDirectory, ...rest }: WriteManifestOptions) => Promise<Manifest>;
47
47
  export { generateManifest, Manifest, writeManifest };
@@ -4,9 +4,26 @@ import globToRegExp from 'glob-to-regexp';
4
4
  import { getPackageVersion } from './package_json.js';
5
5
  import { nonNullable } from './utils/non_nullable.js';
6
6
  const serializePattern = (regex) => regex.source.replace(/\\\//g, '/');
7
- const generateManifest = ({ bundles = [], declarations = [], functions, importMap, layers = [], }) => {
7
+ const sanitizeEdgeFunctionConfig = (config) => {
8
+ const newConfig = {};
9
+ for (const [name, functionConfig] of Object.entries(config)) {
10
+ if (functionConfig.excluded_patterns.length !== 0) {
11
+ newConfig[name] = functionConfig;
12
+ }
13
+ }
14
+ return newConfig;
15
+ };
16
+ const generateManifest = ({ bundles = [], declarations = [], functions, functionConfig = {}, importMap, layers = [], }) => {
8
17
  const preCacheRoutes = [];
9
18
  const postCacheRoutes = [];
19
+ const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
20
+ for (const [name, { excludedPath }] of Object.entries(functionConfig)) {
21
+ if (excludedPath) {
22
+ const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
23
+ const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern);
24
+ manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
25
+ }
26
+ }
10
27
  declarations.forEach((declaration) => {
11
28
  const func = functions.find(({ name }) => declaration.function === name);
12
29
  if (func === undefined) {
@@ -20,7 +37,7 @@ const generateManifest = ({ bundles = [], declarations = [], functions, importMa
20
37
  };
21
38
  const excludedPattern = getExcludedRegularExpression(declaration);
22
39
  if (excludedPattern) {
23
- route.excluded_pattern = serializePattern(excludedPattern);
40
+ manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern));
24
41
  }
25
42
  if (declaration.cache === "manual" /* Cache.Manual */) {
26
43
  postCacheRoutes.push(route);
@@ -40,6 +57,7 @@ const generateManifest = ({ bundles = [], declarations = [], functions, importMa
40
57
  bundler_version: getPackageVersion(),
41
58
  layers,
42
59
  import_map: importMap,
60
+ function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig),
43
61
  };
44
62
  return manifest;
45
63
  };
@@ -67,8 +85,8 @@ const getExcludedRegularExpression = (declaration) => {
67
85
  return pathToRegularExpression(declaration.excludedPath);
68
86
  }
69
87
  };
70
- const writeManifest = async ({ bundles, declarations = [], distDirectory, functions, importMap, layers, }) => {
71
- const manifest = generateManifest({ bundles, declarations, functions, importMap, layers });
88
+ const writeManifest = async ({ distDirectory, ...rest }) => {
89
+ const manifest = generateManifest(rest);
72
90
  const manifestPath = join(distDirectory, 'manifest.json');
73
91
  await fs.writeFile(manifestPath, JSON.stringify(manifest));
74
92
  return manifest;
@@ -53,10 +53,14 @@ test('Generates a manifest with excluded paths and patterns', () => {
53
53
  ];
54
54
  const manifest = generateManifest({ bundles: [], declarations, functions });
55
55
  const expectedRoutes = [
56
- { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$', excluded_pattern: '^/f1/exclude/?$' },
57
- { function: 'func-2', pattern: '^/f2/.*/?$', excluded_pattern: '^/f2/exclude$' },
56
+ { function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$' },
57
+ { function: 'func-2', pattern: '^/f2/.*/?$' },
58
58
  ];
59
59
  expect(manifest.routes).toEqual(expectedRoutes);
60
+ expect(manifest.function_config).toEqual({
61
+ 'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
62
+ 'func-2': { excluded_patterns: ['^/f2/exclude$'] },
63
+ });
60
64
  expect(manifest.bundler_version).toBe(env.npm_package_version);
61
65
  });
62
66
  test('Excludes functions for which there are function files but no matching config declarations', () => {
@@ -1,6 +1,6 @@
1
1
  class NPMImportError extends Error {
2
2
  constructor(originalError, moduleName) {
3
- super(`It seems like you're trying to import an npm module. This is only supported in Deno via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/${moduleName}"'?`);
3
+ super(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/${moduleName}"'?`);
4
4
  this.name = 'NPMImportError';
5
5
  this.stack = originalError.stack;
6
6
  // https://github.com/microsoft/TypeScript-wiki/blob/8a66ecaf77118de456f7cd9c56848a40fe29b9b4/Breaking-Changes.md#implicit-any-error-raised-for-un-annotated-callback-arguments-with-no-matching-overload-arguments
@@ -14,6 +14,11 @@ const wrapNpmImportError = (input) => {
14
14
  const [, moduleName] = match;
15
15
  return new NPMImportError(input, moduleName);
16
16
  }
17
+ const schemeMatch = input.message.match(/Error: Module not found "npm:(.*)"/);
18
+ if (schemeMatch !== null) {
19
+ const [, moduleName] = schemeMatch;
20
+ return new NPMImportError(input, moduleName);
21
+ }
17
22
  }
18
23
  return input;
19
24
  };
@@ -86,6 +86,23 @@ declare const edgeManifestSchema: {
86
86
  bundler_version: {
87
87
  type: string;
88
88
  };
89
+ function_config: {
90
+ type: string;
91
+ items: {
92
+ type: string;
93
+ required: never[];
94
+ properties: {
95
+ excluded_patterns: {
96
+ type: string;
97
+ items: {
98
+ type: string;
99
+ format: string;
100
+ errorMessage: string;
101
+ };
102
+ };
103
+ };
104
+ };
105
+ };
89
106
  };
90
107
  additionalProperties: boolean;
91
108
  };
@@ -21,6 +21,20 @@ const routesSchema = {
21
21
  },
22
22
  additionalProperties: false,
23
23
  };
24
+ const functionConfigSchema = {
25
+ type: 'object',
26
+ required: [],
27
+ properties: {
28
+ excluded_patterns: {
29
+ type: 'array',
30
+ items: {
31
+ type: 'string',
32
+ format: 'regexPattern',
33
+ errorMessage: 'excluded_patterns needs to be an array of regex that starts with ^ and ends with $ without any additional slashes before and afterwards',
34
+ },
35
+ },
36
+ },
37
+ };
24
38
  const layersSchema = {
25
39
  type: 'object',
26
40
  required: ['flag', 'name'],
@@ -53,6 +67,7 @@ const edgeManifestSchema = {
53
67
  },
54
68
  import_map: { type: 'string' },
55
69
  bundler_version: { type: 'string' },
70
+ function_config: { type: 'object', items: functionConfigSchema },
56
71
  },
57
72
  additionalProperties: false,
58
73
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "7.1.0",
3
+ "version": "8.1.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",