@netlify/edge-bundler 5.2.0 → 5.3.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.
@@ -3,7 +3,8 @@ import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.28.0/mod.ts'
3
3
  import * as path from 'https://deno.land/std@0.127.0/path/mod.ts'
4
4
 
5
5
  import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts'
6
- import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
6
+ import { virtualRoot } from '../../shared/consts.ts'
7
+ import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
7
8
  import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
8
9
 
9
10
  interface FunctionReference {
@@ -6,4 +6,5 @@ export interface Bundle {
6
6
  extension: string;
7
7
  format: BundleFormat;
8
8
  hash: string;
9
+ importMapURL?: string;
9
10
  }
@@ -76,6 +76,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
76
76
  declarations,
77
77
  distDirectory,
78
78
  functions,
79
+ importMapURL: functionBundle.importMapURL,
79
80
  layers: deployConfig.layers,
80
81
  });
81
82
  if (distImportMapPath) {
@@ -4,62 +4,65 @@ import process from 'process';
4
4
  import { deleteAsync } from 'del';
5
5
  import tmp from 'tmp-promise';
6
6
  import { test, expect } from 'vitest';
7
- import { fixturesDir } from '../test/util.js';
7
+ import { useFixture } from '../test/util.js';
8
8
  import { BundleError } from './bundle_error.js';
9
9
  import { bundle } from './bundler.js';
10
10
  import { isNodeError } from './utils/error.js';
11
11
  test('Produces an ESZIP bundle', async () => {
12
- const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
13
- const tmpDir = await tmp.dir();
12
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
14
13
  const declarations = [
15
14
  {
16
15
  function: 'func1',
17
16
  path: '/func1',
18
17
  },
19
18
  ];
20
- const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
21
- basePath: fixturesDir,
19
+ const sourceDirectory = join(basePath, 'functions');
20
+ const result = await bundle([sourceDirectory], distPath, declarations, {
21
+ basePath,
22
22
  configPath: join(sourceDirectory, 'config.json'),
23
23
  });
24
- const generatedFiles = await fs.readdir(tmpDir.path);
24
+ const generatedFiles = await fs.readdir(distPath);
25
25
  expect(result.functions.length).toBe(1);
26
- expect(generatedFiles.length).toBe(2);
27
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
26
+ // ESZIP, manifest and import map.
27
+ expect(generatedFiles.length).toBe(3);
28
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
28
29
  const manifest = JSON.parse(manifestFile);
29
- const { bundles } = manifest;
30
+ const { bundles, import_map: importMapURL } = manifest;
30
31
  expect(bundles.length).toBe(1);
31
32
  expect(bundles[0].format).toBe('eszip2');
32
33
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
33
- await fs.rmdir(tmpDir.path, { recursive: true });
34
+ expect(importMapURL).toBe('file:///root/.netlify/edge-functions-dist/import_map.json');
35
+ await cleanup();
34
36
  });
35
37
  test('Uses the vendored eszip module instead of fetching it from deno.land', async () => {
36
- const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
37
- const tmpDir = await tmp.dir();
38
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
38
39
  const declarations = [
39
40
  {
40
41
  function: 'func1',
41
42
  path: '/func1',
42
43
  },
43
44
  ];
44
- const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
45
- basePath: fixturesDir,
45
+ const sourceDirectory = join(basePath, 'functions');
46
+ const result = await bundle([sourceDirectory], distPath, declarations, {
47
+ basePath,
46
48
  configPath: join(sourceDirectory, 'config.json'),
47
49
  });
48
- const generatedFiles = await fs.readdir(tmpDir.path);
50
+ const generatedFiles = await fs.readdir(distPath);
49
51
  expect(result.functions.length).toBe(1);
50
- expect(generatedFiles.length).toBe(2);
51
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
52
+ // ESZIP, manifest and import map.
53
+ expect(generatedFiles.length).toBe(3);
54
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
52
55
  const manifest = JSON.parse(manifestFile);
53
56
  const { bundles } = manifest;
54
57
  expect(bundles.length).toBe(1);
55
58
  expect(bundles[0].format).toBe('eszip2');
56
59
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
57
- await fs.rmdir(tmpDir.path, { recursive: true });
60
+ await cleanup();
58
61
  });
59
62
  test('Adds a custom error property to user errors during bundling', async () => {
60
63
  expect.assertions(2);
61
- const sourceDirectory = resolve(fixturesDir, 'invalid_functions', 'functions');
62
- const tmpDir = await tmp.dir();
64
+ const { basePath, cleanup, distPath } = await useFixture('invalid_functions');
65
+ const sourceDirectory = join(basePath, 'functions');
63
66
  const declarations = [
64
67
  {
65
68
  function: 'func1',
@@ -67,7 +70,7 @@ test('Adds a custom error property to user errors during bundling', async () =>
67
70
  },
68
71
  ];
69
72
  try {
70
- await bundle([sourceDirectory], tmpDir.path, declarations);
73
+ await bundle([sourceDirectory], distPath, declarations, { basePath });
71
74
  }
72
75
  catch (error) {
73
76
  expect(error).toBeInstanceOf(BundleError);
@@ -79,11 +82,14 @@ test('Adds a custom error property to user errors during bundling', async () =>
79
82
  type: 'functionsBundling',
80
83
  });
81
84
  }
85
+ finally {
86
+ await cleanup();
87
+ }
82
88
  });
83
89
  test('Prints a nice error message when user tries importing NPM module', async () => {
84
90
  expect.assertions(2);
85
- const sourceDirectory = resolve(fixturesDir, 'imports_npm_module', 'functions');
86
- const tmpDir = await tmp.dir();
91
+ const { basePath, cleanup, distPath } = await useFixture('imports_npm_module');
92
+ const sourceDirectory = join(basePath, 'functions');
87
93
  const declarations = [
88
94
  {
89
95
  function: 'func1',
@@ -91,18 +97,21 @@ test('Prints a nice error message when user tries importing NPM module', async (
91
97
  },
92
98
  ];
93
99
  try {
94
- await bundle([sourceDirectory], tmpDir.path, declarations);
100
+ await bundle([sourceDirectory], distPath, declarations, { basePath });
95
101
  }
96
102
  catch (error) {
97
103
  expect(error).toBeInstanceOf(BundleError);
98
104
  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"'?`);
99
105
  }
106
+ finally {
107
+ await cleanup();
108
+ }
100
109
  });
101
110
  test('Does not add a custom error property to system errors during bundling', async () => {
102
111
  expect.assertions(1);
103
112
  try {
104
113
  // @ts-expect-error Sending bad input to `bundle` to force a system error.
105
- await bundle([123, 321], tmpDir.path, declarations);
114
+ await bundle([123, 321], '/some/directory', declarations);
106
115
  }
107
116
  catch (error) {
108
117
  expect(error).not.toBeInstanceOf(BundleError);
@@ -110,8 +119,8 @@ test('Does not add a custom error property to system errors during bundling', as
110
119
  });
111
120
  test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_cache_deno_dir` feature flag is set', async () => {
112
121
  expect.assertions(6);
113
- const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
114
- const outDir = await tmp.dir();
122
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
123
+ const sourceDirectory = join(basePath, 'functions');
115
124
  const cacheDir = await tmp.dir();
116
125
  const declarations = [
117
126
  {
@@ -120,15 +129,16 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
120
129
  },
121
130
  ];
122
131
  const options = {
123
- basePath: fixturesDir,
132
+ basePath,
124
133
  cacheDirectory: cacheDir.path,
125
134
  configPath: join(sourceDirectory, 'config.json'),
126
135
  };
127
136
  // Run #1, feature flag off: The directory should not be populated.
128
- const result1 = await bundle([sourceDirectory], outDir.path, declarations, options);
129
- const outFiles1 = await fs.readdir(outDir.path);
137
+ const result1 = await bundle([sourceDirectory], distPath, declarations, options);
138
+ const outFiles1 = await fs.readdir(distPath);
130
139
  expect(result1.functions.length).toBe(1);
131
- expect(outFiles1.length).toBe(2);
140
+ // ESZIP, manifest and import map.
141
+ expect(outFiles1.length).toBe(3);
132
142
  try {
133
143
  await fs.readdir(join(cacheDir.path, 'deno_dir'));
134
144
  }
@@ -136,46 +146,48 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
136
146
  expect(error).toBeInstanceOf(Error);
137
147
  }
138
148
  // Run #2, feature flag on: The directory should be populated.
139
- const result2 = await bundle([sourceDirectory], outDir.path, declarations, {
149
+ const result2 = await bundle([sourceDirectory], distPath, declarations, {
140
150
  ...options,
141
151
  featureFlags: {
142
152
  edge_functions_cache_deno_dir: true,
143
153
  },
144
154
  });
145
- const outFiles2 = await fs.readdir(outDir.path);
155
+ const outFiles2 = await fs.readdir(distPath);
146
156
  expect(result2.functions.length).toBe(1);
147
- expect(outFiles2.length).toBe(2);
157
+ // ESZIP, manifest and import map.
158
+ expect(outFiles2.length).toBe(3);
148
159
  const denoDir2 = await fs.readdir(join(cacheDir.path, 'deno_dir'));
149
160
  expect(denoDir2.includes('gen')).toBe(true);
150
- await fs.rmdir(outDir.path, { recursive: true });
161
+ await cleanup();
151
162
  });
152
163
  test('Supports import maps with relative paths', async () => {
153
- const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
154
- const tmpDir = await tmp.dir();
164
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
165
+ const sourceDirectory = join(basePath, 'functions');
155
166
  const declarations = [
156
167
  {
157
168
  function: 'func1',
158
169
  path: '/func1',
159
170
  },
160
171
  ];
161
- const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
162
- basePath: fixturesDir,
172
+ const result = await bundle([sourceDirectory], distPath, declarations, {
173
+ basePath,
163
174
  configPath: join(sourceDirectory, 'config.json'),
164
175
  });
165
- const generatedFiles = await fs.readdir(tmpDir.path);
176
+ const generatedFiles = await fs.readdir(distPath);
166
177
  expect(result.functions.length).toBe(1);
167
- expect(generatedFiles.length).toBe(2);
168
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
178
+ // ESZIP, manifest and import map.
179
+ expect(generatedFiles.length).toBe(3);
180
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
169
181
  const manifest = JSON.parse(manifestFile);
170
182
  const { bundles } = manifest;
171
183
  expect(bundles.length).toBe(1);
172
184
  expect(bundles[0].format).toBe('eszip2');
173
185
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
174
- await fs.rmdir(tmpDir.path, { recursive: true });
186
+ await cleanup();
175
187
  });
176
188
  test('Ignores any user-defined `deno.json` files', async () => {
177
- const fixtureDir = join(fixturesDir, 'with_import_maps');
178
- const tmpDir = await tmp.dir();
189
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
190
+ const sourceDirectory = join(basePath, 'functions');
179
191
  const declarations = [
180
192
  {
181
193
  function: 'func1',
@@ -209,15 +221,16 @@ test('Ignores any user-defined `deno.json` files', async () => {
209
221
  }
210
222
  }
211
223
  await fs.writeFile(denoConfigPath, JSON.stringify(denoConfig));
212
- expect(() => bundle([join(fixtureDir, 'functions')], tmpDir.path, declarations, {
213
- basePath: fixturesDir,
214
- configPath: join(fixtureDir, 'functions', 'config.json'),
224
+ expect(() => bundle([sourceDirectory], distPath, declarations, {
225
+ basePath,
226
+ configPath: join(sourceDirectory, 'config.json'),
215
227
  })).not.toThrow();
216
- await deleteAsync([tmpDir.path, denoConfigPath, importMapFile.path], { force: true });
228
+ await cleanup();
229
+ await deleteAsync([denoConfigPath, importMapFile.path], { force: true });
217
230
  });
218
231
  test('Processes a function that imports a custom layer', async () => {
219
- const sourceDirectory = resolve(fixturesDir, 'with_layers', 'functions');
220
- const tmpDir = await tmp.dir();
232
+ const { basePath, cleanup, distPath } = await useFixture('with_layers');
233
+ const sourceDirectory = join(basePath, 'functions');
221
234
  const declarations = [
222
235
  {
223
236
  function: 'func1',
@@ -225,44 +238,45 @@ test('Processes a function that imports a custom layer', async () => {
225
238
  },
226
239
  ];
227
240
  const layer = { name: 'layer:test', flag: 'edge-functions-layer-test' };
228
- const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
229
- basePath: fixturesDir,
241
+ const result = await bundle([sourceDirectory], distPath, declarations, {
242
+ basePath,
230
243
  configPath: join(sourceDirectory, 'config.json'),
231
244
  });
232
- const generatedFiles = await fs.readdir(tmpDir.path);
245
+ const generatedFiles = await fs.readdir(distPath);
233
246
  expect(result.functions.length).toBe(1);
234
- expect(generatedFiles.length).toBe(2);
235
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
247
+ // ESZIP, manifest and import map.
248
+ expect(generatedFiles.length).toBe(3);
249
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
236
250
  const manifest = JSON.parse(manifestFile);
237
251
  const { bundles, layers } = manifest;
238
252
  expect(bundles.length).toBe(1);
239
253
  expect(bundles[0].format).toBe('eszip2');
240
254
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
241
255
  expect(layers).toEqual([layer]);
242
- await fs.rmdir(tmpDir.path, { recursive: true });
256
+ await cleanup();
243
257
  });
244
258
  test('Loads declarations and import maps from the deploy configuration', async () => {
245
- const fixtureDir = resolve(fixturesDir, 'with_deploy_config');
246
- const tmpDir = await tmp.dir();
259
+ const { basePath, cleanup, distPath } = await useFixture('with_deploy_config');
247
260
  const declarations = [
248
261
  {
249
262
  function: 'func1',
250
263
  path: '/func1',
251
264
  },
252
265
  ];
253
- const directories = [join(fixtureDir, 'netlify', 'edge-functions'), join(fixtureDir, '.netlify', 'edge-functions')];
254
- const result = await bundle(directories, tmpDir.path, declarations, {
255
- basePath: fixtureDir,
256
- configPath: join(fixtureDir, '.netlify', 'edge-functions', 'config.json'),
266
+ const directories = [join(basePath, 'netlify', 'edge-functions'), join(basePath, '.netlify', 'edge-functions')];
267
+ const result = await bundle(directories, distPath, declarations, {
268
+ basePath,
269
+ configPath: join(basePath, '.netlify', 'edge-functions', 'config.json'),
257
270
  });
258
- const generatedFiles = await fs.readdir(tmpDir.path);
271
+ const generatedFiles = await fs.readdir(distPath);
259
272
  expect(result.functions.length).toBe(2);
260
- expect(generatedFiles.length).toBe(2);
261
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
273
+ // ESZIP, manifest and import map.
274
+ expect(generatedFiles.length).toBe(3);
275
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
262
276
  const manifest = JSON.parse(manifestFile);
263
277
  const { bundles } = manifest;
264
278
  expect(bundles.length).toBe(1);
265
279
  expect(bundles[0].format).toBe('eszip2');
266
280
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
267
- await fs.rmdir(tmpDir.path, { recursive: true });
281
+ await cleanup();
268
282
  });
@@ -8,6 +8,6 @@ export declare const enum Cache {
8
8
  }
9
9
  export interface FunctionConfig {
10
10
  cache?: Cache;
11
- path?: string;
11
+ path?: string | string[];
12
12
  }
13
13
  export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'url';
4
4
  import { deleteAsync } from 'del';
5
5
  import tmp from 'tmp-promise';
6
6
  import { test, expect, vi } from 'vitest';
7
- import { fixturesDir } from '../test/util.js';
7
+ import { fixturesDir, useFixture } from '../test/util.js';
8
8
  import { DenoBridge } from './bridge.js';
9
9
  import { bundle } from './bundler.js';
10
10
  import { getFunctionConfig } from './config.js';
@@ -121,30 +121,31 @@ test('`getFunctionConfig` extracts configuration properties from function file',
121
121
  await deleteAsync(tmpDir, { force: true });
122
122
  });
123
123
  test('Ignores function paths from the in-source `config` function if the feature flag is off', async () => {
124
- const userDirectory = resolve(fixturesDir, 'with_config', 'netlify', 'edge-functions');
125
- const internalDirectory = resolve(fixturesDir, 'with_config', '.netlify', 'edge-functions');
126
- const tmpDir = await tmp.dir();
124
+ const { basePath, cleanup, distPath } = await useFixture('with_config');
125
+ const userDirectory = resolve(basePath, 'netlify', 'edge-functions');
126
+ const internalDirectory = resolve(basePath, '.netlify', 'edge-functions');
127
127
  const declarations = [];
128
- const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
129
- basePath: fixturesDir,
128
+ const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
129
+ basePath,
130
130
  configPath: join(internalDirectory, 'config.json'),
131
131
  });
132
- const generatedFiles = await fs.readdir(tmpDir.path);
132
+ const generatedFiles = await fs.readdir(distPath);
133
133
  expect(result.functions.length).toBe(6);
134
- expect(generatedFiles.length).toBe(2);
135
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
134
+ // ESZIP, manifest and import map.
135
+ expect(generatedFiles.length).toBe(3);
136
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
136
137
  const manifest = JSON.parse(manifestFile);
137
138
  const { bundles, routes } = manifest;
138
139
  expect(bundles.length).toBe(1);
139
140
  expect(bundles[0].format).toBe('eszip2');
140
141
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
141
142
  expect(routes.length).toBe(0);
142
- await fs.rmdir(tmpDir.path, { recursive: true });
143
+ await cleanup();
143
144
  });
144
145
  test('Loads function paths from the in-source `config` function', async () => {
145
- const userDirectory = resolve(fixturesDir, 'with_config', 'netlify', 'edge-functions');
146
- const internalDirectory = resolve(fixturesDir, 'with_config', '.netlify', 'edge-functions');
147
- const tmpDir = await tmp.dir();
146
+ const { basePath, cleanup, distPath } = await useFixture('with_config');
147
+ const userDirectory = resolve(basePath, 'netlify', 'edge-functions');
148
+ const internalDirectory = resolve(basePath, '.netlify', 'edge-functions');
148
149
  const declarations = [
149
150
  {
150
151
  function: 'framework-func2',
@@ -155,17 +156,18 @@ test('Loads function paths from the in-source `config` function', async () => {
155
156
  path: '/user-func2',
156
157
  },
157
158
  ];
158
- const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
159
- basePath: fixturesDir,
159
+ const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
160
+ basePath,
160
161
  configPath: join(internalDirectory, 'config.json'),
161
162
  featureFlags: {
162
163
  edge_functions_config_export: true,
163
164
  },
164
165
  });
165
- const generatedFiles = await fs.readdir(tmpDir.path);
166
+ const generatedFiles = await fs.readdir(distPath);
166
167
  expect(result.functions.length).toBe(6);
167
- expect(generatedFiles.length).toBe(2);
168
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
168
+ // ESZIP, manifest and import map.
169
+ expect(generatedFiles.length).toBe(3);
170
+ const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
169
171
  const manifest = JSON.parse(manifestFile);
170
172
  const { bundles, routes, post_cache_routes: postCacheRoutes } = manifest;
171
173
  expect(bundles.length).toBe(1);
@@ -179,7 +181,7 @@ test('Loads function paths from the in-source `config` function', async () => {
179
181
  expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$' });
180
182
  expect(postCacheRoutes.length).toBe(1);
181
183
  expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$' });
182
- await fs.rmdir(tmpDir.path, { recursive: true });
184
+ await cleanup();
183
185
  });
184
186
  test('Passes validation if default export exists and is a function', async () => {
185
187
  const { path: tmpDir } = await tmp.dir();
@@ -7,16 +7,38 @@ export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig, dep
7
7
  // a function configuration object, we replace the path because that object
8
8
  // takes precedence.
9
9
  for (const declaration of [...tomlDeclarations, ...deployConfig.declarations]) {
10
- const config = (_a = functionsConfig[declaration.function]) !== null && _a !== void 0 ? _a : {};
10
+ const config = functionsConfig[declaration.function];
11
+ // If no config is found, add the declaration as is
12
+ if (!config) {
13
+ declarations.push(declaration);
14
+ // If we have a path specified as either a string or non-empty array
15
+ // create a declaration for each path
16
+ }
17
+ else if ((_a = config.path) === null || _a === void 0 ? void 0 : _a.length) {
18
+ const paths = Array.isArray(config.path) ? config.path : [config.path];
19
+ paths.forEach((path) => {
20
+ declarations.push({ ...declaration, ...config, path });
21
+ });
22
+ // With an in-source config without a path, add the config to the declaration
23
+ }
24
+ else {
25
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
26
+ const { path, ...rest } = config;
27
+ declarations.push({ ...declaration, ...rest });
28
+ }
11
29
  functionsVisited.add(declaration.function);
12
- declarations.push({ ...declaration, ...config });
13
30
  }
14
31
  // Finally, we must create declarations for functions that are not declared
15
32
  // in the TOML at all.
16
33
  for (const name in functionsConfig) {
17
- const { path, ...config } = functionsConfig[name];
34
+ const { ...config } = functionsConfig[name];
35
+ const { path } = functionsConfig[name];
36
+ // If we have path specified create a declaration for each path
18
37
  if (!functionsVisited.has(name) && path) {
19
- declarations.push({ ...config, function: name, path });
38
+ const paths = Array.isArray(path) ? path : [path];
39
+ paths.forEach((singlePath) => {
40
+ declarations.push({ ...config, function: name, path: singlePath });
41
+ });
20
42
  }
21
43
  }
22
44
  return declarations;
@@ -5,29 +5,30 @@ const deployConfig = {
5
5
  declarations: [],
6
6
  layers: [],
7
7
  };
8
- test('In source config takes precedence over netlify.toml config', () => {
8
+ test('In-source config takes precedence over netlify.toml config', () => {
9
9
  const tomlConfig = [
10
10
  { function: 'geolocation', path: '/geo', cache: 'off' },
11
11
  { function: 'json', path: '/json', cache: 'manual' },
12
12
  ];
13
13
  const funcConfig = {
14
- geolocation: { path: '/geo-isc', cache: 'manual' },
14
+ geolocation: { path: ['/geo-isc', '/*'], cache: 'manual' },
15
15
  json: { path: '/json', cache: 'off' },
16
16
  };
17
17
  const expectedDeclarations = [
18
18
  { function: 'geolocation', path: '/geo-isc', cache: 'manual' },
19
+ { function: 'geolocation', path: '/*', cache: 'manual' },
19
20
  { function: 'json', path: '/json', cache: 'off' },
20
21
  ];
21
22
  const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig);
22
23
  expect(declarations).toEqual(expectedDeclarations);
23
24
  });
24
- test("Declarations don't break if no in source config is provided", () => {
25
+ test("Declarations don't break if no in-source config is provided", () => {
25
26
  const tomlConfig = [
26
27
  { function: 'geolocation', path: '/geo', cache: 'off' },
27
28
  { function: 'json', path: '/json', cache: 'manual' },
28
29
  ];
29
30
  const funcConfig = {
30
- geolocation: { path: '/geo-isc', cache: 'manual' },
31
+ geolocation: { path: ['/geo-isc'], cache: 'manual' },
31
32
  json: {},
32
33
  };
33
34
  const expectedDeclarations = [
@@ -37,10 +38,10 @@ test("Declarations don't break if no in source config is provided", () => {
37
38
  const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig);
38
39
  expect(declarations).toEqual(expectedDeclarations);
39
40
  });
40
- test('In source config works independent of the netlify.toml file if a path is defined and otherwise if no path is set', () => {
41
+ test('In-source config works independent of the netlify.toml file if a path is defined and otherwise if no path is set', () => {
41
42
  const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
42
43
  const funcConfigWithPath = {
43
- json: { path: '/json', cache: 'off' },
44
+ json: { path: ['/json', '/json-isc'], cache: 'off' },
44
45
  };
45
46
  const funcConfigWithoutPath = {
46
47
  json: { cache: 'off' },
@@ -48,6 +49,7 @@ test('In source config works independent of the netlify.toml file if a path is d
48
49
  const expectedDeclarationsWithISCPath = [
49
50
  { function: 'geolocation', path: '/geo', cache: 'off' },
50
51
  { function: 'json', path: '/json', cache: 'off' },
52
+ { function: 'json', path: '/json-isc', cache: 'off' },
51
53
  ];
52
54
  const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
53
55
  const declarationsWithISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithPath, deployConfig);
@@ -55,3 +57,38 @@ test('In source config works independent of the netlify.toml file if a path is d
55
57
  const declarationsWithoutISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithoutPath, deployConfig);
56
58
  expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath);
57
59
  });
60
+ test('In-source config works if only the cache config property is set', () => {
61
+ const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
62
+ const funcConfig = {
63
+ geolocation: { cache: 'manual' },
64
+ };
65
+ const expectedDeclarations = [{ function: 'geolocation', path: '/geo', cache: 'manual' }];
66
+ expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations);
67
+ });
68
+ test("In-source config path property works if it's not an array", () => {
69
+ const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
70
+ const funcConfig = {
71
+ json: { path: '/json', cache: 'manual' },
72
+ };
73
+ const expectedDeclarations = [{ function: 'json', path: '/json', cache: 'manual' }];
74
+ expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations);
75
+ });
76
+ test("In-source config path property works if it's not an array and it's not present in toml or deploy config", () => {
77
+ const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
78
+ const funcConfig = {
79
+ json: { path: '/json-isc', cache: 'manual' },
80
+ };
81
+ const expectedDeclarations = [
82
+ { function: 'geolocation', path: '/geo', cache: 'off' },
83
+ { function: 'json', path: '/json-isc', cache: 'manual' },
84
+ ];
85
+ expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations);
86
+ });
87
+ test('In-source config works if path property is an empty array with cache value specified', () => {
88
+ const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
89
+ const funcConfig = {
90
+ json: { path: [], cache: 'manual' },
91
+ };
92
+ const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }];
93
+ expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations);
94
+ });
@@ -1,4 +1,5 @@
1
- import { join } from 'path';
1
+ import { join, relative } from 'path';
2
+ import { virtualRoot } from '../../shared/consts.js';
2
3
  import { BundleFormat } from '../bundle.js';
3
4
  import { wrapBundleError } from '../bundle_error.js';
4
5
  import { wrapNpmImportError } from '../npm_import_error.js';
@@ -8,20 +9,18 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, exte
8
9
  const extension = '.eszip';
9
10
  const destPath = join(distDirectory, `${buildID}${extension}`);
10
11
  const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
12
+ const importMapURL = await createUserImportMap(importMap, basePath, distDirectory);
11
13
  const payload = {
12
14
  basePath,
13
15
  destPath,
14
16
  externals,
15
17
  functions,
16
- importMapURL: importMap.toDataURL(),
18
+ importMapURL,
17
19
  };
18
- const flags = ['--allow-all', '--no-config'];
20
+ const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`];
19
21
  if (!debug) {
20
22
  flags.push('--quiet');
21
23
  }
22
- // To actually vendor the eszip module, we need to supply the import map that
23
- // redirects `https://deno.land/` URLs to the local files.
24
- flags.push(`--import-map=${bundlerImportMap}`);
25
24
  try {
26
25
  await deno.run(['run', ...flags, bundler, JSON.stringify(payload)], { pipeOutput: true });
27
26
  }
@@ -29,7 +28,16 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, exte
29
28
  throw wrapBundleError(wrapNpmImportError(error), { format: 'eszip' });
30
29
  }
31
30
  const hash = await getFileHash(destPath);
32
- return { extension, format: BundleFormat.ESZIP2, hash };
31
+ return { extension, format: BundleFormat.ESZIP2, hash, importMapURL };
32
+ };
33
+ // Takes an import map, writes it to a file on disk, and gets its URL relative
34
+ // to the ESZIP root (i.e. using the virtual root prefix).
35
+ const createUserImportMap = async (importMap, basePath, distDirectory) => {
36
+ const destPath = join(distDirectory, 'import_map.json');
37
+ await importMap.writeToFile(destPath);
38
+ const relativePath = relative(basePath, destPath);
39
+ const importMapURL = new URL(relativePath, virtualRoot);
40
+ return importMapURL.toString();
33
41
  };
34
42
  const getESZIPPaths = () => {
35
43
  const denoPath = join(getPackagePath(), 'deno');
@@ -6,9 +6,12 @@ interface ImportMapFile {
6
6
  declare class ImportMap {
7
7
  files: ImportMapFile[];
8
8
  constructor(files?: ImportMapFile[]);
9
- static resolve(importMapFile: ImportMapFile): import("@import-maps/resolve/types/src/types").ParsedImportMap;
9
+ static resolve(importMapFile: ImportMapFile, rootPath?: string): {
10
+ imports: Record<string, string>;
11
+ scopes?: import("@import-maps/resolve/types/src/types").ParsedScopesMap | undefined;
12
+ };
10
13
  add(file: ImportMapFile): void;
11
- getContents(): string;
14
+ getContents(rootPath?: string): string;
12
15
  toDataURL(): string;
13
16
  writeToFile(path: string): Promise<void>;
14
17
  }
@@ -1,10 +1,10 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { promises as fs } from 'fs';
3
- import { dirname } from 'path';
4
- import { pathToFileURL } from 'url';
3
+ import { dirname, isAbsolute, posix, relative, sep } from 'path';
4
+ import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { parse } from '@import-maps/resolve';
6
6
  const INTERNAL_IMPORTS = {
7
- 'netlify:edge': new URL('https://edge.netlify.com/v1/index.ts'),
7
+ 'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
8
8
  };
9
9
  // ImportMap can take several import map files and merge them into a final
10
10
  // import map object, also adding the internal imports in the right order.
@@ -15,18 +15,40 @@ class ImportMap {
15
15
  this.add(file);
16
16
  });
17
17
  }
18
- static resolve(importMapFile) {
18
+ // Transforms an import map by making any relative paths use a different path
19
+ // as a base.
20
+ static resolve(importMapFile, rootPath) {
19
21
  const { baseURL, ...importMap } = importMapFile;
20
22
  const parsedImportMap = parse(importMap, baseURL);
21
- return parsedImportMap;
23
+ const { imports = {} } = parsedImportMap;
24
+ const newImports = {};
25
+ Object.keys(imports).forEach((specifier) => {
26
+ const url = imports[specifier];
27
+ // If there's no URL, don't even add the specifier to the final imports.
28
+ if (url === null) {
29
+ return;
30
+ }
31
+ // If this is a file URL, we might want to transform it to use another
32
+ // root path, as long as that root path is defined.
33
+ if (url.protocol === 'file:' && rootPath !== undefined) {
34
+ // We want to use POSIX paths for the import map regardless of the OS
35
+ // we're building in.
36
+ const path = relative(rootPath, fileURLToPath(url)).split(sep).join(posix.sep);
37
+ const value = isAbsolute(path) ? path : `.${posix.sep}${path}`;
38
+ newImports[specifier] = value;
39
+ return;
40
+ }
41
+ newImports[specifier] = url.toString();
42
+ });
43
+ return { ...parsedImportMap, imports: newImports };
22
44
  }
23
45
  add(file) {
24
46
  this.files.push(file);
25
47
  }
26
- getContents() {
48
+ getContents(rootPath) {
27
49
  let imports = {};
28
50
  this.files.forEach((file) => {
29
- const importMap = ImportMap.resolve(file);
51
+ const importMap = ImportMap.resolve(file, rootPath);
30
52
  imports = { ...imports, ...importMap.imports };
31
53
  });
32
54
  // Internal imports must come last, because we need to guarantee that
@@ -45,8 +67,9 @@ class ImportMap {
45
67
  return `data:application/json;base64,${encodedImportMap}`;
46
68
  }
47
69
  async writeToFile(path) {
48
- await fs.mkdir(dirname(path), { recursive: true });
49
- const contents = this.getContents();
70
+ const distDirectory = dirname(path);
71
+ await fs.mkdir(distDirectory, { recursive: true });
72
+ const contents = this.getContents(distDirectory);
50
73
  await fs.writeFile(path, contents);
51
74
  }
52
75
  }
@@ -1,14 +1,18 @@
1
+ import { join } from 'path';
2
+ import { cwd } from 'process';
3
+ import { pathToFileURL } from 'url';
1
4
  import { test, expect } from 'vitest';
2
5
  import { ImportMap } from './import_map.js';
3
6
  test('Handles import maps with full URLs without specifying a base URL', () => {
7
+ const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
4
8
  const inputFile1 = {
5
- baseURL: new URL('file:///some/path/import-map.json'),
9
+ baseURL: pathToFileURL(basePath),
6
10
  imports: {
7
11
  'alias:jamstack': 'https://jamstack.org',
8
12
  },
9
13
  };
10
14
  const inputFile2 = {
11
- baseURL: new URL('file:///some/path/import-map.json'),
15
+ baseURL: pathToFileURL(basePath),
12
16
  imports: {
13
17
  'alias:pets': 'https://petsofnetlify.com/',
14
18
  },
@@ -19,15 +23,30 @@ test('Handles import maps with full URLs without specifying a base URL', () => {
19
23
  expect(imports['alias:jamstack']).toBe('https://jamstack.org/');
20
24
  expect(imports['alias:pets']).toBe('https://petsofnetlify.com/');
21
25
  });
22
- test('Handles import maps with relative paths', () => {
26
+ test('Resolves relative paths to absolute paths if a root path is not provided', () => {
27
+ const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
23
28
  const inputFile1 = {
24
- baseURL: new URL('file:///Users/jane-doe/my-site/import-map.json'),
29
+ baseURL: pathToFileURL(basePath),
25
30
  imports: {
26
31
  'alias:pets': './heart/pets/',
27
32
  },
28
33
  };
29
34
  const map = new ImportMap([inputFile1]);
30
35
  const { imports } = JSON.parse(map.getContents());
36
+ const expectedPath = join(cwd(), 'my-cool-site', 'heart', 'pets');
31
37
  expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
32
- expect(imports['alias:pets']).toBe('file:///Users/jane-doe/my-site/heart/pets/');
38
+ expect(imports['alias:pets']).toBe(`${pathToFileURL(expectedPath).toString()}/`);
39
+ });
40
+ test('Transforms relative paths so that they use the root path as a base', () => {
41
+ const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
42
+ const inputFile1 = {
43
+ baseURL: pathToFileURL(basePath),
44
+ imports: {
45
+ 'alias:pets': './heart/pets/',
46
+ },
47
+ };
48
+ const map = new ImportMap([inputFile1]);
49
+ const { imports } = JSON.parse(map.getContents(cwd()));
50
+ expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
51
+ expect(imports['alias:pets']).toBe('./my-cool-site/heart/pets');
33
52
  });
@@ -6,6 +6,7 @@ interface GenerateManifestOptions {
6
6
  bundles?: Bundle[];
7
7
  declarations?: Declaration[];
8
8
  functions: EdgeFunction[];
9
+ importMapURL?: string;
9
10
  layers?: Layer[];
10
11
  }
11
12
  interface Manifest {
@@ -14,6 +15,11 @@ interface Manifest {
14
15
  asset: string;
15
16
  format: string;
16
17
  }[];
18
+ import_map?: string;
19
+ layers: {
20
+ name: string;
21
+ flag: string;
22
+ }[];
17
23
  routes: {
18
24
  function: string;
19
25
  name?: string;
@@ -24,18 +30,15 @@ interface Manifest {
24
30
  name?: string;
25
31
  pattern: string;
26
32
  }[];
27
- layers: {
28
- name: string;
29
- flag: string;
30
- }[];
31
33
  }
32
- declare const generateManifest: ({ bundles, declarations, functions, layers }: GenerateManifestOptions) => Manifest;
34
+ declare const generateManifest: ({ bundles, declarations, functions, importMapURL, layers, }: GenerateManifestOptions) => Manifest;
33
35
  interface WriteManifestOptions {
34
36
  bundles: Bundle[];
35
37
  declarations: Declaration[];
36
38
  distDirectory: string;
37
39
  functions: EdgeFunction[];
40
+ importMapURL?: string;
38
41
  layers?: Layer[];
39
42
  }
40
- declare const writeManifest: ({ bundles, declarations, distDirectory, functions, layers, }: WriteManifestOptions) => Promise<Manifest>;
43
+ declare const writeManifest: ({ bundles, declarations, distDirectory, functions, importMapURL, layers, }: WriteManifestOptions) => Promise<Manifest>;
41
44
  export { generateManifest, Manifest, writeManifest };
@@ -3,7 +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 generateManifest = ({ bundles = [], declarations = [], functions, layers = [] }) => {
6
+ const generateManifest = ({ bundles = [], declarations = [], functions, importMapURL, layers = [], }) => {
7
7
  const preCacheRoutes = [];
8
8
  const postCacheRoutes = [];
9
9
  declarations.forEach((declaration) => {
@@ -35,6 +35,7 @@ const generateManifest = ({ bundles = [], declarations = [], functions, layers =
35
35
  post_cache_routes: postCacheRoutes.filter(nonNullable),
36
36
  bundler_version: getPackageVersion(),
37
37
  layers,
38
+ import_map: importMapURL,
38
39
  };
39
40
  return manifest;
40
41
  };
@@ -51,8 +52,8 @@ const getRegularExpression = (declaration) => {
51
52
  const normalizedSource = `^${regularExpression.source}\\/?$`;
52
53
  return new RegExp(normalizedSource);
53
54
  };
54
- const writeManifest = async ({ bundles, declarations = [], distDirectory, functions, layers, }) => {
55
- const manifest = generateManifest({ bundles, declarations, functions, layers });
55
+ const writeManifest = async ({ bundles, declarations = [], distDirectory, functions, importMapURL, layers, }) => {
56
+ const manifest = generateManifest({ bundles, declarations, functions, importMapURL, layers });
56
57
  const manifestPath = join(distDirectory, 'manifest.json');
57
58
  await fs.writeFile(manifestPath, JSON.stringify(manifest));
58
59
  return manifest;
@@ -0,0 +1 @@
1
+ export declare const virtualRoot = "file:///root/";
@@ -0,0 +1 @@
1
+ export const virtualRoot = 'file:///root/';
@@ -1,3 +1,8 @@
1
1
  declare const testLogger: import("../node/logger.js").Logger;
2
2
  declare const fixturesDir: string;
3
- export { fixturesDir, testLogger };
3
+ declare const useFixture: (fixtureName: string) => Promise<{
4
+ basePath: string;
5
+ cleanup: () => Promise<void>;
6
+ distPath: string;
7
+ }>;
8
+ export { fixturesDir, testLogger, useFixture };
package/dist/test/util.js CHANGED
@@ -1,9 +1,24 @@
1
- import { resolve } from 'path';
1
+ import { promises as fs } from 'fs';
2
+ import { join, resolve } from 'path';
2
3
  import { fileURLToPath } from 'url';
4
+ import cpy from 'cpy';
5
+ import tmp from 'tmp-promise';
3
6
  import { getLogger } from '../node/logger.js';
4
7
  // eslint-disable-next-line @typescript-eslint/no-empty-function
5
8
  const testLogger = getLogger(() => { });
6
9
  const url = new URL(import.meta.url);
7
10
  const dirname = fileURLToPath(url);
8
11
  const fixturesDir = resolve(dirname, '..', 'fixtures');
9
- export { fixturesDir, testLogger };
12
+ const useFixture = async (fixtureName) => {
13
+ const tmpDir = await tmp.dir();
14
+ const cleanup = () => fs.rmdir(tmpDir.path, { recursive: true });
15
+ const fixtureDir = resolve(fixturesDir, fixtureName);
16
+ await cpy(`${fixtureDir}/**`, tmpDir.path);
17
+ const distPath = join(tmpDir.path, '.netlify', 'edge-functions-dist');
18
+ return {
19
+ basePath: tmpDir.path,
20
+ cleanup,
21
+ distPath,
22
+ };
23
+ };
24
+ export { fixturesDir, testLogger, useFixture };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -29,6 +29,7 @@
29
29
  "test:dev": "run-s test:dev:*",
30
30
  "test:ci": "run-s test:ci:*",
31
31
  "test:dev:vitest": "cross-env FORCE_COLOR=0 vitest run",
32
+ "test:dev:vitest:watch": "cross-env FORCE_COLOR=0 vitest watch",
32
33
  "test:dev:deno": "deno test --allow-all deno",
33
34
  "test:ci:vitest": "cross-env FORCE_COLOR=0 vitest run",
34
35
  "test:ci:deno": "deno test --allow-all deno",
@@ -59,6 +60,7 @@
59
60
  "@types/uuid": "^8.3.4",
60
61
  "@vitest/coverage-c8": "^0.25.0",
61
62
  "archiver": "^5.3.1",
63
+ "cpy": "^9.0.1",
62
64
  "cross-env": "^7.0.3",
63
65
  "husky": "^8.0.0",
64
66
  "nock": "^13.2.4",
@@ -0,0 +1 @@
1
+ export const virtualRoot = 'file:///root/'