@netlify/edge-bundler 14.5.6 → 14.7.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.
@@ -0,0 +1,7 @@
1
+ import { loadESZIP } from 'https://deno.land/x/eszip@v0.55.2/eszip.ts'
2
+
3
+ const [functionPath, destPath] = Deno.args
4
+
5
+ const eszip = await loadESZIP(functionPath)
6
+
7
+ await eszip.extract(destPath)
@@ -105,7 +105,7 @@ const stage2Loader = (
105
105
  }
106
106
  }
107
107
 
108
- const writeStage2 = async ({
108
+ export const writeStage2 = async ({
109
109
  basePath,
110
110
  destPath,
111
111
  externals,
@@ -122,5 +122,3 @@ const writeStage2 = async ({
122
122
 
123
123
  return await Deno.writeFile(destPath, bytes)
124
124
  }
125
-
126
- export { writeStage2 }
@@ -0,0 +1,192 @@
1
+ // CLI utility to build/list/extract/run ESZIPs
2
+
3
+ import { build, Parser } from "./mod.ts";
4
+ import { dirname, join } from "https://deno.land/std@0.177.0/path/mod.ts";
5
+
6
+ function hasV2Header(bytes: Uint8Array) {
7
+ const magicV2 = new TextDecoder().decode(bytes.slice(0, 8));
8
+ return magicV2 === "ESZIP_V2";
9
+ }
10
+
11
+ interface ESZIP {
12
+ extract(dest: string): Promise<void>;
13
+ list(): string[];
14
+ }
15
+
16
+ interface V1Entry {
17
+ Source: {
18
+ source: string;
19
+ transpiled: string;
20
+ };
21
+ }
22
+
23
+ class V1 {
24
+ inner: Record<string, V1Entry>;
25
+
26
+ constructor(bytes: Uint8Array) {
27
+ const json = new TextDecoder().decode(bytes);
28
+ const eszip = JSON.parse(json);
29
+ this.inner = eszip;
30
+ }
31
+
32
+ static load(bytes: Uint8Array) {
33
+ return Promise.resolve(new V1(bytes));
34
+ }
35
+
36
+ *entries() {
37
+ for (
38
+ const [
39
+ url,
40
+ {
41
+ Source: { source, transpiled },
42
+ },
43
+ ] of Object.entries(this.inner.modules)
44
+ ) {
45
+ yield { url, source, transpiled };
46
+ }
47
+ }
48
+
49
+ async extract(dest: string) {
50
+ for (const { url, source, transpiled } of this.entries()) {
51
+ await write(join(dest, "source", url2path(url)), source);
52
+ await write(
53
+ join(dest, "transpiled", url2path(url)),
54
+ transpiled ?? source,
55
+ );
56
+ }
57
+ }
58
+
59
+ list() {
60
+ return Array.from(this.entries()).map((e) => e.url);
61
+ }
62
+ }
63
+
64
+ class V2 {
65
+ parser: Parser;
66
+ specifiers: string[];
67
+
68
+ constructor(parser: Parser, specifiers: string[]) {
69
+ this.parser = parser;
70
+ this.specifiers = specifiers;
71
+ }
72
+
73
+ static async load(bytes: Uint8Array) {
74
+ const parser = await Parser.createInstance();
75
+ const specifiers = await parser.parseBytes(bytes);
76
+ await parser.load();
77
+ return new V2(parser, specifiers as string[]);
78
+ }
79
+
80
+ async extract(dest: string) {
81
+ const imports: Record<string, string> = {};
82
+
83
+ for (const specifier of this.specifiers) {
84
+ const module = await this.parser.getModuleSource(specifier);
85
+ await write(join(dest, "source", url2path(specifier)), module);
86
+ // Track import
87
+ imports[specifier] = `./${url2path(specifier)}`;
88
+ }
89
+ // Write import map
90
+ const importMap = JSON.stringify({ imports }, null, 2);
91
+ await Deno.writeTextFile(
92
+ join(dest, "source", "import_map.json"),
93
+ importMap,
94
+ );
95
+ }
96
+
97
+ list() {
98
+ return this.specifiers;
99
+ }
100
+ }
101
+
102
+ export async function loadESZIP(filename: string): Promise<ESZIP> {
103
+ const bytes = await Deno.readFile(filename);
104
+ if (hasV2Header(bytes)) {
105
+ return await V2.load(bytes);
106
+ }
107
+ return await V1.load(bytes);
108
+ }
109
+
110
+ function url2path(urlString: string) {
111
+ const url = new URL(urlString);
112
+ const tail = url.pathname.split("/").filter(Boolean);
113
+ const relativePath = tail.length === 0 ? [".root"] : tail;
114
+ return join(url.hostname, ...relativePath);
115
+ }
116
+
117
+ async function write(path: string, content: string) {
118
+ await Deno.mkdir(dirname(path), { recursive: true });
119
+ await Deno.writeTextFile(path, content);
120
+ }
121
+
122
+ async function run(eszip: ESZIP, specifier: string) {
123
+ // Extract to tmp directory
124
+ const tmpDir = await Deno.makeTempDir({ prefix: "esz" });
125
+ try {
126
+ // Extract
127
+ await eszip.extract(tmpDir);
128
+ const importMap = join(tmpDir, "source", "import_map.json");
129
+ // Run
130
+ const p = new Deno.Command("deno", {
131
+ args: [
132
+ "run",
133
+ "-A",
134
+ "--no-check",
135
+ "--import-map",
136
+ importMap,
137
+ specifier,
138
+ ],
139
+ });
140
+ await p.output();
141
+ } finally {
142
+ // Cleanup
143
+ await Deno.remove(tmpDir, { recursive: true });
144
+ }
145
+ }
146
+
147
+ // Main
148
+ async function main() {
149
+ const args = Deno.args;
150
+ const [subcmd, filename, ...rest] = args;
151
+
152
+ if (subcmd === "help") {
153
+ return console.log("TODO");
154
+ }
155
+
156
+ switch (subcmd) {
157
+ case "build":
158
+ case "b": {
159
+ const eszip = await build([filename]);
160
+ let out = rest[0];
161
+ if (!out) {
162
+ // Create outfile name from url filename
163
+ out = new URL(filename).pathname.split("/").pop() || "out";
164
+ }
165
+ console.log(`${out}.eszip: ${eszip.length} bytes`);
166
+ await Deno.writeFile(`${out}.eszip`, eszip);
167
+ return;
168
+ }
169
+ case "x":
170
+ case "extract": {
171
+ const eszip = await loadESZIP(filename);
172
+ return await eszip.extract(rest[0] ?? Deno.cwd());
173
+ }
174
+ case "l":
175
+ case "ls":
176
+ case "list": {
177
+ const eszip = await loadESZIP(filename);
178
+ return console.log(eszip.list().join("\n"));
179
+ }
180
+ case "r":
181
+ case "run": {
182
+ const eszip = await loadESZIP(filename);
183
+ const specifier = rest[0];
184
+ if (!specifier) {
185
+ return console.error("Please provide a specifier to run");
186
+ }
187
+ return await run(eszip, specifier);
188
+ }
189
+ }
190
+ }
191
+
192
+ await main();
@@ -15,21 +15,28 @@ const DENO_VERSION_FILE = 'version.txt';
15
15
  export const DENO_VERSION_RANGE = '1.39.0 - 2.2.4';
16
16
  const NEXT_DENO_VERSION_RANGE = '^2.4.2';
17
17
  export class DenoBridge {
18
+ cacheDirectory;
19
+ currentDownload;
20
+ debug;
21
+ denoDir;
22
+ logger;
23
+ onAfterDownload;
24
+ onBeforeDownload;
25
+ useGlobal;
26
+ versionRange;
18
27
  constructor(options) {
19
- var _a, _b, _c, _d, _e, _f;
20
- this.cacheDirectory = (_a = options.cacheDirectory) !== null && _a !== void 0 ? _a : getPathInHome('deno-cli');
21
- this.debug = (_b = options.debug) !== null && _b !== void 0 ? _b : false;
28
+ this.cacheDirectory = options.cacheDirectory ?? getPathInHome('deno-cli');
29
+ this.debug = options.debug ?? false;
22
30
  this.denoDir = options.denoDir;
23
- this.logger = (_c = options.logger) !== null && _c !== void 0 ? _c : getLogger(undefined, undefined, options.debug);
31
+ this.logger = options.logger ?? getLogger(undefined, undefined, options.debug);
24
32
  this.onAfterDownload = options.onAfterDownload;
25
33
  this.onBeforeDownload = options.onBeforeDownload;
26
- this.useGlobal = (_d = options.useGlobal) !== null && _d !== void 0 ? _d : true;
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);
34
+ this.useGlobal = options.useGlobal ?? true;
35
+ const useNextDeno = options.featureFlags?.edge_bundler_generate_tarball || options.featureFlags?.edge_bundler_deno_v2;
36
+ this.versionRange = options.versionRange ?? (useNextDeno ? NEXT_DENO_VERSION_RANGE : DENO_VERSION_RANGE);
29
37
  }
30
38
  async downloadBinary() {
31
- var _a, _b, _c;
32
- await ((_a = this.onBeforeDownload) === null || _a === void 0 ? void 0 : _a.call(this));
39
+ await this.onBeforeDownload?.();
33
40
  await this.ensureCacheDirectory();
34
41
  this.logger.system(`Downloading Deno CLI to ${this.cacheDirectory}`);
35
42
  const binaryPath = await download(this.cacheDirectory, this.versionRange, this.logger);
@@ -39,12 +46,12 @@ export class DenoBridge {
39
46
  // that the tests catch it.
40
47
  if (downloadedVersion === undefined) {
41
48
  const error = new Error('There was a problem setting up the Edge Functions environment. To try a manual installation, visit https://ntl.fyi/install-deno.');
42
- await ((_b = this.onAfterDownload) === null || _b === void 0 ? void 0 : _b.call(this, error));
49
+ await this.onAfterDownload?.(error);
43
50
  this.logger.system('Could not run downloaded Deno CLI', error);
44
51
  throw error;
45
52
  }
46
53
  await this.writeVersionFile(downloadedVersion);
47
- await ((_c = this.onAfterDownload) === null || _c === void 0 ? void 0 : _c.call(this));
54
+ await this.onAfterDownload?.();
48
55
  return binaryPath;
49
56
  }
50
57
  async getBinaryVersion(binaryPath) {
@@ -97,19 +104,18 @@ export class DenoBridge {
97
104
  return this.currentDownload;
98
105
  }
99
106
  static runWithBinary(binaryPath, args, { options, pipeOutput, stderr, stdout, }) {
100
- var _a, _b, _c, _d;
101
107
  const runDeno = execa(binaryPath, args, options);
102
108
  if (stderr) {
103
- (_a = runDeno.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
109
+ runDeno.stderr?.pipe(stderr);
104
110
  }
105
111
  else if (pipeOutput) {
106
- (_b = runDeno.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
112
+ runDeno.stderr?.pipe(process.stderr);
107
113
  }
108
114
  if (stdout) {
109
- (_c = runDeno.stdout) === null || _c === void 0 ? void 0 : _c.pipe(stdout);
115
+ runDeno.stdout?.pipe(stdout);
110
116
  }
111
117
  else if (pipeOutput) {
112
- (_d = runDeno.stdout) === null || _d === void 0 ? void 0 : _d.pipe(process.stdout);
118
+ runDeno.stdout?.pipe(process.stdout);
113
119
  }
114
120
  return runDeno;
115
121
  }
@@ -124,14 +130,14 @@ export class DenoBridge {
124
130
  async getBinaryPath(options) {
125
131
  const globalPath = await this.getGlobalBinary();
126
132
  if (globalPath !== undefined) {
127
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
133
+ if (!options?.silent) {
128
134
  this.logger.system('Using global installation of Deno CLI');
129
135
  }
130
136
  return { global: true, path: globalPath };
131
137
  }
132
138
  const cachedPath = await this.getCachedBinary();
133
139
  if (cachedPath !== undefined) {
134
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
140
+ if (!options?.silent) {
135
141
  this.logger.system('Using cached Deno CLI from', cachedPath);
136
142
  }
137
143
  return { global: false, path: cachedPath };
@@ -12,8 +12,7 @@ import { getPlatformTarget } from './platform.js';
12
12
  const require = createRequire(import.meta.url);
13
13
  const archiver = require('archiver');
14
14
  const getMockDenoBridge = function (tmpDir, mockBinaryOutput) {
15
- var _a, _b;
16
- const latestVersion = (_b = (_a = semver.minVersion(DENO_VERSION_RANGE)) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : '';
15
+ const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '';
17
16
  const data = new PassThrough();
18
17
  const archive = archiver('zip', { zlib: { level: 9 } });
19
18
  archive.pipe(data);
@@ -32,7 +31,6 @@ const getMockDenoBridge = function (tmpDir, mockBinaryOutput) {
32
31
  });
33
32
  };
34
33
  test('Does not inherit environment variables if `extendEnv` is false', async () => {
35
- var _a;
36
34
  const tmpDir = await tmp.dir();
37
35
  const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
38
36
 
@@ -46,9 +44,9 @@ test('Does not inherit environment variables if `extendEnv` is false', async ()
46
44
  const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: false });
47
45
  env.TADA = 'TUDU';
48
46
  const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: false });
49
- let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
47
+ let output = result?.stdout ?? '';
50
48
  delete env.TADA;
51
- referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
49
+ referenceOutput?.stdout.split('\n').forEach((line) => {
52
50
  output = output.replace(line.trim(), '');
53
51
  });
54
52
  output = output.trim().replace(/\n+/g, '\n');
@@ -56,7 +54,6 @@ test('Does not inherit environment variables if `extendEnv` is false', async ()
56
54
  await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
57
55
  });
58
56
  test('Does inherit environment variables if `extendEnv` is true', async () => {
59
- var _a;
60
57
  const tmpDir = await tmp.dir();
61
58
  const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
62
59
 
@@ -70,9 +67,9 @@ test('Does inherit environment variables if `extendEnv` is true', async () => {
70
67
  const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
71
68
  env.TADA = 'TUDU';
72
69
  const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: true });
73
- let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
70
+ let output = result?.stdout ?? '';
74
71
  delete env.TADA;
75
- referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
72
+ referenceOutput?.stdout.split('\n').forEach((line) => {
76
73
  output = output.replace(line.trim(), '');
77
74
  });
78
75
  // lets remove holes, split lines and sort lines by name, as different OSes might order them different
@@ -81,7 +78,6 @@ test('Does inherit environment variables if `extendEnv` is true', async () => {
81
78
  await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
82
79
  });
83
80
  test('Does inherit environment variables if `extendEnv` is not set', async () => {
84
- var _a;
85
81
  const tmpDir = await tmp.dir();
86
82
  const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
87
83
 
@@ -95,9 +91,9 @@ test('Does inherit environment variables if `extendEnv` is not set', async () =>
95
91
  const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
96
92
  env.TADA = 'TUDU';
97
93
  const result = await deno.run(['test'], { env: { LULU: 'LALA' } });
98
- let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
94
+ let output = result?.stdout ?? '';
99
95
  delete env.TADA;
100
- referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
96
+ referenceOutput?.stdout.split('\n').forEach((line) => {
101
97
  output = output.replace(line.trim(), '');
102
98
  });
103
99
  // lets remove holes, split lines and sort lines by name, as different OSes might order them different
@@ -1,5 +1,6 @@
1
1
  interface BundleErrorOptions {
2
- format: string;
2
+ cause?: unknown;
3
+ format?: string;
3
4
  }
4
5
  declare const getCustomErrorInfo: (options?: BundleErrorOptions) => {
5
6
  location: {
@@ -8,12 +9,12 @@ declare const getCustomErrorInfo: (options?: BundleErrorOptions) => {
8
9
  };
9
10
  type: string;
10
11
  };
11
- declare class BundleError extends Error {
12
+ export declare class BundleError extends Error {
12
13
  customErrorInfo: ReturnType<typeof getCustomErrorInfo>;
13
14
  constructor(originalError: Error, options?: BundleErrorOptions);
14
15
  }
15
16
  /**
16
17
  * BundleErrors are treated as user-error, so Netlify Team is not alerted about them.
17
18
  */
18
- declare const wrapBundleError: (input: unknown, options?: BundleErrorOptions) => unknown;
19
- export { BundleError, wrapBundleError };
19
+ export declare const wrapBundleError: (input: unknown, options?: BundleErrorOptions) => unknown;
20
+ export {};
@@ -1,13 +1,14 @@
1
1
  const getCustomErrorInfo = (options) => ({
2
2
  location: {
3
- format: options === null || options === void 0 ? void 0 : options.format,
3
+ format: options?.format,
4
4
  runtime: 'deno',
5
5
  },
6
6
  type: 'functionsBundling',
7
7
  });
8
- class BundleError extends Error {
8
+ export class BundleError extends Error {
9
+ customErrorInfo;
9
10
  constructor(originalError, options) {
10
- super(originalError.message);
11
+ super(originalError.message, { cause: options?.cause });
11
12
  this.customErrorInfo = getCustomErrorInfo(options);
12
13
  this.name = 'BundleError';
13
14
  this.stack = originalError.stack;
@@ -18,7 +19,7 @@ class BundleError extends Error {
18
19
  /**
19
20
  * BundleErrors are treated as user-error, so Netlify Team is not alerted about them.
20
21
  */
21
- const wrapBundleError = (input, options) => {
22
+ export const wrapBundleError = (input, options) => {
22
23
  if (input instanceof Error) {
23
24
  if (input.message.includes("The module's source code could not be parsed")) {
24
25
  input.message = input.stderr;
@@ -27,4 +28,3 @@ const wrapBundleError = (input, options) => {
27
28
  }
28
29
  return input;
29
30
  };
30
- export { BundleError, wrapBundleError };
@@ -1,5 +1,5 @@
1
1
  import { promises as fs } from 'fs';
2
- import { join } from 'path';
2
+ import { join, relative } from 'path';
3
3
  import commonPathPrefix from 'common-path-prefix';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import { importMapSpecifier } from '../shared/consts.js';
@@ -9,7 +9,7 @@ import { mergeDeclarations } from './declaration.js';
9
9
  import { load as loadDeployConfig } from './deploy_config.js';
10
10
  import { getFlags } from './feature_flags.js';
11
11
  import { findFunctions } from './finder.js';
12
- import { bundle as bundleESZIP } from './formats/eszip.js';
12
+ import { bundle as bundleESZIP, extension as eszipExtension, extract as extractESZIP } from './formats/eszip.js';
13
13
  import { bundle as bundleTarball } from './formats/tarball.js';
14
14
  import { ImportMap } from './import_map.js';
15
15
  import { getLogger } from './logger.js';
@@ -47,7 +47,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
47
47
  const internalSrcFolders = (Array.isArray(internalSrcFolder) ? internalSrcFolder : [internalSrcFolder]).filter(nonNullable);
48
48
  const userSourceDirectories = sourceDirectories.filter((dir) => !internalSrcFolders.includes(dir));
49
49
  const importMap = new ImportMap();
50
- await importMap.addFiles([deployConfig === null || deployConfig === void 0 ? void 0 : deployConfig.importMap, ...importMapPaths], logger);
50
+ await importMap.addFiles([deployConfig?.importMap, ...importMapPaths], logger);
51
51
  const userFunctions = userSourceDirectories.length === 0 ? [] : await findFunctions(userSourceDirectories);
52
52
  const internalFunctions = internalSrcFolder ? await findFunctions(internalSrcFolders) : [];
53
53
  const functions = [...internalFunctions, ...userFunctions];
@@ -57,7 +57,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
57
57
  functions,
58
58
  importMap,
59
59
  logger,
60
- rootPath: rootPath !== null && rootPath !== void 0 ? rootPath : basePath,
60
+ rootPath: rootPath ?? basePath,
61
61
  vendorDirectory,
62
62
  });
63
63
  const bundles = [];
@@ -71,7 +71,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
71
71
  functions,
72
72
  featureFlags,
73
73
  importMap: importMap.clone(),
74
- vendorDirectory: vendor === null || vendor === void 0 ? void 0 : vendor.directory,
74
+ vendorDirectory: vendor?.directory,
75
75
  }));
76
76
  }
77
77
  if (vendor) {
@@ -87,19 +87,23 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
87
87
  functions,
88
88
  featureFlags,
89
89
  importMap,
90
- vendorDirectory: vendor === null || vendor === void 0 ? void 0 : vendor.directory,
90
+ vendorDirectory: vendor?.directory,
91
91
  }));
92
92
  // The final file name of the bundles contains a SHA256 hash of the contents,
93
93
  // which we can only compute now that the files have been generated. So let's
94
94
  // rename the bundles to their permanent names.
95
- await createFinalBundles(bundles, distDirectory, buildID);
96
- // Retrieving a configuration object for each function.
97
- // Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
98
- const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger })]);
99
- const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger })]);
100
- // Creating a hash of function names to configuration objects.
101
- const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
102
- const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
95
+ const bundlePaths = await createFinalBundles(bundles, distDirectory, buildID);
96
+ const eszipPath = bundlePaths.find((path) => path.endsWith(eszipExtension));
97
+ const { internalFunctions: internalFunctionsWithConfig, userFunctions: userFunctionsWithConfig } = await getFunctionConfigs({
98
+ basePath,
99
+ deno,
100
+ eszipPath,
101
+ featureFlags,
102
+ importMap,
103
+ internalFunctions,
104
+ log: logger,
105
+ userFunctions,
106
+ });
103
107
  // Creating a final declarations array by combining the TOML file with the
104
108
  // deploy configuration API and the in-source configuration.
105
109
  const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations, featureFlags);
@@ -118,19 +122,54 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
118
122
  importMap: importMapSpecifier,
119
123
  layers: deployConfig.layers,
120
124
  });
121
- await (vendor === null || vendor === void 0 ? void 0 : vendor.cleanup());
125
+ await vendor?.cleanup();
122
126
  if (distImportMapPath) {
123
127
  await importMap.writeToFile(distImportMapPath);
124
128
  }
125
129
  return { functions, manifest };
126
130
  };
131
+ const getFunctionConfigs = async ({ basePath, deno, eszipPath, featureFlags, importMap, log, internalFunctions, userFunctions, }) => {
132
+ try {
133
+ const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
134
+ const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
135
+ // Creating a hash of function names to configuration objects.
136
+ const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
137
+ const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
138
+ return {
139
+ internalFunctions: internalFunctionsWithConfig,
140
+ userFunctions: userFunctionsWithConfig,
141
+ };
142
+ }
143
+ catch (err) {
144
+ if (!(err instanceof Error && err.cause === 'IMPORT_ASSERT') || !eszipPath || !featureFlags?.edge_bundler_deno_v2) {
145
+ throw err;
146
+ }
147
+ // We failed to extract the configuration because there is an import assert
148
+ // in the function code, a deprecated feature that we used to support with
149
+ // Deno 1.x. To avoid a breaking change, we treat this error as a special
150
+ // case, using the generated ESZIP to extract the configuration. This works
151
+ // because import asserts are transpiled to import attributes.
152
+ const extractedESZIP = await extractESZIP(deno, eszipPath);
153
+ const configs = await Promise.all([...internalFunctions, ...userFunctions].map(async (func) => {
154
+ const relativePath = relative(basePath, func.path);
155
+ const functionPath = join(extractedESZIP.path, relativePath);
156
+ return [func.name, await getFunctionConfig({ functionPath, importMap, deno, log })];
157
+ }));
158
+ await extractedESZIP.cleanup();
159
+ return {
160
+ internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
161
+ userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
162
+ };
163
+ }
164
+ };
127
165
  const createFinalBundles = async (bundles, distDirectory, buildID) => {
128
166
  const renamingOps = bundles.map(async ({ extension, hash }) => {
129
167
  const tempBundlePath = join(distDirectory, `${buildID}${extension}`);
130
168
  const finalBundlePath = join(distDirectory, `${hash}${extension}`);
131
169
  await fs.rename(tempBundlePath, finalBundlePath);
170
+ return finalBundlePath;
132
171
  });
133
- await Promise.all(renamingOps);
172
+ return await Promise.all(renamingOps);
134
173
  };
135
174
  const getBasePath = (sourceDirectories, inputBasePath) => {
136
175
  // If there's a specific base path supplied, that takes precedence.
@@ -148,11 +187,11 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
148
187
  // declaration level. We want these properties to live at the function level
149
188
  // in their config object, so we translate that for backwards-compatibility.
150
189
  const mergeWithDeclarationConfig = ({ functionName, config, declarations }) => {
151
- const declaration = declarations === null || declarations === void 0 ? void 0 : declarations.find((decl) => decl.function === functionName);
190
+ const declaration = declarations?.find((decl) => decl.function === functionName);
152
191
  return {
153
192
  ...config,
154
- name: (declaration === null || declaration === void 0 ? void 0 : declaration.name) || config.name,
155
- generator: (declaration === null || declaration === void 0 ? void 0 : declaration.generator) || config.generator,
193
+ name: declaration?.name || config.name,
194
+ generator: declaration?.generator || config.generator,
156
195
  };
157
196
  };
158
197
  const addGeneratorFallback = (config) => ({
@@ -3,7 +3,7 @@ import { access, readdir, readFile, rm, writeFile } from 'fs/promises';
3
3
  import { join, resolve } from 'path';
4
4
  import process from 'process';
5
5
  import { pathToFileURL } from 'url';
6
- import { gte, lt } from 'semver';
6
+ import { lt } from 'semver';
7
7
  import * as tar from 'tar';
8
8
  import tmp from 'tmp-promise';
9
9
  import { test, expect, vi, describe } from 'vitest';
@@ -498,23 +498,16 @@ test('Loads JSON modules with `with` attribute', async () => {
498
498
  await cleanup();
499
499
  await rm(vendorDirectory.path, { force: true, recursive: true });
500
500
  });
501
- // We can't run this on versions above 2.0.0 because the bundling will fail
502
- // entirely, and what we're asserting here is that we emit a system log when
503
- // import assertions are detected on successful builds. Also, running it on
504
- // earlier versions won't work either, since those won't even show a warning.
505
- test.skipIf(lt(denoVersion, '1.46.3') || gte(denoVersion, '2.0.0'))('Emits a system log when import assertions are used', async () => {
501
+ test('Emits a system log when import assertions are used', async () => {
506
502
  const { basePath, cleanup, distPath } = await useFixture('with_import_assert');
507
503
  const sourceDirectory = join(basePath, 'functions');
508
- const declarations = [
509
- {
510
- function: 'func1',
511
- path: '/func1',
512
- },
513
- ];
514
504
  const vendorDirectory = await tmp.dir();
515
505
  const systemLogger = vi.fn();
516
- await bundle([sourceDirectory], distPath, declarations, {
506
+ await bundle([sourceDirectory], distPath, [], {
517
507
  basePath,
508
+ featureFlags: {
509
+ edge_bundler_deno_v2: true,
510
+ },
518
511
  systemLogger,
519
512
  vendorDirectory: vendorDirectory.path,
520
513
  });
@@ -524,6 +517,12 @@ test.skipIf(lt(denoVersion, '1.46.3') || gte(denoVersion, '2.0.0'))('Emits a sys
524
517
  const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
525
518
  expect(func1).toBe(`{"foo":"bar"}`);
526
519
  expect(systemLogger).toHaveBeenCalledWith(`Edge function uses import assertions: ${join(sourceDirectory, 'func1.ts')}`);
520
+ expect(manifest.routes[0]).toEqual({
521
+ function: 'func1',
522
+ pattern: '^/with-import-assert/?$',
523
+ excluded_patterns: [],
524
+ path: '/with-import-assert',
525
+ });
527
526
  await cleanup();
528
527
  await rm(vendorDirectory.path, { force: true, recursive: true });
529
528
  });
@@ -656,4 +655,4 @@ describe.skipIf(lt(denoVersion, '2.4.3'))('Produces a tarball bundle', () => {
656
655
  await cleanup();
657
656
  await rm(vendorDirectory.path, { force: true, recursive: true });
658
657
  });
659
- }, 10000);
658
+ }, 10_000);
@@ -1,5 +1,4 @@
1
1
  import { DenoBridge } from './bridge.js';
2
- import { EdgeFunction } from './edge_function.js';
3
2
  import { ImportMap } from './import_map.js';
4
3
  import { Logger } from './logger.js';
5
4
  import { RateLimit } from './rate_limit.js';
@@ -31,10 +30,10 @@ interface FunctionConfigWithPattern extends BaseFunctionConfig {
31
30
  }
32
31
  export type FunctionConfig = FunctionConfigWithPath | FunctionConfigWithPattern;
33
32
  export type FunctionConfigWithAllPossibleFields = BaseFunctionConfig & Partial<FunctionConfigWithPath & FunctionConfigWithPattern>;
34
- export declare const getFunctionConfig: ({ func, importMap, deno, log, }: {
35
- func: EdgeFunction;
36
- importMap: ImportMap;
33
+ export declare const getFunctionConfig: ({ deno, functionPath, importMap, log, }: {
37
34
  deno: DenoBridge;
35
+ functionPath: string;
36
+ importMap: ImportMap;
38
37
  log: Logger;
39
38
  }) => Promise<FunctionConfig>;
40
39
  export {};
@@ -27,7 +27,7 @@ const getConfigExtractor = () => {
27
27
  const configExtractorPath = join(packagePath, 'deno', 'config.ts');
28
28
  return configExtractorPath;
29
29
  };
30
- export const getFunctionConfig = async ({ func, importMap, deno, log, }) => {
30
+ export const getFunctionConfig = async ({ deno, functionPath, importMap, log, }) => {
31
31
  // The extractor is a Deno script that will import the function and run its
32
32
  // `config` export, if one exists.
33
33
  const extractorPath = getConfigExtractor();
@@ -54,15 +54,12 @@ export const getFunctionConfig = async ({ func, importMap, deno, log, }) => {
54
54
  '--node-modules-dir=false',
55
55
  '--quiet',
56
56
  extractorPath,
57
- pathToFileURL(func.path).href,
57
+ pathToFileURL(functionPath).href,
58
58
  pathToFileURL(collector.path).href,
59
59
  JSON.stringify(ConfigExitCode),
60
60
  ].filter(Boolean), { rejectOnExitCode: false });
61
- if (stderr.includes('Import assertions are deprecated')) {
62
- log.system(`Edge function uses import assertions: ${func.path}`);
63
- }
64
61
  if (exitCode !== ConfigExitCode.Success) {
65
- handleConfigError(func, exitCode, stderr, log);
62
+ handleConfigError(functionPath, exitCode, stderr, log);
66
63
  return {};
67
64
  }
68
65
  if (stdout !== '') {
@@ -74,35 +71,38 @@ export const getFunctionConfig = async ({ func, importMap, deno, log, }) => {
74
71
  collectorData = JSON.parse(collectorDataJSON);
75
72
  }
76
73
  catch {
77
- handleConfigError(func, ConfigExitCode.UnhandledError, stderr, log);
74
+ handleConfigError(functionPath, ConfigExitCode.UnhandledError, stderr, log);
78
75
  }
79
76
  finally {
80
77
  await collector.cleanup();
81
78
  }
82
79
  if (!isValidOnError(collectorData.onError)) {
83
- throw new BundleError(new Error(`The 'onError' configuration property in edge function at '${func.path}' must be one of 'fail', 'bypass', or a path starting with '/'. Got '${collectorData.onError}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
80
+ throw new BundleError(new Error(`The 'onError' configuration property in edge function at '${functionPath}' must be one of 'fail', 'bypass', or a path starting with '/'. Got '${collectorData.onError}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
84
81
  }
85
82
  return collectorData;
86
83
  };
87
- const handleConfigError = (func, exitCode, stderr, log) => {
84
+ const handleConfigError = (functionPath, exitCode, stderr, log) => {
85
+ let cause;
86
+ if (stderr.includes('Import assertions are deprecated')) {
87
+ log.system(`Edge function uses import assertions: ${functionPath}`);
88
+ cause = 'IMPORT_ASSERT';
89
+ }
88
90
  switch (exitCode) {
89
91
  case ConfigExitCode.ImportError:
90
92
  log.user(stderr);
91
- throw new BundleError(new Error(`Could not load edge function at '${func.path}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
92
- break;
93
+ throw new BundleError(new Error(`Could not load edge function at '${functionPath}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`), { cause });
93
94
  case ConfigExitCode.NoConfig:
94
- log.system(`No in-source config found for edge function at '${func.path}'`);
95
+ log.system(`No in-source config found for edge function at '${functionPath}'`);
95
96
  break;
96
97
  case ConfigExitCode.InvalidExport:
97
- throw new BundleError(new Error(`The 'config' export in edge function at '${func.path}' must be an object. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
98
- break;
98
+ throw new BundleError(new Error(`The 'config' export in edge function at '${functionPath}' must be an object. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
99
99
  case ConfigExitCode.SerializationError:
100
- throw new BundleError(new Error(`The 'config' object in the edge function at '${func.path}' must contain primitive values only. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
100
+ throw new BundleError(new Error(`The 'config' object in the edge function at '${functionPath}' must contain primitive values only. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
101
101
  break;
102
102
  case ConfigExitCode.InvalidDefaultExport:
103
- throw new BundleError(new Error(`Default export in '${func.path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
103
+ throw new BundleError(new Error(`Default export in '${functionPath}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
104
104
  default:
105
- log.user(`Could not load configuration for edge function at '${func.path}'`);
105
+ log.user(`Could not load configuration for edge function at '${functionPath}'`);
106
106
  log.user(stderr);
107
107
  }
108
108
  };
@@ -172,10 +172,7 @@ describe('`getFunctionConfig` extracts configuration properties from function fi
172
172
  const path = join(tmpDir, `${func.name}.js`);
173
173
  await fs.writeFile(path, func.source);
174
174
  const funcCall = () => getFunctionConfig({
175
- func: {
176
- name: func.name,
177
- path,
178
- },
175
+ functionPath: path,
179
176
  importMap: new ImportMap([importMapFile]),
180
177
  deno,
181
178
  log: logger,
@@ -349,10 +346,7 @@ test('Passes validation if default export exists and is a function', async () =>
349
346
  const path = join(tmpDir, `${func.name}.ts`);
350
347
  await fs.writeFile(path, func.source);
351
348
  await expect(getFunctionConfig({
352
- func: {
353
- name: func.name,
354
- path,
355
- },
349
+ functionPath: path,
356
350
  importMap: new ImportMap([importMapFile]),
357
351
  deno,
358
352
  log: logger,
@@ -378,10 +372,7 @@ test('Fails validation if default export is not function', async () => {
378
372
  const path = join(tmpDir, `${func.name}.ts`);
379
373
  await fs.writeFile(path, func.source);
380
374
  const config = getFunctionConfig({
381
- func: {
382
- name: func.name,
383
- path,
384
- },
375
+ functionPath: path,
385
376
  importMap: new ImportMap([importMapFile]),
386
377
  deno,
387
378
  log: logger,
@@ -407,10 +398,7 @@ test('Fails validation if default export is not present', async () => {
407
398
  const path = join(tmpDir, `${func.name}.ts`);
408
399
  await fs.writeFile(path, func.source);
409
400
  const config = getFunctionConfig({
410
- func: {
411
- name: func.name,
412
- path,
413
- },
401
+ functionPath: path,
414
402
  importMap: new ImportMap([importMapFile]),
415
403
  deno,
416
404
  log: logger,
@@ -16,7 +16,6 @@ export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, interna
16
16
  return declarations;
17
17
  };
18
18
  const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsVisited) => {
19
- var _a, _b;
20
19
  const declarations = [];
21
20
  // For any declaration for which we also have a function configuration object,
22
21
  // we replace the path because that object takes precedence.
@@ -26,7 +25,7 @@ const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsV
26
25
  // If no config is found, add the declaration as is.
27
26
  declarations.push(declaration);
28
27
  }
29
- else if ('pattern' in config && ((_a = config.pattern) === null || _a === void 0 ? void 0 : _a.length)) {
28
+ else if ('pattern' in config && config.pattern?.length) {
30
29
  // If we have a pattern specified as either a string or non-empty array,
31
30
  // create a declaration for each pattern.
32
31
  const patterns = Array.isArray(config.pattern) ? config.pattern : [config.pattern];
@@ -34,7 +33,7 @@ const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsV
34
33
  declarations.push({ ...declaration, cache: config.cache, pattern });
35
34
  });
36
35
  }
37
- else if ('path' in config && ((_b = config.path) === null || _b === void 0 ? void 0 : _b.length)) {
36
+ else if ('path' in config && config.path?.length) {
38
37
  // If we have a path specified as either a string or non-empty array,
39
38
  // create a declaration for each path.
40
39
  const paths = Array.isArray(config.path) ? config.path : [config.path];
@@ -24,13 +24,12 @@ export const load = async (path, logger) => {
24
24
  };
25
25
  };
26
26
  const parse = (data, path) => {
27
- var _a, _b;
28
27
  if (data.version !== 1) {
29
28
  throw new Error(`Unsupported file version: ${data.version}`);
30
29
  }
31
30
  const config = {
32
- declarations: (_a = data.functions) !== null && _a !== void 0 ? _a : [],
33
- layers: (_b = data.layers) !== null && _b !== void 0 ? _b : [],
31
+ declarations: data.functions ?? [],
32
+ layers: data.layers ?? [],
34
33
  };
35
34
  if (data.import_map) {
36
35
  const importMapPath = resolve(dirname(path), data.import_map);
@@ -71,8 +71,7 @@ const getLatestVersion = async () => {
71
71
  }
72
72
  };
73
73
  const getLatestVersionForRange = async (range) => {
74
- var _a;
75
- const minimumVersion = (_a = semver.minVersion(range)) === null || _a === void 0 ? void 0 : _a.version;
74
+ const minimumVersion = semver.minVersion(range)?.version;
76
75
  // We should never get here, because it means that `DENO_VERSION_RANGE` is
77
76
  // a malformed semver range. If this does happen, let's throw an error so
78
77
  // that the tests catch it.
@@ -1,10 +1,12 @@
1
1
  declare const defaultFlags: {
2
2
  edge_bundler_generate_tarball: boolean;
3
+ edge_bundler_deno_v2: boolean;
3
4
  };
4
5
  type FeatureFlag = keyof typeof defaultFlags;
5
6
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
6
7
  declare const getFlags: (input?: Record<string, boolean>, flags?: {
7
8
  edge_bundler_generate_tarball: boolean;
9
+ edge_bundler_deno_v2: boolean;
8
10
  }) => FeatureFlags;
9
11
  export { defaultFlags, getFlags };
10
12
  export type { FeatureFlag, FeatureFlags };
@@ -1,5 +1,6 @@
1
1
  const defaultFlags = {
2
2
  edge_bundler_generate_tarball: false,
3
+ edge_bundler_deno_v2: false,
3
4
  };
4
5
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
5
6
  ...result,
@@ -3,6 +3,7 @@ import { Bundle } from '../bundle.js';
3
3
  import { EdgeFunction } from '../edge_function.js';
4
4
  import { FeatureFlags } from '../feature_flags.js';
5
5
  import { ImportMap } from '../import_map.js';
6
+ export declare const extension = ".eszip";
6
7
  interface BundleESZIPOptions {
7
8
  basePath: string;
8
9
  buildID: string;
@@ -15,5 +16,9 @@ interface BundleESZIPOptions {
15
16
  importMap: ImportMap;
16
17
  vendorDirectory?: string;
17
18
  }
18
- declare const bundleESZIP: ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, vendorDirectory, }: BundleESZIPOptions) => Promise<Bundle>;
19
- export { bundleESZIP as bundle };
19
+ export declare const bundle: ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, vendorDirectory, }: BundleESZIPOptions) => Promise<Bundle>;
20
+ export declare const extract: (deno: DenoBridge, functionPath: string) => Promise<{
21
+ cleanup: () => Promise<void>;
22
+ path: string;
23
+ }>;
24
+ export {};
@@ -1,13 +1,14 @@
1
1
  import { join } from 'path';
2
2
  import { pathToFileURL } from 'url';
3
+ import tmp from 'tmp-promise';
3
4
  import { virtualRoot, virtualVendorRoot } from '../../shared/consts.js';
4
5
  import { BundleFormat } from '../bundle.js';
5
6
  import { wrapBundleError } from '../bundle_error.js';
6
7
  import { wrapNpmImportError } from '../npm_import_error.js';
7
8
  import { getPackagePath } from '../package_json.js';
8
9
  import { getFileHash } from '../utils/sha256.js';
9
- const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, vendorDirectory, }) => {
10
- const extension = '.eszip';
10
+ export const extension = '.eszip';
11
+ export const bundle = async ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, vendorDirectory, }) => {
11
12
  const destPath = join(distDirectory, `${buildID}${extension}`);
12
13
  const importMapPrefixes = {
13
14
  [`${pathToFileURL(basePath)}/`]: virtualRoot,
@@ -44,7 +45,17 @@ const getESZIPPaths = () => {
44
45
  const denoPath = join(getPackagePath(), 'deno');
45
46
  return {
46
47
  bundler: join(denoPath, 'bundle.ts'),
48
+ extractor: join(denoPath, 'extract.ts'),
47
49
  importMap: join(denoPath, 'vendor', 'import_map.json'),
48
50
  };
49
51
  };
50
- export { bundleESZIP as bundle };
52
+ export const extract = async (deno, functionPath) => {
53
+ const tmpDir = await tmp.dir({ unsafeCleanup: true });
54
+ const { extractor, importMap } = getESZIPPaths();
55
+ const flags = ['--allow-all', '--no-config', '--no-lock', `--import-map=${importMap}`, '--quiet'];
56
+ await deno.run(['run', ...flags, extractor, functionPath, tmpDir.path], { pipeOutput: true });
57
+ return {
58
+ cleanup: tmpDir.cleanup,
59
+ path: join(tmpDir.path, 'source', 'root'),
60
+ };
61
+ };
@@ -12,6 +12,11 @@ const INTERNAL_IMPORTS = {
12
12
  // ImportMap can take several import map files and merge them into a final
13
13
  // import map object, also adding the internal imports in the right order.
14
14
  export class ImportMap {
15
+ // The root path which import maps can reference. If an import map attempts
16
+ // to reference a file outside this directory, an error will be thrown.
17
+ rootPath;
18
+ // The different import map files that make up the wider import map.
19
+ sources;
15
20
  constructor(sources = [], rootPath = null) {
16
21
  this.rootPath = rootPath;
17
22
  this.sources = [];
@@ -5,8 +5,8 @@ const getLogger = (systemLogger, userLogger, debug = false) => {
5
5
  // If there is a system logger configured, we'll use that. If there isn't,
6
6
  // we'll pipe system logs to stdout if `debug` is enabled and swallow them
7
7
  // otherwise.
8
- const system = systemLogger !== null && systemLogger !== void 0 ? systemLogger : (debug ? console.log : noopLogger);
9
- const user = userLogger !== null && userLogger !== void 0 ? userLogger : console.log;
8
+ const system = systemLogger ?? (debug ? console.log : noopLogger);
9
+ const user = userLogger ?? console.log;
10
10
  return {
11
11
  system,
12
12
  user,
@@ -12,8 +12,7 @@ import { getPlatformTarget } from './platform.js';
12
12
  const require = createRequire(import.meta.url);
13
13
  const archiver = require('archiver');
14
14
  test('Downloads the Deno CLI on demand and caches it for subsequent calls', async () => {
15
- var _a, _b, _c, _d;
16
- const latestVersion = (_b = (_a = semver.minVersion(DENO_VERSION_RANGE)) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : '';
15
+ const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '';
17
16
  const mockBinaryOutput = `#!/usr/bin/env sh\n\necho "deno ${latestVersion}"`;
18
17
  const data = new PassThrough();
19
18
  const archive = archiver('zip', { zlib: { level: 9 } });
@@ -39,8 +38,8 @@ test('Downloads the Deno CLI on demand and caches it for subsequent calls', asyn
39
38
  const expectedOutput = /^deno [\d.]+/;
40
39
  expect(latestReleaseMock.isDone()).toBe(true);
41
40
  expect(downloadMock.isDone()).toBe(true);
42
- expect((_c = output1 === null || output1 === void 0 ? void 0 : output1.stdout) !== null && _c !== void 0 ? _c : '').toMatch(expectedOutput);
43
- expect((_d = output2 === null || output2 === void 0 ? void 0 : output2.stdout) !== null && _d !== void 0 ? _d : '').toMatch(expectedOutput);
41
+ expect(output1?.stdout ?? '').toMatch(expectedOutput);
42
+ expect(output2?.stdout ?? '').toMatch(expectedOutput);
44
43
  expect(beforeDownload).toHaveBeenCalledTimes(1);
45
44
  expect(afterDownload).toHaveBeenCalledTimes(1);
46
45
  await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
@@ -5,7 +5,7 @@ import { getHeaderMatchers, normalizePattern } from './declaration.js';
5
5
  import { getPackageVersion } from './package_json.js';
6
6
  import { RateLimitAction, RateLimitAlgorithm, RateLimitAggregator } from './rate_limit.js';
7
7
  import { nonNullable } from './utils/non_nullable.js';
8
- import { ExtendedURLPattern } from './utils/urlpattern.js';
8
+ import { getRegexpFromURLPatternPath } from './utils/urlpattern.js';
9
9
  const removeEmptyConfigValues = (functionConfig) => Object.entries(functionConfig).reduce((acc, [key, value]) => {
10
10
  if (value && !(Array.isArray(value) && value.length === 0)) {
11
11
  return { ...acc, [key]: value };
@@ -176,10 +176,9 @@ const pathToRegularExpression = (path) => {
176
176
  return null;
177
177
  }
178
178
  try {
179
- const pattern = new ExtendedURLPattern({ pathname: path });
180
179
  // Removing the `^` and `$` delimiters because we'll need to modify what's
181
180
  // between them.
182
- const source = pattern.regexp.pathname.source.slice(1, -1);
181
+ const source = getRegexpFromURLPatternPath(path).slice(1, -1);
183
182
  // Wrapping the expression source with `^` and `$`. Also, adding an optional
184
183
  // trailing slash, so that a declaration of `path: "/foo"` matches requests
185
184
  // for both `/foo` and `/foo/`.
@@ -70,7 +70,6 @@ test('Generates a manifest with a generator field', () => {
70
70
  expect(manifest.function_config).toEqual(expectedFunctionConfig);
71
71
  });
72
72
  test('Generates a manifest with excluded paths and patterns', () => {
73
- var _a, _b;
74
73
  const functions = [
75
74
  { name: 'func-1', path: '/path/to/func-1.ts' },
76
75
  { name: 'func-2', path: '/path/to/func-2.ts' },
@@ -104,8 +103,8 @@ test('Generates a manifest with excluded paths and patterns', () => {
104
103
  expect(manifest.function_config).toEqual({});
105
104
  expect(manifest.bundler_version).toBe(version);
106
105
  const matcher = getRouteMatcher(manifest);
107
- expect((_a = matcher('/f1/hello')) === null || _a === void 0 ? void 0 : _a.function).toBe('func-1');
108
- expect((_b = matcher('/grandparent/parent/child/grandchild.html')) === null || _b === void 0 ? void 0 : _b.function).toBeUndefined();
106
+ expect(matcher('/f1/hello')?.function).toBe('func-1');
107
+ expect(matcher('/grandparent/parent/child/grandchild.html')?.function).toBeUndefined();
109
108
  });
110
109
  test('TOML-defined paths can be combined with ISC-defined excluded paths', () => {
111
110
  const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
@@ -36,7 +36,7 @@ const getTypePathFromTypesPackage = async (packageName, packageJsonPath) => {
36
36
  return undefined;
37
37
  }
38
38
  const { types, typings } = JSON.parse(await fs.readFile(typesPackagePath, 'utf8'));
39
- const declarationPath = types !== null && types !== void 0 ? types : typings;
39
+ const declarationPath = types ?? typings;
40
40
  if (typeof declarationPath === 'string') {
41
41
  return path.join(typesPackagePath, '..', declarationPath);
42
42
  }
@@ -51,7 +51,7 @@ const getTypesPath = async (packageJsonPath) => {
51
51
  const { name, types, typings } = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
52
52
  // this only looks at `.types` and `.typings` fields. there might also be data in `exports -> . -> types -> import/default`.
53
53
  // we're ignoring that for now.
54
- const declarationPath = types !== null && types !== void 0 ? types : typings;
54
+ const declarationPath = types ?? typings;
55
55
  if (typeof declarationPath === 'string') {
56
56
  return path.join(packageJsonPath, '..', declarationPath);
57
57
  }
@@ -117,7 +117,6 @@ async function parseImportsForFile(file, rootPath) {
117
117
  * to npm modules.
118
118
  */
119
119
  const getNPMSpecifiers = async ({ basePath, functions, importMap, environment, rootPath }, alreadySeenPaths = new Set()) => {
120
- var _a;
121
120
  const baseURL = pathToFileURL(basePath);
122
121
  const npmSpecifiers = [];
123
122
  for (const func of functions) {
@@ -152,7 +151,7 @@ const getNPMSpecifiers = async ({ basePath, functions, importMap, environment, r
152
151
  }
153
152
  const { matched, resolvedImport } = resolve(specifier, importMap, baseURL);
154
153
  if (matched) {
155
- if ((resolvedImport === null || resolvedImport === void 0 ? void 0 : resolvedImport.protocol) === 'file:') {
154
+ if (resolvedImport?.protocol === 'file:') {
156
155
  const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
157
156
  if (alreadySeenPaths.has(newSpecifier)) {
158
157
  break;
@@ -161,7 +160,7 @@ const getNPMSpecifiers = async ({ basePath, functions, importMap, environment, r
161
160
  npmSpecifiers.push(...(await getNPMSpecifiers({ basePath, functions: [newSpecifier], importMap, environment, rootPath }, alreadySeenPaths)));
162
161
  }
163
162
  }
164
- else if (!((_a = resolvedImport === null || resolvedImport === void 0 ? void 0 : resolvedImport.protocol) === null || _a === void 0 ? void 0 : _a.startsWith('http'))) {
163
+ else if (!resolvedImport?.protocol?.startsWith('http')) {
165
164
  const t = await safelyDetectTypes(specifier, basePath);
166
165
  npmSpecifiers.push({
167
166
  specifier: specifier,
@@ -235,8 +234,7 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
235
234
  });
236
235
  outputFiles.push(...outputFilesFromEsBuild.map((file) => file.path));
237
236
  await Promise.all(outputFilesFromEsBuild.map(async (file) => {
238
- var _a;
239
- const types = (_a = ops.find((op) => path.basename(file.path) === path.basename(op.filePath))) === null || _a === void 0 ? void 0 : _a.types;
237
+ const types = ops.find((op) => path.basename(file.path) === path.basename(op.filePath))?.types;
240
238
  let content = file.text;
241
239
  if (types) {
242
240
  content = `/// <reference types="${path.relative(path.dirname(file.path), types)}" />\n${content}`;
@@ -22,8 +22,7 @@ const cleanDirectory = async (directory, except) => {
22
22
  const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImportMapPath, flags: denoFlags, formatExportTypeError, formatImportError, logger, port, rootPath, stderr, stdout, }) => {
23
23
  const processRef = {};
24
24
  const startServer = async (functions, env = {}, options = {}) => {
25
- var _a;
26
- if ((processRef === null || processRef === void 0 ? void 0 : processRef.ps) !== undefined) {
25
+ if (processRef?.ps !== undefined) {
27
26
  await killProcess(processRef.ps);
28
27
  }
29
28
  let graph = {
@@ -41,7 +40,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
41
40
  });
42
41
  const features = {};
43
42
  const importMap = new ImportMap();
44
- await importMap.addFiles((_a = options.importMapPaths) !== null && _a !== void 0 ? _a : [], logger);
43
+ await importMap.addFiles(options.importMapPaths ?? [], logger);
45
44
  // we keep track of the files that are relevant to the user's code, so we can clean up leftovers from past executions later
46
45
  const relevantFiles = [stage2Path];
47
46
  const vendor = await vendorNPMSpecifiers({
@@ -84,7 +83,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
84
83
  });
85
84
  let functionsConfig = [];
86
85
  if (options.getFunctionsConfig) {
87
- functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ func, importMap, deno, log: logger })));
86
+ functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ functionPath: func.path, importMap, deno, log: logger })));
88
87
  }
89
88
  if (distImportMapPath) {
90
89
  await importMap.writeToFile(distImportMapPath);
@@ -52,9 +52,9 @@ test('Starts a server and serves requests for edge functions', async () => {
52
52
  expect(features).toEqual({ npmModules: true });
53
53
  expect(success).toBe(true);
54
54
  expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
55
- const modules = graph === null || graph === void 0 ? void 0 : graph.modules.filter(({ kind, mediaType }) => kind === 'esm' && mediaType === 'TypeScript');
55
+ const modules = graph?.modules.filter(({ kind, mediaType }) => kind === 'esm' && mediaType === 'TypeScript');
56
56
  for (const key in functions) {
57
- const graphEntry = modules === null || modules === void 0 ? void 0 : modules.some(({ local }) => local === functions[key].path);
57
+ const graphEntry = modules?.some(({ local }) => local === functions[key].path);
58
58
  expect(graphEntry).toBe(true);
59
59
  }
60
60
  const response1 = await fetch(`http://0.0.0.0:${port}/foo`, {
@@ -127,7 +127,7 @@ test('Serves edge functions in a monorepo setup', async () => {
127
127
  expect(success).toBe(true);
128
128
  expect(functionsConfig).toEqual([{ path: '/func1' }]);
129
129
  for (const key in functions) {
130
- const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
130
+ const graphEntry = graph?.modules.some(({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
131
131
  expect(graphEntry).toBe(true);
132
132
  }
133
133
  const response1 = await fetch(`http://0.0.0.0:${port}/func1`, {
@@ -10,7 +10,7 @@ const SERVER_POLL_TIMEOUT = 1e4;
10
10
  const isServerReady = async (port, successRef, ps) => {
11
11
  // If the process has been killed or if it exited with an error, we return
12
12
  // early with `success: false`.
13
- if ((ps === null || ps === void 0 ? void 0 : ps.killed) || ((ps === null || ps === void 0 ? void 0 : ps.exitCode) && ps.exitCode > 0)) {
13
+ if (ps?.killed || (ps?.exitCode && ps.exitCode > 0)) {
14
14
  return true;
15
15
  }
16
16
  try {
@@ -24,7 +24,7 @@ const isServerReady = async (port, successRef, ps) => {
24
24
  };
25
25
  const killProcess = (ps) => {
26
26
  // If the process is no longer running, there's nothing left to do.
27
- if ((ps === null || ps === void 0 ? void 0 : ps.exitCode) !== null) {
27
+ if (ps?.exitCode !== null) {
28
28
  return;
29
29
  }
30
30
  return new Promise((resolve, reject) => {
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
2
2
  import { join } from 'path';
3
3
  const TYPES_URL = 'https://edge.netlify.com';
4
4
  const ensureLatestTypes = async (deno, logger, customTypesURL) => {
5
- const typesURL = customTypesURL !== null && customTypesURL !== void 0 ? customTypesURL : TYPES_URL;
5
+ const typesURL = customTypesURL ?? TYPES_URL;
6
6
  // eslint-disable-next-line prefer-const
7
7
  let [localVersion, remoteVersion] = [await getLocalVersion(deno), ''];
8
8
  try {
@@ -1,4 +1 @@
1
- import { URLPattern } from 'urlpattern-polyfill';
2
- export declare class ExtendedURLPattern extends URLPattern {
3
- regexp: Record<string, RegExp>;
4
- }
1
+ export declare const getRegexpFromURLPatternPath: (path: string) => string;
@@ -1,3 +1,5 @@
1
1
  import { URLPattern } from 'urlpattern-polyfill';
2
- export class ExtendedURLPattern extends URLPattern {
3
- }
2
+ export const getRegexpFromURLPatternPath = (path) => {
3
+ const pattern = new URLPattern({ pathname: path });
4
+ return pattern.regexp.pathname.source;
5
+ };
@@ -1,9 +1,9 @@
1
1
  export default class ManifestValidationError extends Error {
2
+ customErrorInfo = {
3
+ type: 'functionsBundling',
4
+ };
2
5
  constructor(message) {
3
6
  super(`Validation of Edge Functions manifest failed\n${message}`);
4
- this.customErrorInfo = {
5
- type: 'functionsBundling',
6
- };
7
7
  this.name = 'ManifestValidationError';
8
8
  // https://github.com/microsoft/TypeScript-wiki/blob/0fecbda7263f130c57394d779b8ca13f0a2e9123/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
9
9
  Object.setPrototypeOf(this, ManifestValidationError.prototype);
package/dist/test/util.js CHANGED
@@ -67,7 +67,6 @@ for (const functionName in manifest.functions) {
67
67
  console.log(JSON.stringify(responses));
68
68
  `;
69
69
  export const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find((route) => {
70
- var _a, _b;
71
70
  const regex = new RegExp(route.pattern);
72
71
  if (!regex.test(candidate)) {
73
72
  return false;
@@ -75,12 +74,11 @@ export const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find
75
74
  if (route.excluded_patterns.some((pattern) => new RegExp(pattern).test(candidate))) {
76
75
  return false;
77
76
  }
78
- const excludedPatterns = (_b = (_a = manifest.function_config[route.function]) === null || _a === void 0 ? void 0 : _a.excluded_patterns) !== null && _b !== void 0 ? _b : [];
77
+ const excludedPatterns = manifest.function_config[route.function]?.excluded_patterns ?? [];
79
78
  const isExcluded = excludedPatterns.some((pattern) => new RegExp(pattern).test(candidate));
80
79
  return !isExcluded;
81
80
  });
82
81
  export const runESZIP = async (eszipPath, vendorDirectory) => {
83
- var _a, _b, _c;
84
82
  const tmpDir = await tmp.dir({ unsafeCleanup: true });
85
83
  // Extract ESZIP into temporary directory.
86
84
  const extractCommand = execa('deno', [
@@ -92,8 +90,8 @@ export const runESZIP = async (eszipPath, vendorDirectory) => {
92
90
  eszipPath,
93
91
  tmpDir.path,
94
92
  ]);
95
- (_a = extractCommand.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
96
- (_b = extractCommand.stdout) === null || _b === void 0 ? void 0 : _b.pipe(stdout);
93
+ extractCommand.stderr?.pipe(stderr);
94
+ extractCommand.stdout?.pipe(stdout);
97
95
  await extractCommand;
98
96
  const virtualRootPath = join(tmpDir.path, 'source', 'root');
99
97
  const stage2Path = join(virtualRootPath, '..', 'bootstrap-stage2');
@@ -109,13 +107,12 @@ export const runESZIP = async (eszipPath, vendorDirectory) => {
109
107
  await fs.rename(stage2Path, `${stage2Path}.js`);
110
108
  // Run function that imports the extracted stage 2 and invokes each function.
111
109
  const evalCommand = execa('deno', ['eval', '--import-map', importMapPath, inspectESZIPFunction(stage2Path)]);
112
- (_c = evalCommand.stderr) === null || _c === void 0 ? void 0 : _c.pipe(stderr);
110
+ evalCommand.stderr?.pipe(stderr);
113
111
  const result = await evalCommand;
114
112
  await tmpDir.cleanup();
115
113
  return JSON.parse(result.stdout);
116
114
  };
117
115
  export const runTarball = async (tarballPath) => {
118
- var _a;
119
116
  const tmpDir = await tmp.dir({ unsafeCleanup: true });
120
117
  await tar.extract({
121
118
  cwd: tmpDir.path,
@@ -124,7 +121,7 @@ export const runTarball = async (tarballPath) => {
124
121
  const evalCommand = execa('deno', ['eval', inspectTarballFunction()], {
125
122
  cwd: tmpDir.path,
126
123
  });
127
- (_a = evalCommand.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
124
+ evalCommand.stderr?.pipe(stderr);
128
125
  const result = await evalCommand;
129
126
  await tmpDir.cleanup();
130
127
  return JSON.parse(result.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "14.5.6",
3
+ "version": "14.7.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -80,5 +80,5 @@
80
80
  "urlpattern-polyfill": "8.0.2",
81
81
  "uuid": "^11.0.0"
82
82
  },
83
- "gitHead": "ce591ef6a6fb974be2a38812a1e0abc98c3c14f6"
83
+ "gitHead": "ff114da3f2bcb5bea83bb4a3747201192aecfec0"
84
84
  }