@netlify/edge-bundler 4.4.3 → 5.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/bundle.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { writeStage2 } from './lib/stage2.ts'
2
2
 
3
3
  const [payload] = Deno.args
4
- const { basePath, destPath, functions, importMapURL } = JSON.parse(payload)
4
+ const { basePath, destPath, externals, functions, importMapURL } = JSON.parse(payload)
5
5
 
6
- await writeStage2({ basePath, destPath, functions, importMapURL })
6
+ await writeStage2({ basePath, destPath, externals, functions, importMapURL })
@@ -1,4 +1,3 @@
1
- export const CUSTOM_LAYER_PREFIX = 'layer:'
2
1
  export const PUBLIC_SPECIFIER = 'netlify:edge'
3
2
  export const STAGE1_SPECIFIER = 'netlify:bootstrap-stage1'
4
3
  export const STAGE2_SPECIFIER = 'netlify:bootstrap-stage2'
@@ -3,7 +3,7 @@ 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 { CUSTOM_LAYER_PREFIX, PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
6
+ import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
7
7
  import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
8
8
 
9
9
  interface FunctionReference {
@@ -62,7 +62,7 @@ const getVirtualPath = (basePath: string, filePath: string) => {
62
62
  return url
63
63
  }
64
64
 
65
- const stage2Loader = (basePath: string, functions: InputFunction[]) => {
65
+ const stage2Loader = (basePath: string, functions: InputFunction[], externals: Set<string>) => {
66
66
  return async (specifier: string): Promise<LoadResponse | undefined> => {
67
67
  if (specifier === STAGE2_SPECIFIER) {
68
68
  const stage2Entry = getStage2Entry(basePath, functions)
@@ -70,7 +70,7 @@ const stage2Loader = (basePath: string, functions: InputFunction[]) => {
70
70
  return inlineModule(specifier, stage2Entry)
71
71
  }
72
72
 
73
- if (specifier === PUBLIC_SPECIFIER || specifier.startsWith(CUSTOM_LAYER_PREFIX)) {
73
+ if (specifier === PUBLIC_SPECIFIER || externals.has(specifier)) {
74
74
  return {
75
75
  kind: 'external',
76
76
  specifier,
@@ -85,8 +85,8 @@ const stage2Loader = (basePath: string, functions: InputFunction[]) => {
85
85
  }
86
86
  }
87
87
 
88
- const writeStage2 = async ({ basePath, destPath, functions, importMapURL }: WriteStage2Options) => {
89
- const loader = stage2Loader(basePath, functions)
88
+ const writeStage2 = async ({ basePath, destPath, externals, functions, importMapURL }: WriteStage2Options) => {
89
+ const loader = stage2Loader(basePath, functions, new Set(externals))
90
90
  const bytes = await build([STAGE2_SPECIFIER], loader, importMapURL)
91
91
  const directory = path.dirname(destPath)
92
92
 
@@ -1,24 +1,20 @@
1
1
  import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
2
2
  import { Declaration } from './declaration.js';
3
- import { EdgeFunction } from './edge_function.js';
4
3
  import { FeatureFlags } from './feature_flags.js';
5
- import { ImportMapFile } from './import_map.js';
6
- import { Layer } from './layer.js';
7
4
  import { LogFunction } from './logger.js';
8
5
  interface BundleOptions {
9
6
  basePath?: string;
10
7
  cacheDirectory?: string;
8
+ configPath?: string;
11
9
  debug?: boolean;
12
10
  distImportMapPath?: string;
13
11
  featureFlags?: FeatureFlags;
14
- importMaps?: ImportMapFile[];
15
- layers?: Layer[];
16
12
  onAfterDownload?: OnAfterDownloadHook;
17
13
  onBeforeDownload?: OnBeforeDownloadHook;
18
14
  systemLogger?: LogFunction;
19
15
  }
20
- declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, layers, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
21
- functions: EdgeFunction[];
16
+ declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
17
+ functions: import("./edge_function.js").EdgeFunction[];
22
18
  manifest: import("./manifest.js").Manifest;
23
19
  }>;
24
20
  export { bundle };
@@ -5,37 +5,15 @@ import { v4 as uuidv4 } from 'uuid';
5
5
  import { DenoBridge } from './bridge.js';
6
6
  import { getFunctionConfig } from './config.js';
7
7
  import { getDeclarationsFromConfig } from './declaration.js';
8
+ import { load as loadDeployConfig } from './deploy_config.js';
8
9
  import { getFlags } from './feature_flags.js';
9
10
  import { findFunctions } from './finder.js';
10
11
  import { bundle as bundleESZIP } from './formats/eszip.js';
11
- import { bundle as bundleJS } from './formats/javascript.js';
12
12
  import { ImportMap } from './import_map.js';
13
13
  import { getLogger } from './logger.js';
14
14
  import { writeManifest } from './manifest.js';
15
15
  import { ensureLatestTypes } from './types.js';
16
- const createBundle = ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, featureFlags, }) => {
17
- if (featureFlags.edge_functions_produce_eszip) {
18
- return bundleESZIP({
19
- basePath,
20
- buildID,
21
- debug,
22
- deno,
23
- distDirectory,
24
- featureFlags,
25
- functions,
26
- importMap,
27
- });
28
- }
29
- return bundleJS({
30
- buildID,
31
- debug,
32
- deno,
33
- distDirectory,
34
- functions,
35
- importMap,
36
- });
37
- };
38
- const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, layers, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
16
+ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
39
17
  const logger = getLogger(systemLogger, debug);
40
18
  const featureFlags = getFlags(inputFeatureFlags);
41
19
  const options = {
@@ -55,19 +33,27 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
55
33
  // compute until we run the bundle process. For now, we'll use a random ID
56
34
  // to create the bundle artifacts and rename them later.
57
35
  const buildID = uuidv4();
58
- // Creating an ImportMap instance with any import maps supplied by the user,
59
- // if any.
60
- const importMap = new ImportMap(importMaps);
36
+ // Loading any configuration options from the deploy configuration API, if it
37
+ // exists.
38
+ const deployConfig = await loadDeployConfig(configPath, logger);
39
+ // Layers are marked as externals in the ESZIP, so that those specifiers are
40
+ // not actually included in the bundle.
41
+ const externals = deployConfig.layers.map((layer) => layer.name);
42
+ const importMap = new ImportMap();
43
+ if (deployConfig.importMap) {
44
+ importMap.add(deployConfig.importMap);
45
+ }
61
46
  const functions = await findFunctions(sourceDirectories);
62
- const functionBundle = await createBundle({
47
+ const functionBundle = await bundleESZIP({
63
48
  basePath,
64
49
  buildID,
65
50
  debug,
66
51
  deno,
67
52
  distDirectory,
53
+ externals,
68
54
  functions,
69
- importMap,
70
55
  featureFlags,
56
+ importMap,
71
57
  });
72
58
  // The final file name of the bundles contains a SHA256 hash of the contents,
73
59
  // which we can only compute now that the files have been generated. So let's
@@ -82,15 +68,15 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
82
68
  }));
83
69
  // Creating a hash of function names to configuration objects.
84
70
  const functionsWithConfig = functions.reduce((acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }), {});
85
- // Creating a final declarations array by combining the TOML entries with the
86
- // function configuration objects.
87
- const declarations = getDeclarationsFromConfig(tomlDeclarations, functionsWithConfig);
71
+ // Creating a final declarations array by combining the TOML file with the
72
+ // deploy configuration API and the in-source configuration.
73
+ const declarations = getDeclarationsFromConfig(tomlDeclarations, functionsWithConfig, deployConfig);
88
74
  const manifest = await writeManifest({
89
75
  bundles: [functionBundle],
90
76
  declarations,
91
77
  distDirectory,
92
78
  functions,
93
- layers,
79
+ layers: deployConfig.layers,
94
80
  });
95
81
  if (distImportMapPath) {
96
82
  await importMap.writeToFile(distImportMapPath);
@@ -1,14 +1,14 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { join, resolve } from 'path';
3
3
  import process from 'process';
4
- import { pathToFileURL } from 'url';
5
4
  import { deleteAsync } from 'del';
6
5
  import tmp from 'tmp-promise';
7
6
  import { test, expect } from 'vitest';
8
7
  import { fixturesDir } from '../test/util.js';
9
8
  import { BundleError } from './bundle_error.js';
10
9
  import { bundle } from './bundler.js';
11
- test('Produces a JavaScript bundle and a manifest file', async () => {
10
+ import { isNodeError } from './utils/error.js';
11
+ test('Produces an ESZIP bundle', async () => {
12
12
  const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
13
13
  const tmpDir = await tmp.dir();
14
14
  const declarations = [
@@ -19,49 +19,7 @@ test('Produces a JavaScript bundle and a manifest file', async () => {
19
19
  ];
20
20
  const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
21
21
  basePath: fixturesDir,
22
- importMaps: [
23
- {
24
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
25
- imports: {
26
- 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
27
- },
28
- },
29
- ],
30
- });
31
- const generatedFiles = await fs.readdir(tmpDir.path);
32
- expect(result.functions.length).toBe(1);
33
- expect(generatedFiles.length).toBe(2);
34
- const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
35
- const manifest = JSON.parse(manifestFile);
36
- const { bundles } = manifest;
37
- expect(bundles.length).toBe(1);
38
- expect(bundles[0].format).toBe('js');
39
- expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
40
- expect(result.manifest).toEqual(manifest);
41
- await fs.rmdir(tmpDir.path, { recursive: true });
42
- });
43
- test('Produces only a ESZIP bundle when the `edge_functions_produce_eszip` feature flag is set', async () => {
44
- const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
45
- const tmpDir = await tmp.dir();
46
- const declarations = [
47
- {
48
- function: 'func1',
49
- path: '/func1',
50
- },
51
- ];
52
- const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
53
- basePath: fixturesDir,
54
- featureFlags: {
55
- edge_functions_produce_eszip: true,
56
- },
57
- importMaps: [
58
- {
59
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
60
- imports: {
61
- 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
62
- },
63
- },
64
- ],
22
+ configPath: join(sourceDirectory, 'config.json'),
65
23
  });
66
24
  const generatedFiles = await fs.readdir(tmpDir.path);
67
25
  expect(result.functions.length).toBe(1);
@@ -85,17 +43,7 @@ test('Uses the vendored eszip module instead of fetching it from deno.land', asy
85
43
  ];
86
44
  const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
87
45
  basePath: fixturesDir,
88
- featureFlags: {
89
- edge_functions_produce_eszip: true,
90
- },
91
- importMaps: [
92
- {
93
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
94
- imports: {
95
- 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
96
- },
97
- },
98
- ],
46
+ configPath: join(sourceDirectory, 'config.json'),
99
47
  });
100
48
  const generatedFiles = await fs.readdir(tmpDir.path);
101
49
  expect(result.functions.length).toBe(1);
@@ -125,7 +73,7 @@ test('Adds a custom error property to user errors during bundling', async () =>
125
73
  expect(error).toBeInstanceOf(BundleError);
126
74
  expect(error.customErrorInfo).toEqual({
127
75
  location: {
128
- format: 'javascript',
76
+ format: 'eszip',
129
77
  runtime: 'deno',
130
78
  },
131
79
  type: 'functionsBundling',
@@ -143,11 +91,7 @@ test('Prints a nice error message when user tries importing NPM module', async (
143
91
  },
144
92
  ];
145
93
  try {
146
- await bundle([sourceDirectory], tmpDir.path, declarations, {
147
- featureFlags: {
148
- edge_functions_produce_eszip: true,
149
- },
150
- });
94
+ await bundle([sourceDirectory], tmpDir.path, declarations);
151
95
  }
152
96
  catch (error) {
153
97
  expect(error).toBeInstanceOf(BundleError);
@@ -165,7 +109,7 @@ test('Does not add a custom error property to system errors during bundling', as
165
109
  }
166
110
  });
167
111
  test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_cache_deno_dir` feature flag is set', async () => {
168
- expect.assertions(7);
112
+ expect.assertions(6);
169
113
  const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
170
114
  const outDir = await tmp.dir();
171
115
  const cacheDir = await tmp.dir();
@@ -178,14 +122,7 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
178
122
  const options = {
179
123
  basePath: fixturesDir,
180
124
  cacheDirectory: cacheDir.path,
181
- importMaps: [
182
- {
183
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
184
- imports: {
185
- 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
186
- },
187
- },
188
- ],
125
+ configPath: join(sourceDirectory, 'config.json'),
189
126
  };
190
127
  // Run #1, feature flag off: The directory should not be populated.
191
128
  const result1 = await bundle([sourceDirectory], outDir.path, declarations, options);
@@ -209,7 +146,6 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
209
146
  expect(result2.functions.length).toBe(1);
210
147
  expect(outFiles2.length).toBe(2);
211
148
  const denoDir2 = await fs.readdir(join(cacheDir.path, 'deno_dir'));
212
- expect(denoDir2.includes('deps')).toBe(true);
213
149
  expect(denoDir2.includes('gen')).toBe(true);
214
150
  await fs.rmdir(outDir.path, { recursive: true });
215
151
  });
@@ -224,17 +160,7 @@ test('Supports import maps with relative paths', async () => {
224
160
  ];
225
161
  const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
226
162
  basePath: fixturesDir,
227
- featureFlags: {
228
- edge_functions_produce_eszip: true,
229
- },
230
- importMaps: [
231
- {
232
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
233
- imports: {
234
- 'alias:helper': './helper.ts',
235
- },
236
- },
237
- ],
163
+ configPath: join(sourceDirectory, 'config.json'),
238
164
  });
239
165
  const generatedFiles = await fs.readdir(tmpDir.path);
240
166
  expect(result.functions.length).toBe(1);
@@ -278,25 +204,14 @@ test('Ignores any user-defined `deno.json` files', async () => {
278
204
  throw new Error(`The file at '${denoConfigPath} would be overwritten by this test. Please move the file to a different location and try again.'`);
279
205
  }
280
206
  catch (error) {
281
- // @ts-expect-error Error is not typed
282
- if (error.code !== 'ENOENT') {
207
+ if (isNodeError(error) && error.code !== 'ENOENT') {
283
208
  throw error;
284
209
  }
285
210
  }
286
211
  await fs.writeFile(denoConfigPath, JSON.stringify(denoConfig));
287
212
  expect(() => bundle([join(fixtureDir, 'functions')], tmpDir.path, declarations, {
288
213
  basePath: fixturesDir,
289
- featureFlags: {
290
- edge_functions_produce_eszip: true,
291
- },
292
- importMaps: [
293
- {
294
- baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
295
- imports: {
296
- 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
297
- },
298
- },
299
- ],
214
+ configPath: join(fixtureDir, 'functions', 'config.json'),
300
215
  })).not.toThrow();
301
216
  await deleteAsync([tmpDir.path, denoConfigPath, importMapFile.path], { force: true });
302
217
  });
@@ -309,13 +224,10 @@ test('Processes a function that imports a custom layer', async () => {
309
224
  path: '/func1',
310
225
  },
311
226
  ];
312
- const layer = { name: 'test', flag: 'edge-functions-layer-test' };
227
+ const layer = { name: 'layer:test', flag: 'edge-functions-layer-test' };
313
228
  const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
314
229
  basePath: fixturesDir,
315
- featureFlags: {
316
- edge_functions_produce_eszip: true,
317
- },
318
- layers: [layer],
230
+ configPath: join(sourceDirectory, 'config.json'),
319
231
  });
320
232
  const generatedFiles = await fs.readdir(tmpDir.path);
321
233
  expect(result.functions.length).toBe(1);
@@ -329,3 +241,28 @@ test('Processes a function that imports a custom layer', async () => {
329
241
  expect(layers).toEqual([layer]);
330
242
  await fs.rmdir(tmpDir.path, { recursive: true });
331
243
  });
244
+ 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();
247
+ const declarations = [
248
+ {
249
+ function: 'func1',
250
+ path: '/func1',
251
+ },
252
+ ];
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'),
257
+ });
258
+ const generatedFiles = await fs.readdir(tmpDir.path);
259
+ expect(result.functions.length).toBe(2);
260
+ expect(generatedFiles.length).toBe(2);
261
+ const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
262
+ const manifest = JSON.parse(manifestFile);
263
+ const { bundles } = manifest;
264
+ expect(bundles.length).toBe(1);
265
+ expect(bundles[0].format).toBe('eszip2');
266
+ expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
267
+ await fs.rmdir(tmpDir.path, { recursive: true });
268
+ });
@@ -127,10 +127,7 @@ test('Ignores function paths from the in-source `config` function if the feature
127
127
  const declarations = [];
128
128
  const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
129
129
  basePath: fixturesDir,
130
- featureFlags: {
131
- edge_functions_produce_eszip: true,
132
- },
133
- importMaps: [importMapFile],
130
+ configPath: join(internalDirectory, 'config.json'),
134
131
  });
135
132
  const generatedFiles = await fs.readdir(tmpDir.path);
136
133
  expect(result.functions.length).toBe(6);
@@ -160,11 +157,10 @@ test('Loads function paths from the in-source `config` function', async () => {
160
157
  ];
161
158
  const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
162
159
  basePath: fixturesDir,
160
+ configPath: join(internalDirectory, 'config.json'),
163
161
  featureFlags: {
164
162
  edge_functions_config_export: true,
165
- edge_functions_produce_eszip: true,
166
163
  },
167
- importMaps: [importMapFile],
168
164
  });
169
165
  const generatedFiles = await fs.readdir(tmpDir.path);
170
166
  expect(result.functions.length).toBe(6);
@@ -1,4 +1,5 @@
1
1
  import { FunctionConfig } from './config.js';
2
+ import type { DeployConfig } from './deploy_config.js';
2
3
  interface BaseDeclaration {
3
4
  cache?: string;
4
5
  function: string;
@@ -11,5 +12,5 @@ type DeclarationWithPattern = BaseDeclaration & {
11
12
  pattern: string;
12
13
  };
13
14
  type Declaration = DeclarationWithPath | DeclarationWithPattern;
14
- export declare const getDeclarationsFromConfig: (tomlDeclarations: Declaration[], functionsConfig: Record<string, FunctionConfig>) => Declaration[];
15
+ export declare const getDeclarationsFromConfig: (tomlDeclarations: Declaration[], functionsConfig: Record<string, FunctionConfig>, deployConfig: DeployConfig) => Declaration[];
15
16
  export { Declaration, DeclarationWithPath, DeclarationWithPattern };
@@ -1,12 +1,12 @@
1
- export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig) => {
1
+ export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig, deployConfig) => {
2
2
  var _a;
3
3
  const declarations = [];
4
4
  const functionsVisited = new Set();
5
- // We start by iterating over all the TOML declarations. For any declaration
6
- // for which we also have a function configuration object, we replace the
7
- // defined config (currently path or cache or both) because that object takes
8
- // precedence.
9
- for (const declaration of tomlDeclarations) {
5
+ // We start by iterating over all the declarations in the TOML file and in
6
+ // the deploy configuration file. For any declaration for which we also have
7
+ // a function configuration object, we replace the path because that object
8
+ // takes precedence.
9
+ for (const declaration of [...tomlDeclarations, ...deployConfig.declarations]) {
10
10
  const config = (_a = functionsConfig[declaration.function]) !== null && _a !== void 0 ? _a : {};
11
11
  functionsVisited.add(declaration.function);
12
12
  declarations.push({ ...declaration, ...config });
@@ -1,5 +1,10 @@
1
1
  import { test, expect } from 'vitest';
2
2
  import { getDeclarationsFromConfig } from './declaration.js';
3
+ // TODO: Add tests with the deploy config.
4
+ const deployConfig = {
5
+ declarations: [],
6
+ layers: [],
7
+ };
3
8
  test('In source config takes precedence over netlify.toml config', () => {
4
9
  const tomlConfig = [
5
10
  { function: 'geolocation', path: '/geo', cache: 'off' },
@@ -13,7 +18,7 @@ test('In source config takes precedence over netlify.toml config', () => {
13
18
  { function: 'geolocation', path: '/geo-isc', cache: 'manual' },
14
19
  { function: 'json', path: '/json', cache: 'off' },
15
20
  ];
16
- const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig);
21
+ const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig);
17
22
  expect(declarations).toEqual(expectedDeclarations);
18
23
  });
19
24
  test("Declarations don't break if no in source config is provided", () => {
@@ -29,7 +34,7 @@ test("Declarations don't break if no in source config is provided", () => {
29
34
  { function: 'geolocation', path: '/geo-isc', cache: 'manual' },
30
35
  { function: 'json', path: '/json', cache: 'manual' },
31
36
  ];
32
- const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig);
37
+ const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig);
33
38
  expect(declarations).toEqual(expectedDeclarations);
34
39
  });
35
40
  test('In source config works independent of the netlify.toml file if a path is defined and otherwise if no path is set', () => {
@@ -45,8 +50,8 @@ test('In source config works independent of the netlify.toml file if a path is d
45
50
  { function: 'json', path: '/json', cache: 'off' },
46
51
  ];
47
52
  const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
48
- const declarationsWithISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithPath);
49
- const declarationsWithoutISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithoutPath);
53
+ const declarationsWithISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithPath, deployConfig);
50
54
  expect(declarationsWithISCPath).toEqual(expectedDeclarationsWithISCPath);
55
+ const declarationsWithoutISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithoutPath, deployConfig);
51
56
  expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath);
52
57
  });
@@ -0,0 +1,10 @@
1
+ import type { Declaration } from './declaration.js';
2
+ import { ImportMapFile } from './import_map.js';
3
+ import type { Layer } from './layer.js';
4
+ import type { Logger } from './logger.js';
5
+ export interface DeployConfig {
6
+ declarations: Declaration[];
7
+ importMap?: ImportMapFile;
8
+ layers: Layer[];
9
+ }
10
+ export declare const load: (path: string | undefined, logger: Logger) => Promise<DeployConfig>;
@@ -0,0 +1,45 @@
1
+ import { promises as fs } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { readFile as readImportMap } from './import_map.js';
4
+ import { isNodeError } from './utils/error.js';
5
+ export const load = async (path, logger) => {
6
+ if (path === undefined) {
7
+ return {
8
+ declarations: [],
9
+ layers: [],
10
+ };
11
+ }
12
+ try {
13
+ const data = await fs.readFile(path, 'utf8');
14
+ const config = JSON.parse(data);
15
+ return parse(config, path);
16
+ }
17
+ catch (error) {
18
+ if (isNodeError(error) && error.code !== 'ENOENT') {
19
+ logger.system('Error while parsing internal edge functions manifest:', error);
20
+ }
21
+ }
22
+ return {
23
+ declarations: [],
24
+ layers: [],
25
+ };
26
+ };
27
+ const parse = async (data, path) => {
28
+ var _a, _b;
29
+ if (data.version !== 1) {
30
+ throw new Error(`Unsupported file version: ${data.version}`);
31
+ }
32
+ const config = {
33
+ declarations: (_a = data.functions) !== null && _a !== void 0 ? _a : [],
34
+ layers: (_b = data.layers) !== null && _b !== void 0 ? _b : [],
35
+ };
36
+ if (data.import_map) {
37
+ const importMapPath = resolve(dirname(path), data.import_map);
38
+ const importMap = await readImportMap(importMapPath);
39
+ return {
40
+ ...config,
41
+ importMap,
42
+ };
43
+ }
44
+ return config;
45
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { cwd } from 'process';
4
+ import { pathToFileURL } from 'url';
5
+ import tmp from 'tmp-promise';
6
+ import { test, expect } from 'vitest';
7
+ import { load } from './deploy_config.js';
8
+ import { getLogger } from './logger.js';
9
+ const logger = getLogger(console.log);
10
+ test('Returns an empty config object if there is no file at the given path', async () => {
11
+ const mockPath = join(cwd(), 'some-directory', `a-file-that-does-not-exist-${Date.now()}.json`);
12
+ const config = await load(mockPath, logger);
13
+ expect(config.declarations).toEqual([]);
14
+ expect(config.layers).toEqual([]);
15
+ });
16
+ test('Returns a config object with declarations, layers, and import map', async () => {
17
+ var _a, _b;
18
+ const importMapFile = await tmp.file();
19
+ const importMap = {
20
+ imports: {
21
+ 'https://deno.land/': 'https://black.hole/',
22
+ },
23
+ };
24
+ await fs.writeFile(importMapFile.path, JSON.stringify(importMap));
25
+ const configFile = await tmp.file();
26
+ const config = {
27
+ functions: [
28
+ {
29
+ function: 'func1',
30
+ path: '/func1',
31
+ },
32
+ ],
33
+ layers: [
34
+ {
35
+ name: 'layer1',
36
+ flag: 'edge_functions_layer1_url',
37
+ local: 'https://some-url.netlify.app/mod.ts',
38
+ },
39
+ ],
40
+ import_map: importMapFile.path,
41
+ version: 1,
42
+ };
43
+ await fs.writeFile(configFile.path, JSON.stringify(config));
44
+ const parsedConfig = await load(configFile.path, logger);
45
+ expect(parsedConfig.declarations).toEqual(config.functions);
46
+ expect(parsedConfig.layers).toEqual(config.layers);
47
+ expect(parsedConfig.importMap).toBeTruthy();
48
+ expect((_a = parsedConfig.importMap) === null || _a === void 0 ? void 0 : _a.baseURL).toEqual(pathToFileURL(importMapFile.path));
49
+ expect((_b = parsedConfig.importMap) === null || _b === void 0 ? void 0 : _b.imports).toEqual(importMap.imports);
50
+ });
@@ -1,7 +1,6 @@
1
1
  const defaultFlags = {
2
2
  edge_functions_cache_deno_dir: false,
3
3
  edge_functions_config_export: false,
4
- edge_functions_produce_eszip: false,
5
4
  };
6
5
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
7
6
  ...result,
@@ -9,9 +9,10 @@ interface BundleESZIPOptions {
9
9
  debug?: boolean;
10
10
  deno: DenoBridge;
11
11
  distDirectory: string;
12
+ externals: string[];
12
13
  featureFlags: FeatureFlags;
13
14
  functions: EdgeFunction[];
14
15
  importMap: ImportMap;
15
16
  }
16
- declare const bundleESZIP: ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, }: BundleESZIPOptions) => Promise<Bundle>;
17
+ declare const bundleESZIP: ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, }: BundleESZIPOptions) => Promise<Bundle>;
17
18
  export { bundleESZIP as bundle };
@@ -4,13 +4,14 @@ import { wrapBundleError } from '../bundle_error.js';
4
4
  import { wrapNpmImportError } from '../npm_import_error.js';
5
5
  import { getPackagePath } from '../package_json.js';
6
6
  import { getFileHash } from '../utils/sha256.js';
7
- const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, }) => {
7
+ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, }) => {
8
8
  const extension = '.eszip';
9
9
  const destPath = join(distDirectory, `${buildID}${extension}`);
10
10
  const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
11
11
  const payload = {
12
12
  basePath,
13
13
  destPath,
14
+ externals,
14
15
  functions,
15
16
  importMapURL: importMap.toDataURL(),
16
17
  };
@@ -1,30 +1,17 @@
1
- import { DenoBridge } from '../bridge.js';
2
- import { Bundle } from '../bundle.js';
3
1
  import { EdgeFunction } from '../edge_function.js';
4
- import { ImportMap } from '../import_map.js';
5
2
  import type { FormatFunction } from '../server/server.js';
6
- interface BundleJSOptions {
7
- buildID: string;
8
- debug?: boolean;
9
- deno: DenoBridge;
10
- distDirectory: string;
11
- functions: EdgeFunction[];
12
- importMap: ImportMap;
13
- }
14
- declare const bundleJS: ({ buildID, debug, deno, distDirectory, functions, importMap, }: BundleJSOptions) => Promise<Bundle>;
15
3
  interface GenerateStage2Options {
16
4
  distDirectory: string;
17
5
  fileName: string;
18
6
  formatExportTypeError?: FormatFunction;
19
7
  formatImportError?: FormatFunction;
20
8
  functions: EdgeFunction[];
21
- type?: 'local' | 'production';
22
9
  }
23
- declare const generateStage2: ({ distDirectory, fileName, formatExportTypeError, formatImportError, functions, type, }: GenerateStage2Options) => Promise<string>;
10
+ declare const generateStage2: ({ distDirectory, fileName, formatExportTypeError, formatImportError, functions, }: GenerateStage2Options) => Promise<string>;
24
11
  declare const getBootstrapURL: () => string;
25
12
  interface GetLocalEntryPointOptions {
26
13
  formatExportTypeError?: FormatFunction;
27
14
  formatImportError?: FormatFunction;
28
15
  }
29
16
  declare const getLocalEntryPoint: (functions: EdgeFunction[], { formatExportTypeError, formatImportError, }: GetLocalEntryPointOptions) => string;
30
- export { bundleJS as bundle, generateStage2, getBootstrapURL, getLocalEntryPoint };
17
+ export { generateStage2, getBootstrapURL, getLocalEntryPoint };
@@ -3,37 +3,13 @@ import { join } from 'path';
3
3
  import { env } from 'process';
4
4
  import { pathToFileURL } from 'url';
5
5
  import { deleteAsync } from 'del';
6
- import { BundleFormat } from '../bundle.js';
7
- import { wrapBundleError } from '../bundle_error.js';
8
- import { wrapNpmImportError } from '../npm_import_error.js';
9
- import { getFileHash } from '../utils/sha256.js';
10
6
  const BOOTSTRAP_LATEST = 'https://637cf7ce9214b300099b3aa8--edge.netlify.com/bootstrap/index-combined.ts';
11
- const bundleJS = async ({ buildID, debug, deno, distDirectory, functions, importMap, }) => {
12
- const stage2Path = await generateStage2({ distDirectory, functions, fileName: `${buildID}-pre.js` });
13
- const extension = '.js';
14
- const jsBundlePath = join(distDirectory, `${buildID}${extension}`);
15
- const flags = [`--import-map=${importMap.toDataURL()}`, '--no-config'];
16
- if (!debug) {
17
- flags.push('--quiet');
18
- }
19
- try {
20
- await deno.run(['bundle', ...flags, stage2Path, jsBundlePath], { pipeOutput: true });
21
- }
22
- catch (error) {
23
- throw wrapBundleError(wrapNpmImportError(error), { format: 'javascript' });
24
- }
25
- await fs.unlink(stage2Path);
26
- const hash = await getFileHash(jsBundlePath);
27
- return { extension, format: BundleFormat.JS, hash };
28
- };
29
7
  const defaultFormatExportTypeError = (name) => `The Edge Function "${name}" has failed to load. Does it have a function as the default export?`;
30
8
  const defaultFormatImpoortError = (name) => `There was an error with Edge Function "${name}".`;
31
- const generateStage2 = async ({ distDirectory, fileName, formatExportTypeError, formatImportError, functions, type = 'production', }) => {
9
+ const generateStage2 = async ({ distDirectory, fileName, formatExportTypeError, formatImportError, functions, }) => {
32
10
  await deleteAsync(distDirectory, { force: true });
33
11
  await fs.mkdir(distDirectory, { recursive: true });
34
- const entryPoint = type === 'local'
35
- ? getLocalEntryPoint(functions, { formatExportTypeError, formatImportError })
36
- : getProductionEntryPoint(functions);
12
+ const entryPoint = getLocalEntryPoint(functions, { formatExportTypeError, formatImportError });
37
13
  const stage2Path = join(distDirectory, fileName);
38
14
  await fs.writeFile(stage2Path, entryPoint);
39
15
  return stage2Path;
@@ -69,21 +45,4 @@ const getLocalEntryPoint = (functions, { formatExportTypeError = defaultFormatEx
69
45
  const bootCall = `boot(functions, metadata);`;
70
46
  return [bootImport, declaration, ...imports, bootCall].join('\n\n');
71
47
  };
72
- const getProductionEntryPoint = (functions) => {
73
- const bootImport = `import { boot } from "${getBootstrapURL()}";`;
74
- const lines = functions.map((func, index) => {
75
- const importName = `func${index}`;
76
- const exportLine = `"${func.name}": ${importName}`;
77
- const url = pathToFileURL(func.path);
78
- return {
79
- exportLine,
80
- importLine: `import ${importName} from "${url}";`,
81
- };
82
- });
83
- const importLines = lines.map(({ importLine }) => importLine).join('\n');
84
- const exportLines = lines.map(({ exportLine }) => exportLine).join(', ');
85
- const exportDeclaration = `const functions = {${exportLines}};`;
86
- const defaultExport = 'boot(functions);';
87
- return [bootImport, importLines, exportDeclaration, defaultExport].join('\n\n');
88
- };
89
- export { bundleJS as bundle, generateStage2, getBootstrapURL, getLocalEntryPoint };
48
+ export { generateStage2, getBootstrapURL, getLocalEntryPoint };
@@ -4,12 +4,14 @@ interface ImportMapFile {
4
4
  scopes?: Record<string, Record<string, string>>;
5
5
  }
6
6
  declare class ImportMap {
7
- imports: Record<string, URL | null>;
7
+ files: ImportMapFile[];
8
8
  constructor(files?: ImportMapFile[]);
9
9
  static resolve(importMapFile: ImportMapFile): import("@import-maps/resolve/types/src/types").ParsedImportMap;
10
+ add(file: ImportMapFile): void;
10
11
  getContents(): string;
11
12
  toDataURL(): string;
12
13
  writeToFile(path: string): Promise<void>;
13
14
  }
14
- export { ImportMap };
15
+ declare const readFile: (path: string) => Promise<ImportMapFile>;
16
+ export { ImportMap, readFile };
15
17
  export type { ImportMapFile };
@@ -1,14 +1,31 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { promises as fs } from 'fs';
3
3
  import { dirname } from 'path';
4
+ import { pathToFileURL } from 'url';
4
5
  import { parse } from '@import-maps/resolve';
5
6
  const INTERNAL_IMPORTS = {
6
7
  'netlify:edge': new URL('https://edge.netlify.com/v1/index.ts'),
7
8
  };
9
+ // ImportMap can take several import map files and merge them into a final
10
+ // import map object, also adding the internal imports in the right order.
8
11
  class ImportMap {
9
12
  constructor(files = []) {
10
- let imports = {};
13
+ this.files = [];
11
14
  files.forEach((file) => {
15
+ this.add(file);
16
+ });
17
+ }
18
+ static resolve(importMapFile) {
19
+ const { baseURL, ...importMap } = importMapFile;
20
+ const parsedImportMap = parse(importMap, baseURL);
21
+ return parsedImportMap;
22
+ }
23
+ add(file) {
24
+ this.files.push(file);
25
+ }
26
+ getContents() {
27
+ let imports = {};
28
+ this.files.forEach((file) => {
12
29
  const importMap = ImportMap.resolve(file);
13
30
  imports = { ...imports, ...importMap.imports };
14
31
  });
@@ -18,16 +35,8 @@ class ImportMap {
18
35
  const [specifier, url] = internalImport;
19
36
  imports[specifier] = url;
20
37
  });
21
- this.imports = imports;
22
- }
23
- static resolve(importMapFile) {
24
- const { baseURL, ...importMap } = importMapFile;
25
- const parsedImportMap = parse(importMap, baseURL);
26
- return parsedImportMap;
27
- }
28
- getContents() {
29
38
  const contents = {
30
- imports: this.imports,
39
+ imports,
31
40
  };
32
41
  return JSON.stringify(contents);
33
42
  }
@@ -41,4 +50,22 @@ class ImportMap {
41
50
  await fs.writeFile(path, contents);
42
51
  }
43
52
  }
44
- export { ImportMap };
53
+ const readFile = async (path) => {
54
+ const baseURL = pathToFileURL(path);
55
+ try {
56
+ const data = await fs.readFile(path, 'utf8');
57
+ const importMap = JSON.parse(data);
58
+ return {
59
+ ...importMap,
60
+ baseURL,
61
+ };
62
+ }
63
+ catch {
64
+ // no-op
65
+ }
66
+ return {
67
+ baseURL,
68
+ imports: {},
69
+ };
70
+ };
71
+ export { ImportMap, readFile };
@@ -19,7 +19,6 @@ const prepareServer = ({ deno, distDirectory, flags: denoFlags, formatExportType
19
19
  functions,
20
20
  formatExportTypeError,
21
21
  formatImportError,
22
- type: 'local',
23
22
  });
24
23
  try {
25
24
  // This command will print a JSON object with all the modules found in
@@ -71,7 +70,7 @@ const serve = async ({ certificatePath, debug, distImportMapPath, inspectSetting
71
70
  await ensureLatestTypes(deno, logger);
72
71
  // Creating an ImportMap instance with any import maps supplied by the user,
73
72
  // if any.
74
- const importMap = new ImportMap(importMaps);
73
+ const importMap = new ImportMap(importMaps !== null && importMaps !== void 0 ? importMaps : []);
75
74
  const flags = ['--allow-all', '--unstable', `--import-map=${importMap.toDataURL()}`, '--no-config'];
76
75
  if (certificatePath) {
77
76
  flags.push(`--cert=${certificatePath}`);
@@ -0,0 +1,2 @@
1
+ /// <reference types="node" />
2
+ export declare const isNodeError: (error: any) => error is NodeJS.ErrnoException;
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export const isNodeError = (error) => error instanceof Error;
@@ -5,6 +5,7 @@ export interface InputFunction {
5
5
  export interface WriteStage2Options {
6
6
  basePath: string;
7
7
  destPath: string;
8
+ externals: string[];
8
9
  functions: InputFunction[];
9
10
  importMapURL?: string;
10
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "4.4.3",
3
+ "version": "5.0.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
package/shared/stage2.ts CHANGED
@@ -6,6 +6,7 @@ export interface InputFunction {
6
6
  export interface WriteStage2Options {
7
7
  basePath: string
8
8
  destPath: string
9
+ externals: string[]
9
10
  functions: InputFunction[]
10
11
  importMapURL?: string
11
12
  }