@netlify/edge-bundler 7.0.1 → 8.0.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) {
@@ -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,10 +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 } = manifest;
302
+ const { bundles, routes } = 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);
306
+ // respects excludedPath from deploy config
307
+ expect(routes[1].excluded_pattern).toEqual('^/func2/skip/?$');
285
308
  await cleanup();
286
309
  });
287
310
  test("Ignores entries in `importMapPaths` that don't point to an existing import map file", async () => {
@@ -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,23 +67,8 @@ 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" })
84
- `,
85
- },
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
- }
70
+ export const config = { path: "/home" }
98
71
  `,
99
- userLog: /^Hello from config!$/,
100
72
  },
101
73
  ];
102
74
  for (const func of functions) {
@@ -7,9 +7,11 @@ interface BaseDeclaration {
7
7
  }
8
8
  type DeclarationWithPath = BaseDeclaration & {
9
9
  path: string;
10
+ excludedPath?: string;
10
11
  };
11
12
  type DeclarationWithPattern = BaseDeclaration & {
12
13
  pattern: string;
14
+ excludedPattern?: string;
13
15
  };
14
16
  type Declaration = DeclarationWithPath | DeclarationWithPattern;
15
17
  export declare const getDeclarationsFromConfig: (tomlDeclarations: Declaration[], functionsConfig: Record<string, FunctionConfig>, deployConfig: DeployConfig) => Declaration[];
@@ -92,3 +92,10 @@ test('In-source config works if path property is an empty array with cache value
92
92
  const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }];
93
93
  expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations);
94
94
  });
95
+ test('netlify.toml-defined excludedPath are respected', () => {
96
+ const tomlConfig = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
97
+ const funcConfig = {};
98
+ const expectedDeclarations = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
99
+ const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig);
100
+ expect(declarations).toEqual(expectedDeclarations);
101
+ });
@@ -9,6 +9,12 @@ interface GenerateManifestOptions {
9
9
  importMap?: string;
10
10
  layers?: Layer[];
11
11
  }
12
+ interface Route {
13
+ function: string;
14
+ name?: string;
15
+ pattern: string;
16
+ excluded_pattern?: string;
17
+ }
12
18
  interface Manifest {
13
19
  bundler_version: string;
14
20
  bundles: {
@@ -20,16 +26,13 @@ interface Manifest {
20
26
  name: string;
21
27
  flag: string;
22
28
  }[];
23
- routes: {
24
- function: string;
25
- name?: string;
26
- pattern: string;
27
- }[];
28
- post_cache_routes: {
29
- function: string;
30
- name?: string;
31
- pattern: string;
32
- }[];
29
+ routes: Route[];
30
+ post_cache_routes: Route[];
31
+ }
32
+ interface Route {
33
+ function: string;
34
+ name?: string;
35
+ pattern: string;
33
36
  }
34
37
  declare const generateManifest: ({ bundles, declarations, functions, importMap, layers, }: GenerateManifestOptions) => Manifest;
35
38
  interface WriteManifestOptions {
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import globToRegExp from 'glob-to-regexp';
4
4
  import { getPackageVersion } from './package_json.js';
5
5
  import { nonNullable } from './utils/non_nullable.js';
6
+ const serializePattern = (regex) => regex.source.replace(/\\\//g, '/');
6
7
  const generateManifest = ({ bundles = [], declarations = [], functions, importMap, layers = [], }) => {
7
8
  const preCacheRoutes = [];
8
9
  const postCacheRoutes = [];
@@ -12,12 +13,15 @@ const generateManifest = ({ bundles = [], declarations = [], functions, importMa
12
13
  return;
13
14
  }
14
15
  const pattern = getRegularExpression(declaration);
15
- const serializablePattern = pattern.source.replace(/\\\//g, '/');
16
16
  const route = {
17
17
  function: func.name,
18
18
  name: declaration.name,
19
- pattern: serializablePattern,
19
+ pattern: serializePattern(pattern),
20
20
  };
21
+ const excludedPattern = getExcludedRegularExpression(declaration);
22
+ if (excludedPattern) {
23
+ route.excluded_pattern = serializePattern(excludedPattern);
24
+ }
21
25
  if (declaration.cache === "manual" /* Cache.Manual */) {
22
26
  postCacheRoutes.push(route);
23
27
  }
@@ -39,19 +43,30 @@ const generateManifest = ({ bundles = [], declarations = [], functions, importMa
39
43
  };
40
44
  return manifest;
41
45
  };
42
- const getRegularExpression = (declaration) => {
43
- if ('pattern' in declaration) {
44
- return new RegExp(declaration.pattern);
45
- }
46
+ const pathToRegularExpression = (path) => {
46
47
  // We use the global flag so that `globToRegExp` will not wrap the expression
47
48
  // with `^` and `$`. We'll do that ourselves.
48
- const regularExpression = globToRegExp(declaration.path, { flags: 'g' });
49
+ const regularExpression = globToRegExp(path, { flags: 'g' });
49
50
  // Wrapping the expression source with `^` and `$`. Also, adding an optional
50
51
  // trailing slash, so that a declaration of `path: "/foo"` matches requests
51
52
  // for both `/foo` and `/foo/`.
52
53
  const normalizedSource = `^${regularExpression.source}\\/?$`;
53
54
  return new RegExp(normalizedSource);
54
55
  };
56
+ const getRegularExpression = (declaration) => {
57
+ if ('pattern' in declaration) {
58
+ return new RegExp(declaration.pattern);
59
+ }
60
+ return pathToRegularExpression(declaration.path);
61
+ };
62
+ const getExcludedRegularExpression = (declaration) => {
63
+ if ('pattern' in declaration && declaration.excludedPattern) {
64
+ return new RegExp(declaration.excludedPattern);
65
+ }
66
+ if ('path' in declaration && declaration.excludedPath) {
67
+ return pathToRegularExpression(declaration.excludedPath);
68
+ }
69
+ };
55
70
  const writeManifest = async ({ bundles, declarations = [], distDirectory, functions, importMap, layers, }) => {
56
71
  const manifest = generateManifest({ bundles, declarations, functions, importMap, layers });
57
72
  const manifestPath = join(distDirectory, 'manifest.json');
@@ -42,6 +42,23 @@ test('Generates a manifest with display names', () => {
42
42
  expect(manifest.routes).toEqual(expectedRoutes);
43
43
  expect(manifest.bundler_version).toBe(env.npm_package_version);
44
44
  });
45
+ test('Generates a manifest with excluded paths and patterns', () => {
46
+ const functions = [
47
+ { name: 'func-1', path: '/path/to/func-1.ts' },
48
+ { name: 'func-2', path: '/path/to/func-2.ts' },
49
+ ];
50
+ const declarations = [
51
+ { function: 'func-1', name: 'Display Name', path: '/f1/*', excludedPath: '/f1/exclude' },
52
+ { function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: '^/f2/exclude$' },
53
+ ];
54
+ const manifest = generateManifest({ bundles: [], declarations, functions });
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$' },
58
+ ];
59
+ expect(manifest.routes).toEqual(expectedRoutes);
60
+ expect(manifest.bundler_version).toBe(env.npm_package_version);
61
+ });
45
62
  test('Excludes functions for which there are function files but no matching config declarations', () => {
46
63
  const bundle1 = {
47
64
  extension: '.ext2',
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "7.0.1",
3
+ "version": "8.0.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",