@netlify/edge-bundler 14.2.2 → 14.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.
@@ -42,7 +42,7 @@ const loadWithRetry = (specifier: string, delay = 1000, maxTry = 3) => {
42
42
  maxTry,
43
43
  });
44
44
  } catch (error) {
45
- if (isTooManyTries(error as Error)) {
45
+ if (error instanceof Error && isTooManyTries(error)) {
46
46
  console.error(`Loading ${specifier} failed after ${maxTry} tries.`);
47
47
  }
48
48
  throw error;
@@ -1,23 +1,26 @@
1
1
  import { type WriteStream } from 'fs';
2
2
  import { type ExecaChildProcess } from 'execa';
3
+ import { FeatureFlags } from './feature_flags.js';
3
4
  import { Logger } from './logger.js';
4
- declare const DENO_VERSION_RANGE = "1.39.0 - 2.2.4";
5
- type OnBeforeDownloadHook = () => void | Promise<void>;
6
- type OnAfterDownloadHook = (error?: Error) => void | Promise<void>;
7
- interface DenoOptions {
5
+ export declare const DENO_VERSION_RANGE = "1.39.0 - 2.2.4";
6
+ export type OnBeforeDownloadHook = () => void | Promise<void>;
7
+ export type OnAfterDownloadHook = (error?: Error) => void | Promise<void>;
8
+ export interface DenoOptions {
8
9
  cacheDirectory?: string;
9
10
  debug?: boolean;
10
11
  denoDir?: string;
12
+ featureFlags?: FeatureFlags;
11
13
  logger?: Logger;
12
14
  onAfterDownload?: OnAfterDownloadHook;
13
15
  onBeforeDownload?: OnBeforeDownloadHook;
14
16
  useGlobal?: boolean;
15
17
  versionRange?: string;
16
18
  }
17
- interface ProcessRef {
19
+ export interface ProcessRef {
18
20
  ps?: ExecaChildProcess<string>;
19
21
  }
20
22
  interface RunOptions {
23
+ cwd?: string;
21
24
  env?: NodeJS.ProcessEnv;
22
25
  extendEnv?: boolean;
23
26
  pipeOutput?: boolean;
@@ -25,7 +28,7 @@ interface RunOptions {
25
28
  stdout?: WriteStream;
26
29
  rejectOnExitCode?: boolean;
27
30
  }
28
- declare class DenoBridge {
31
+ export declare class DenoBridge {
29
32
  cacheDirectory: string;
30
33
  currentDownload?: ReturnType<DenoBridge['downloadBinary']>;
31
34
  debug: boolean;
@@ -51,8 +54,7 @@ declare class DenoBridge {
51
54
  path: string;
52
55
  }>;
53
56
  getEnvironmentVariables(inputEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
54
- run(args: string[], { env: inputEnv, extendEnv, rejectOnExitCode, stderr, stdout }?: RunOptions): Promise<import("execa").ExecaReturnValue<string>>;
57
+ run(args: string[], { cwd, env: inputEnv, extendEnv, rejectOnExitCode, stderr, stdout }?: RunOptions): Promise<import("execa").ExecaReturnValue<string>>;
55
58
  runInBackground(args: string[], ref?: ProcessRef, { env: inputEnv, extendEnv, pipeOutput, stderr, stdout }?: RunOptions): Promise<void>;
56
59
  }
57
- export { DENO_VERSION_RANGE, DenoBridge };
58
- export type { DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook, ProcessRef };
60
+ export {};
@@ -12,10 +12,11 @@ const DENO_VERSION_FILE = 'version.txt';
12
12
  // When updating DENO_VERSION_RANGE, ensure that the deno version
13
13
  // on the netlify/buildbot build image satisfies this range!
14
14
  // https://github.com/netlify/buildbot/blob/f9c03c9dcb091d6570e9d0778381560d469e78ad/build-image/noble/Dockerfile#L410
15
- const DENO_VERSION_RANGE = '1.39.0 - 2.2.4';
16
- class DenoBridge {
15
+ export const DENO_VERSION_RANGE = '1.39.0 - 2.2.4';
16
+ const NEXT_DENO_VERSION_RANGE = '^2.4.2';
17
+ export class DenoBridge {
17
18
  constructor(options) {
18
- var _a, _b, _c, _d, _e;
19
+ var _a, _b, _c, _d, _e, _f;
19
20
  this.cacheDirectory = (_a = options.cacheDirectory) !== null && _a !== void 0 ? _a : getPathInHome('deno-cli');
20
21
  this.debug = (_b = options.debug) !== null && _b !== void 0 ? _b : false;
21
22
  this.denoDir = options.denoDir;
@@ -23,7 +24,8 @@ class DenoBridge {
23
24
  this.onAfterDownload = options.onAfterDownload;
24
25
  this.onBeforeDownload = options.onBeforeDownload;
25
26
  this.useGlobal = (_d = options.useGlobal) !== null && _d !== void 0 ? _d : true;
26
- this.versionRange = (_e = options.versionRange) !== null && _e !== void 0 ? _e : DENO_VERSION_RANGE;
27
+ this.versionRange =
28
+ (_e = options.versionRange) !== null && _e !== void 0 ? _e : (((_f = options.featureFlags) === null || _f === void 0 ? void 0 : _f.edge_bundler_generate_tarball) ? NEXT_DENO_VERSION_RANGE : DENO_VERSION_RANGE);
27
29
  }
28
30
  async downloadBinary() {
29
31
  var _a, _b, _c;
@@ -148,10 +150,10 @@ class DenoBridge {
148
150
  }
149
151
  // Runs the Deno CLI in the background and returns a reference to the child
150
152
  // process, awaiting its execution.
151
- async run(args, { env: inputEnv, extendEnv = true, rejectOnExitCode = true, stderr, stdout } = {}) {
153
+ async run(args, { cwd, env: inputEnv, extendEnv = true, rejectOnExitCode = true, stderr, stdout } = {}) {
152
154
  const { path: binaryPath } = await this.getBinaryPath();
153
155
  const env = this.getEnvironmentVariables(inputEnv);
154
- const options = { env, extendEnv, reject: rejectOnExitCode };
156
+ const options = { cwd, env, extendEnv, reject: rejectOnExitCode };
155
157
  return DenoBridge.runWithBinary(binaryPath, args, { options, stderr, stdout });
156
158
  }
157
159
  // Runs the Deno CLI in the background, assigning a reference of the child
@@ -166,4 +168,3 @@ class DenoBridge {
166
168
  }
167
169
  }
168
170
  }
169
- export { DENO_VERSION_RANGE, DenoBridge };
@@ -1,6 +1,7 @@
1
1
  export declare enum BundleFormat {
2
2
  ESZIP2 = "eszip2",
3
- JS = "js"
3
+ JS = "js",
4
+ TARBALL = "tar"
4
5
  }
5
6
  export interface Bundle {
6
7
  extension: string;
@@ -2,4 +2,5 @@ export var BundleFormat;
2
2
  (function (BundleFormat) {
3
3
  BundleFormat["ESZIP2"] = "eszip2";
4
4
  BundleFormat["JS"] = "js";
5
+ BundleFormat["TARBALL"] = "tar";
5
6
  })(BundleFormat || (BundleFormat = {}));
@@ -10,6 +10,7 @@ import { load as loadDeployConfig } from './deploy_config.js';
10
10
  import { getFlags } from './feature_flags.js';
11
11
  import { findFunctions } from './finder.js';
12
12
  import { bundle as bundleESZIP } from './formats/eszip.js';
13
+ import { bundle as bundleTarball } from './formats/tarball.js';
13
14
  import { ImportMap } from './import_map.js';
14
15
  import { getLogger } from './logger.js';
15
16
  import { writeManifest } from './manifest.js';
@@ -22,6 +23,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
22
23
  const options = {
23
24
  debug,
24
25
  cacheDirectory,
26
+ featureFlags,
25
27
  logger,
26
28
  onAfterDownload,
27
29
  onBeforeDownload,
@@ -58,10 +60,24 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
58
60
  rootPath: rootPath !== null && rootPath !== void 0 ? rootPath : basePath,
59
61
  vendorDirectory,
60
62
  });
63
+ const bundles = [];
64
+ if (featureFlags.edge_bundler_generate_tarball) {
65
+ bundles.push(await bundleTarball({
66
+ basePath,
67
+ buildID,
68
+ debug,
69
+ deno,
70
+ distDirectory,
71
+ functions,
72
+ featureFlags,
73
+ importMap: importMap.clone(),
74
+ vendorDirectory: vendor === null || vendor === void 0 ? void 0 : vendor.directory,
75
+ }));
76
+ }
61
77
  if (vendor) {
62
78
  importMap.add(vendor.importMap);
63
79
  }
64
- const functionBundle = await bundleESZIP({
80
+ bundles.push(await bundleESZIP({
65
81
  basePath,
66
82
  buildID,
67
83
  debug,
@@ -72,11 +88,11 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
72
88
  featureFlags,
73
89
  importMap,
74
90
  vendorDirectory: vendor === null || vendor === void 0 ? void 0 : vendor.directory,
75
- });
91
+ }));
76
92
  // The final file name of the bundles contains a SHA256 hash of the contents,
77
93
  // which we can only compute now that the files have been generated. So let's
78
94
  // rename the bundles to their permanent names.
79
- await createFinalBundles([functionBundle], distDirectory, buildID);
95
+ await createFinalBundles(bundles, distDirectory, buildID);
80
96
  // Retrieving a configuration object for each function.
81
97
  // Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
82
98
  const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger })]);
@@ -92,7 +108,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
92
108
  declarations,
93
109
  });
94
110
  const manifest = await writeManifest({
95
- bundles: [functionBundle],
111
+ bundles,
96
112
  declarations,
97
113
  distDirectory,
98
114
  featureFlags,
@@ -1,12 +1,14 @@
1
1
  import { Buffer } from 'buffer';
2
+ import { execSync } from 'node:child_process';
2
3
  import { access, readdir, readFile, rm, writeFile } from 'fs/promises';
3
4
  import { join, resolve } from 'path';
4
5
  import process from 'process';
5
6
  import { pathToFileURL } from 'url';
7
+ import { lt } from 'semver';
6
8
  import tmp from 'tmp-promise';
7
- import { test, expect, vi } from 'vitest';
9
+ import { test, expect, vi, describe } from 'vitest';
8
10
  import { importMapSpecifier } from '../shared/consts.js';
9
- import { runESZIP, useFixture } from '../test/util.js';
11
+ import { runESZIP, runTarball, useFixture } from '../test/util.js';
10
12
  import { BundleError } from './bundle_error.js';
11
13
  import { bundle } from './bundler.js';
12
14
  import { isFileNotFoundError } from './utils/error.js';
@@ -398,7 +400,7 @@ test('Loads npm modules from bare specifiers', async () => {
398
400
  const manifest = JSON.parse(manifestFile);
399
401
  const bundlePath = join(distPath, manifest.bundles[0].asset);
400
402
  const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
401
- expect(func1).toBe(`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`);
403
+ expect(func1).toBe(`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>, TmV0bGlmeQ==`);
402
404
  await cleanup();
403
405
  await rm(vendorDirectory.path, { force: true, recursive: true });
404
406
  });
@@ -550,3 +552,73 @@ test('Loads edge functions from the Frameworks API', async () => {
550
552
  });
551
553
  await cleanup();
552
554
  });
555
+ const denoVersion = execSync('deno eval --no-lock "console.log(Deno.version.deno)"').toString();
556
+ describe.skipIf(lt(denoVersion, '2.4.2'))('Produces a tarball bundle', () => {
557
+ test('With only local imports', async () => {
558
+ const systemLogger = vi.fn();
559
+ const { basePath, cleanup, distPath } = await useFixture('imports_node_builtin', { copyDirectory: true });
560
+ const declarations = [
561
+ {
562
+ function: 'func1',
563
+ path: '/func1',
564
+ },
565
+ ];
566
+ const vendorDirectory = await tmp.dir();
567
+ await bundle([join(basePath, 'netlify/edge-functions')], distPath, declarations, {
568
+ basePath,
569
+ configPath: join(basePath, '.netlify/edge-functions/config.json'),
570
+ featureFlags: {
571
+ edge_bundler_generate_tarball: true,
572
+ },
573
+ systemLogger,
574
+ });
575
+ expect(systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:')).toBeUndefined();
576
+ const expectedOutput = {
577
+ func1: 'ok',
578
+ };
579
+ const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8');
580
+ const manifest = JSON.parse(manifestFile);
581
+ const tarballPath = join(distPath, manifest.bundles[0].asset);
582
+ const tarballResult = await runTarball(tarballPath);
583
+ expect(tarballResult).toStrictEqual(expectedOutput);
584
+ const eszipPath = join(distPath, manifest.bundles[1].asset);
585
+ const eszipResult = await runESZIP(eszipPath);
586
+ expect(eszipResult).toStrictEqual(expectedOutput);
587
+ await cleanup();
588
+ await rm(vendorDirectory.path, { force: true, recursive: true });
589
+ });
590
+ // TODO: https://github.com/denoland/deno/issues/30187
591
+ test.todo('Using npm modules', async () => {
592
+ const systemLogger = vi.fn();
593
+ const { basePath, cleanup, distPath } = await useFixture('imports_npm_module', { copyDirectory: true });
594
+ const sourceDirectory = join(basePath, 'functions');
595
+ const declarations = [
596
+ {
597
+ function: 'func1',
598
+ path: '/func1',
599
+ },
600
+ ];
601
+ const vendorDirectory = await tmp.dir();
602
+ await bundle([sourceDirectory], distPath, declarations, {
603
+ basePath,
604
+ featureFlags: {
605
+ edge_bundler_generate_tarball: true,
606
+ },
607
+ importMapPaths: [join(basePath, 'import_map.json')],
608
+ vendorDirectory: vendorDirectory.path,
609
+ systemLogger,
610
+ });
611
+ expect(systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:')).toBeUndefined();
612
+ const expectedOutput = `<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>, TmV0bGlmeQ==`;
613
+ const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8');
614
+ const manifest = JSON.parse(manifestFile);
615
+ const tarballPath = join(distPath, manifest.bundles[0].asset);
616
+ const tarballResult = await runTarball(tarballPath);
617
+ expect(tarballResult.func1).toBe(expectedOutput);
618
+ const eszipPath = join(distPath, manifest.bundles[1].asset);
619
+ const eszipResult = await runESZIP(eszipPath, vendorDirectory.path);
620
+ expect(eszipResult.func1).toBe(expectedOutput);
621
+ await cleanup();
622
+ await rm(vendorDirectory.path, { force: true, recursive: true });
623
+ });
624
+ }, 10000);
@@ -1,6 +1,10 @@
1
- declare const defaultFlags: {};
1
+ declare const defaultFlags: {
2
+ edge_bundler_generate_tarball: boolean;
3
+ };
2
4
  type FeatureFlag = keyof typeof defaultFlags;
3
5
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
4
- declare const getFlags: (input?: Record<string, boolean>, flags?: {}) => FeatureFlags;
6
+ declare const getFlags: (input?: Record<string, boolean>, flags?: {
7
+ edge_bundler_generate_tarball: boolean;
8
+ }) => FeatureFlags;
5
9
  export { defaultFlags, getFlags };
6
10
  export type { FeatureFlag, FeatureFlags };
@@ -1,4 +1,6 @@
1
- const defaultFlags = {};
1
+ const defaultFlags = {
2
+ edge_bundler_generate_tarball: false,
3
+ };
2
4
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
3
5
  ...result,
4
6
  [key]: input[key] === undefined ? defaultValue : input[key],
@@ -0,0 +1,18 @@
1
+ import { DenoBridge } from '../bridge.js';
2
+ import { Bundle } from '../bundle.js';
3
+ import { EdgeFunction } from '../edge_function.js';
4
+ import { FeatureFlags } from '../feature_flags.js';
5
+ import { ImportMap } from '../import_map.js';
6
+ interface BundleTarballOptions {
7
+ basePath: string;
8
+ buildID: string;
9
+ debug?: boolean;
10
+ deno: DenoBridge;
11
+ distDirectory: string;
12
+ featureFlags: FeatureFlags;
13
+ functions: EdgeFunction[];
14
+ importMap: ImportMap;
15
+ vendorDirectory?: string;
16
+ }
17
+ export declare const bundle: ({ basePath, buildID, deno, distDirectory, functions, importMap, vendorDirectory, }: BundleTarballOptions) => Promise<Bundle>;
18
+ export {};
@@ -0,0 +1,92 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import commonPathPrefix from 'common-path-prefix';
4
+ import * as tar from 'tar';
5
+ import tmp from 'tmp-promise';
6
+ import { BundleFormat } from '../bundle.js';
7
+ import { getDirectoryHash, getStringHash } from '../utils/sha256.js';
8
+ const TARBALL_EXTENSION = '.tar';
9
+ const getUnixPath = (input) => input.split(path.sep).join('/');
10
+ export const bundle = async ({ basePath, buildID, deno, distDirectory, functions, importMap, vendorDirectory, }) => {
11
+ const sideFilesDir = await tmp.dir({ unsafeCleanup: true });
12
+ const cleanup = [sideFilesDir.cleanup];
13
+ let denoDir = vendorDirectory ? path.join(vendorDirectory, 'deno_dir') : undefined;
14
+ if (!denoDir) {
15
+ const tmpDir = await tmp.dir({ unsafeCleanup: true });
16
+ denoDir = tmpDir.path;
17
+ cleanup.push(tmpDir.cleanup);
18
+ }
19
+ const manifest = {
20
+ functions: {},
21
+ version: 1,
22
+ };
23
+ const entryPoints = functions.map((func) => func.path);
24
+ // `deno bundle` does not return the paths of the files it emits, so we have
25
+ // to infer them. When multiple entry points are supplied, it will find the
26
+ // common path prefix and use that as the base directory in `outdir`. When
27
+ // using a single entry point, `commonPathPrefix` returns an empty string,
28
+ // so we use the path of the first entry point's directory.
29
+ const commonPath = commonPathPrefix(entryPoints) || path.dirname(entryPoints[0]);
30
+ for (const func of functions) {
31
+ const relativePath = path.relative(commonPath, func.path);
32
+ const bundledPath = path.format({
33
+ ...path.parse(relativePath),
34
+ base: undefined,
35
+ ext: '.js',
36
+ });
37
+ manifest.functions[func.name] = getUnixPath(bundledPath);
38
+ }
39
+ await deno.run([
40
+ 'bundle',
41
+ '--import-map',
42
+ importMap.withNodeBuiltins().toDataURL(),
43
+ '--quiet',
44
+ '--code-splitting',
45
+ '--outdir',
46
+ distDirectory,
47
+ ...functions.map((func) => func.path),
48
+ ], {
49
+ cwd: basePath,
50
+ });
51
+ const manifestPath = path.join(sideFilesDir.path, 'manifest.json');
52
+ const manifestContents = JSON.stringify(manifest);
53
+ await fs.writeFile(manifestPath, manifestContents);
54
+ const denoConfigPath = path.join(sideFilesDir.path, 'deno.json');
55
+ const denoConfigContents = JSON.stringify(importMap.getContentsWithRelativePaths());
56
+ await fs.writeFile(denoConfigPath, denoConfigContents);
57
+ const rootLevel = await fs.readdir(distDirectory);
58
+ const hash = await getDirectoryHash(distDirectory);
59
+ const tarballPath = path.join(distDirectory, buildID + TARBALL_EXTENSION);
60
+ await fs.mkdir(path.dirname(tarballPath), { recursive: true });
61
+ // Adding all the bundled files.
62
+ await tar.create({
63
+ cwd: distDirectory,
64
+ file: tarballPath,
65
+ onWriteEntry(entry) {
66
+ entry.path = getUnixPath(`./${entry.path}`);
67
+ },
68
+ }, rootLevel);
69
+ // Adding `deno.json`.
70
+ await tar.update({
71
+ cwd: distDirectory,
72
+ file: tarballPath,
73
+ onWriteEntry(entry) {
74
+ entry.path = './deno.json';
75
+ },
76
+ }, [denoConfigPath]);
77
+ // Adding the manifest file.
78
+ await tar.update({
79
+ cwd: distDirectory,
80
+ file: tarballPath,
81
+ onWriteEntry(entry) {
82
+ entry.path = './___netlify-edge-functions.json';
83
+ },
84
+ }, [manifestPath]);
85
+ await Promise.all(cleanup);
86
+ const finalHash = [hash, getStringHash(manifestContents), getStringHash(denoConfigContents)].join('');
87
+ return {
88
+ extension: TARBALL_EXTENSION,
89
+ format: BundleFormat.TARBALL,
90
+ hash: finalHash,
91
+ };
92
+ };
@@ -24,6 +24,10 @@ export declare class ImportMap {
24
24
  imports: Imports;
25
25
  scopes: {};
26
26
  };
27
+ getContentsWithRelativePaths(): {
28
+ imports: Imports;
29
+ scopes: Record<string, Imports>;
30
+ };
27
31
  getContentsWithURLObjects(prefixes?: Record<string, string>): {
28
32
  imports: Record<string, URL>;
29
33
  scopes: Record<string, Record<string, URL>>;
@@ -34,6 +38,7 @@ export declare class ImportMap {
34
38
  scopes: Record<string, Imports>;
35
39
  };
36
40
  toDataURL(): string;
41
+ withNodeBuiltins(): this;
37
42
  writeToFile(path: string): Promise<void>;
38
43
  }
39
44
  export {};
@@ -1,7 +1,8 @@
1
- import { Buffer } from 'buffer';
2
- import { promises as fs } from 'fs';
3
- import { dirname, relative } from 'path';
4
- import { fileURLToPath, pathToFileURL } from 'url';
1
+ import { Buffer } from 'node:buffer';
2
+ import { promises as fs } from 'node:fs';
3
+ import { builtinModules } from 'node:module';
4
+ import { dirname, relative } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { parse } from '@import-maps/resolve';
6
7
  import { isFileNotFoundError } from './utils/error.js';
7
8
  const INTERNAL_IMPORTS = {
@@ -134,6 +135,24 @@ export class ImportMap {
134
135
  scopes: transformedScopes,
135
136
  };
136
137
  }
138
+ getContentsWithRelativePaths() {
139
+ let imports = {};
140
+ let scopes = {};
141
+ this.sources.forEach((file) => {
142
+ imports = { ...imports, ...file.imports };
143
+ scopes = { ...scopes, ...file.scopes };
144
+ });
145
+ // Internal imports must come last, because we need to guarantee that
146
+ // `netlify:edge` isn't user-defined.
147
+ Object.entries(INTERNAL_IMPORTS).forEach((internalImport) => {
148
+ const [specifier, url] = internalImport;
149
+ imports[specifier] = url;
150
+ });
151
+ return {
152
+ imports,
153
+ scopes,
154
+ };
155
+ }
137
156
  // The same as `getContents`, but the URLs are represented as URL objects
138
157
  // instead of strings. This is compatible with the `ParsedImportMap` type
139
158
  // from the `@import-maps/resolve` library.
@@ -182,6 +201,19 @@ export class ImportMap {
182
201
  const encodedImportMap = Buffer.from(data).toString('base64');
183
202
  return `data:application/json;base64,${encodedImportMap}`;
184
203
  }
204
+ // Adds an import map source mapping Node.js built-in modules to their prefixed
205
+ // version (e.g. "path" => "node:path").
206
+ withNodeBuiltins() {
207
+ const imports = {};
208
+ for (const name of builtinModules) {
209
+ imports[name] = `node:${name}`;
210
+ }
211
+ this.sources.push({
212
+ baseURL: new URL(import.meta.url),
213
+ imports,
214
+ });
215
+ return this;
216
+ }
185
217
  async writeToFile(path) {
186
218
  const distDirectory = dirname(path);
187
219
  await fs.mkdir(distDirectory, { recursive: true });
@@ -8,7 +8,7 @@ import { findUp } from 'find-up';
8
8
  import { parseImports } from 'parse-imports';
9
9
  import tmp from 'tmp-promise';
10
10
  import { pathsBetween } from './utils/fs.js';
11
- const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.cts', '.ctsx', '.mts', '.mtsx']);
11
+ import { TYPESCRIPT_EXTENSIONS } from './utils/typescript.js';
12
12
  const slugifyFileName = (specifier) => {
13
13
  return specifier.replace(/\//g, '_');
14
14
  };
@@ -2,6 +2,11 @@ import { createWriteStream } from 'fs';
2
2
  import { readFile } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import process from 'process';
5
+ // @ts-expect-error TypeScript is complaining about the values for the `module`
6
+ // and `moduleResolution` configuration properties, but changing those to more
7
+ // modern values causes other packages to fail. Leaving this for now, but we
8
+ // should have a proper fix for this.
9
+ import { getURL as getBootstrapURL } from '@netlify/edge-functions-bootstrap/version';
5
10
  import getPort from 'get-port';
6
11
  import tmp from 'tmp-promise';
7
12
  import { v4 as uuidv4 } from 'uuid';
@@ -19,7 +24,7 @@ test('Starts a server and serves requests for edge functions', async () => {
19
24
  const servePath = join(basePath, '.netlify', 'edge-functions-serve');
20
25
  const server = await serve({
21
26
  basePath,
22
- bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
27
+ bootstrapURL: await getBootstrapURL(),
23
28
  port,
24
29
  servePath,
25
30
  });
@@ -99,7 +104,7 @@ test('Serves edge functions in a monorepo setup', async () => {
99
104
  const servePath = join(basePath, '.netlify', 'edge-functions-serve');
100
105
  const server = await serve({
101
106
  basePath,
102
- bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
107
+ bootstrapURL: await getBootstrapURL(),
103
108
  port,
104
109
  rootPath,
105
110
  servePath,
@@ -1,2 +1,3 @@
1
- declare const getFileHash: (path: string) => Promise<string>;
2
- export { getFileHash };
1
+ export declare const getDirectoryHash: (dirPath: string) => Promise<string>;
2
+ export declare const getFileHash: (path: string) => Promise<string>;
3
+ export declare const getStringHash: (input: string) => string;
@@ -1,10 +1,30 @@
1
- import crypto from 'crypto';
2
- import fs from 'fs';
3
- const getFileHash = (path) => {
1
+ import crypto from 'node:crypto';
2
+ import { createReadStream, promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ export const getDirectoryHash = async (dirPath) => {
5
+ const entries = [];
6
+ async function walk(currentPath) {
7
+ const dirents = await fs.readdir(currentPath, { withFileTypes: true });
8
+ for (const dirent of dirents) {
9
+ const fullPath = path.join(currentPath, dirent.name);
10
+ const relativePath = path.relative(dirPath, fullPath);
11
+ if (dirent.isDirectory()) {
12
+ await walk(fullPath);
13
+ }
14
+ else if (dirent.isFile() || dirent.isSymbolicLink()) {
15
+ const fileHash = await getFileHash(fullPath);
16
+ entries.push(`${relativePath}:${fileHash}`);
17
+ }
18
+ }
19
+ }
20
+ await walk(dirPath);
21
+ return getStringHash(entries.sort((a, b) => a.localeCompare(b)).join('\n'));
22
+ };
23
+ export const getFileHash = (path) => {
4
24
  const hash = crypto.createHash('sha256');
5
25
  hash.setEncoding('hex');
6
26
  return new Promise((resolve, reject) => {
7
- const file = fs.createReadStream(path);
27
+ const file = createReadStream(path);
8
28
  file.on('end', () => {
9
29
  hash.end();
10
30
  resolve(hash.read());
@@ -13,4 +33,9 @@ const getFileHash = (path) => {
13
33
  file.pipe(hash);
14
34
  });
15
35
  };
16
- export { getFileHash };
36
+ export const getStringHash = (input) => {
37
+ const hash = crypto.createHash('sha256');
38
+ hash.setEncoding('hex');
39
+ hash.update(input);
40
+ return hash.digest('hex');
41
+ };
@@ -0,0 +1 @@
1
+ export declare const TYPESCRIPT_EXTENSIONS: Set<string>;
@@ -0,0 +1 @@
1
+ export const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.cts', '.ctsx', '.mts', '.mtsx']);
@@ -1,11 +1,19 @@
1
1
  import type { Manifest } from '../node/manifest.js';
2
- declare const testLogger: import("../node/logger.js").Logger;
3
- declare const fixturesDir: string;
4
- declare const useFixture: (fixtureName: string) => Promise<{
2
+ export declare const testLogger: import("../node/logger.js").Logger;
3
+ export declare const fixturesDir: string;
4
+ interface UseFixtureOptions {
5
+ copyDirectory?: boolean;
6
+ }
7
+ export declare const useFixture: (fixtureName: string, { copyDirectory }?: UseFixtureOptions) => Promise<{
8
+ basePath: string;
9
+ cleanup: () => Promise<[PromiseSettledResult<() => Promise<void>>, PromiseSettledResult<() => Promise<void>>]>;
10
+ distPath: string;
11
+ } | {
5
12
  basePath: string;
6
13
  cleanup: () => Promise<void>;
7
14
  distPath: string;
8
15
  }>;
9
- declare const getRouteMatcher: (manifest: Manifest) => (candidate: string) => import("../node/manifest.js").Route | undefined;
10
- declare const runESZIP: (eszipPath: string, vendorDirectory?: string) => Promise<any>;
11
- export { fixturesDir, getRouteMatcher, testLogger, runESZIP, useFixture };
16
+ export declare const getRouteMatcher: (manifest: Manifest) => (candidate: string) => import("../node/manifest.js").Route | undefined;
17
+ export declare const runESZIP: (eszipPath: string, vendorDirectory?: string) => Promise<any>;
18
+ export declare const runTarball: (tarballPath: string) => Promise<any>;
19
+ export {};
package/dist/test/util.js CHANGED
@@ -2,26 +2,38 @@ import { promises as fs } from 'fs';
2
2
  import { join, resolve } from 'path';
3
3
  import { stderr, stdout } from 'process';
4
4
  import { fileURLToPath, pathToFileURL } from 'url';
5
+ import cpy from 'cpy';
5
6
  import { execa } from 'execa';
7
+ import * as tar from 'tar';
6
8
  import tmp from 'tmp-promise';
7
9
  import { getLogger } from '../node/logger.js';
8
- const testLogger = getLogger(() => {
10
+ export const testLogger = getLogger(() => {
9
11
  // no-op
10
12
  });
11
13
  const url = new URL(import.meta.url);
12
14
  const dirname = fileURLToPath(url);
13
- const fixturesDir = resolve(dirname, '..', 'fixtures');
14
- const useFixture = async (fixtureName) => {
15
- const tmpDir = await tmp.dir({ unsafeCleanup: true });
15
+ export const fixturesDir = resolve(dirname, '..', 'fixtures');
16
+ export const useFixture = async (fixtureName, { copyDirectory } = {}) => {
17
+ const tmpDistDir = await tmp.dir({ unsafeCleanup: true });
16
18
  const fixtureDir = resolve(fixturesDir, fixtureName);
17
- const distPath = join(tmpDir.path, '.netlify', 'edge-functions-dist');
19
+ const distPath = join(tmpDistDir.path, '.netlify', 'edge-functions-dist');
20
+ if (copyDirectory) {
21
+ const tmpFixtureDir = await tmp.dir({ unsafeCleanup: true });
22
+ // TODO: Replace with `fs.cp` once the Node.js version range allows.
23
+ await cpy(`${fixtureDir}/**`, tmpFixtureDir.path);
24
+ return {
25
+ basePath: tmpFixtureDir.path,
26
+ cleanup: () => Promise.allSettled([tmpDistDir.cleanup, tmpFixtureDir.cleanup]),
27
+ distPath,
28
+ };
29
+ }
18
30
  return {
19
31
  basePath: fixtureDir,
20
- cleanup: tmpDir.cleanup,
32
+ cleanup: tmpDistDir.cleanup,
21
33
  distPath,
22
34
  };
23
35
  };
24
- const inspectFunction = (path) => `
36
+ const inspectESZIPFunction = (path) => `
25
37
  import { functions } from "${pathToFileURL(path)}.js";
26
38
 
27
39
  const responses = {};
@@ -35,7 +47,25 @@ const inspectFunction = (path) => `
35
47
 
36
48
  console.log(JSON.stringify(responses));
37
49
  `;
38
- const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find((route) => {
50
+ const inspectTarballFunction = () => `
51
+ import path from "node:path";
52
+ import { pathToFileURL } from "node:url";
53
+ import manifest from "./___netlify-edge-functions.json" with { type: "json" };
54
+
55
+ const responses = {};
56
+
57
+ for (const functionName in manifest.functions) {
58
+ const req = new Request("https://test.netlify");
59
+ const entrypoint = path.resolve(manifest.functions[functionName]);
60
+ const func = await import(pathToFileURL(entrypoint))
61
+ const res = await func.default(req);
62
+
63
+ responses[functionName] = await res.text();
64
+ }
65
+
66
+ console.log(JSON.stringify(responses));
67
+ `;
68
+ export const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find((route) => {
39
69
  var _a, _b;
40
70
  const regex = new RegExp(route.pattern);
41
71
  if (!regex.test(candidate)) {
@@ -48,13 +78,14 @@ const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find((route
48
78
  const isExcluded = excludedPatterns.some((pattern) => new RegExp(pattern).test(candidate));
49
79
  return !isExcluded;
50
80
  });
51
- const runESZIP = async (eszipPath, vendorDirectory) => {
81
+ export const runESZIP = async (eszipPath, vendorDirectory) => {
52
82
  var _a, _b, _c;
53
83
  const tmpDir = await tmp.dir({ unsafeCleanup: true });
54
84
  // Extract ESZIP into temporary directory.
55
85
  const extractCommand = execa('deno', [
56
86
  'run',
57
87
  '--allow-all',
88
+ '--no-lock',
58
89
  'https://deno.land/x/eszip@v0.55.2/eszip.ts',
59
90
  'x',
60
91
  eszipPath,
@@ -76,10 +107,24 @@ const runESZIP = async (eszipPath, vendorDirectory) => {
76
107
  }
77
108
  await fs.rename(stage2Path, `${stage2Path}.js`);
78
109
  // Run function that imports the extracted stage 2 and invokes each function.
79
- const evalCommand = execa('deno', ['eval', '--import-map', importMapPath, inspectFunction(stage2Path)]);
110
+ const evalCommand = execa('deno', ['eval', '--import-map', importMapPath, inspectESZIPFunction(stage2Path)]);
80
111
  (_c = evalCommand.stderr) === null || _c === void 0 ? void 0 : _c.pipe(stderr);
81
112
  const result = await evalCommand;
82
113
  await tmpDir.cleanup();
83
114
  return JSON.parse(result.stdout);
84
115
  };
85
- export { fixturesDir, getRouteMatcher, testLogger, runESZIP, useFixture };
116
+ export const runTarball = async (tarballPath) => {
117
+ var _a;
118
+ const tmpDir = await tmp.dir({ unsafeCleanup: true });
119
+ await tar.extract({
120
+ cwd: tmpDir.path,
121
+ file: tarballPath,
122
+ });
123
+ const evalCommand = execa('deno', ['eval', inspectTarballFunction()], {
124
+ cwd: tmpDir.path,
125
+ });
126
+ (_a = evalCommand.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
127
+ const result = await evalCommand;
128
+ await tmpDir.cleanup();
129
+ return JSON.parse(result.stdout);
130
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "14.2.2",
3
+ "version": "14.3.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -42,6 +42,7 @@
42
42
  "test": "test/node"
43
43
  },
44
44
  "devDependencies": {
45
+ "@netlify/edge-functions-bootstrap": "^2.14.0",
45
46
  "@types/node": "^18.19.111",
46
47
  "@types/semver": "^7.3.9",
47
48
  "@types/uuid": "^10.0.0",
@@ -51,7 +52,6 @@
51
52
  "cpy": "^11.1.0",
52
53
  "nock": "^14.0.0",
53
54
  "npm-run-all2": "^6.0.0",
54
- "tar": "^7.0.0",
55
55
  "typescript": "^5.0.0",
56
56
  "vitest": "^3.0.0"
57
57
  },
@@ -77,9 +77,10 @@
77
77
  "parse-imports": "^2.2.1",
78
78
  "path-key": "^4.0.0",
79
79
  "semver": "^7.3.8",
80
+ "tar": "^7.4.3",
80
81
  "tmp-promise": "^3.0.3",
81
82
  "urlpattern-polyfill": "8.0.2",
82
83
  "uuid": "^11.0.0"
83
84
  },
84
- "gitHead": "8b7583e1890636bd64b54e20aee40ae5365edeaf"
85
+ "gitHead": "f65a08178a04db0ad274aa62f7d46319f2ef661a"
85
86
  }