@netlify/edge-bundler 14.5.5 → 14.6.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,189 @@
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(url: string) {
111
+ return join(...(new URL(url).pathname.split("/").filter(Boolean)));
112
+ }
113
+
114
+ async function write(path: string, content: string) {
115
+ await Deno.mkdir(dirname(path), { recursive: true });
116
+ await Deno.writeTextFile(path, content);
117
+ }
118
+
119
+ async function run(eszip: ESZIP, specifier: string) {
120
+ // Extract to tmp directory
121
+ const tmpDir = await Deno.makeTempDir({ prefix: "esz" });
122
+ try {
123
+ // Extract
124
+ await eszip.extract(tmpDir);
125
+ const importMap = join(tmpDir, "source", "import_map.json");
126
+ // Run
127
+ const p = new Deno.Command("deno", {
128
+ args: [
129
+ "run",
130
+ "-A",
131
+ "--no-check",
132
+ "--import-map",
133
+ importMap,
134
+ specifier,
135
+ ],
136
+ });
137
+ await p.output();
138
+ } finally {
139
+ // Cleanup
140
+ await Deno.remove(tmpDir, { recursive: true });
141
+ }
142
+ }
143
+
144
+ // Main
145
+ async function main() {
146
+ const args = Deno.args;
147
+ const [subcmd, filename, ...rest] = args;
148
+
149
+ if (subcmd === "help") {
150
+ return console.log("TODO");
151
+ }
152
+
153
+ switch (subcmd) {
154
+ case "build":
155
+ case "b": {
156
+ const eszip = await build([filename]);
157
+ let out = rest[0];
158
+ if (!out) {
159
+ // Create outfile name from url filename
160
+ out = new URL(filename).pathname.split("/").pop() || "out";
161
+ }
162
+ console.log(`${out}.eszip: ${eszip.length} bytes`);
163
+ await Deno.writeFile(`${out}.eszip`, eszip);
164
+ return;
165
+ }
166
+ case "x":
167
+ case "extract": {
168
+ const eszip = await loadESZIP(filename);
169
+ return await eszip.extract(rest[0] ?? Deno.cwd());
170
+ }
171
+ case "l":
172
+ case "ls":
173
+ case "list": {
174
+ const eszip = await loadESZIP(filename);
175
+ return console.log(eszip.list().join("\n"));
176
+ }
177
+ case "r":
178
+ case "run": {
179
+ const eszip = await loadESZIP(filename);
180
+ const specifier = rest[0];
181
+ if (!specifier) {
182
+ return console.error("Please provide a specifier to run");
183
+ }
184
+ return await run(eszip, specifier);
185
+ }
186
+ }
187
+ }
188
+
189
+ await main();
@@ -2,7 +2,7 @@ import { type WriteStream } from 'fs';
2
2
  import { type ExecaChildProcess } from 'execa';
3
3
  import { FeatureFlags } from './feature_flags.js';
4
4
  import { Logger } from './logger.js';
5
- export declare const DENO_VERSION_RANGE = "1.39.0 - 2.2.4";
5
+ export declare const DENO_VERSION_RANGE = "^2.4.2";
6
6
  export type OnBeforeDownloadHook = () => void | Promise<void>;
7
7
  export type OnAfterDownloadHook = (error?: Error) => void | Promise<void>;
8
8
  export interface DenoOptions {
@@ -12,24 +12,29 @@ 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
- export const DENO_VERSION_RANGE = '1.39.0 - 2.2.4';
16
- const NEXT_DENO_VERSION_RANGE = '^2.4.2';
15
+ export const DENO_VERSION_RANGE = '^2.4.2';
17
16
  export class DenoBridge {
17
+ cacheDirectory;
18
+ currentDownload;
19
+ debug;
20
+ denoDir;
21
+ logger;
22
+ onAfterDownload;
23
+ onBeforeDownload;
24
+ useGlobal;
25
+ versionRange;
18
26
  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;
27
+ this.cacheDirectory = options.cacheDirectory ?? getPathInHome('deno-cli');
28
+ this.debug = options.debug ?? false;
22
29
  this.denoDir = options.denoDir;
23
- this.logger = (_c = options.logger) !== null && _c !== void 0 ? _c : getLogger(undefined, undefined, options.debug);
30
+ this.logger = options.logger ?? getLogger(undefined, undefined, options.debug);
24
31
  this.onAfterDownload = options.onAfterDownload;
25
32
  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);
33
+ this.useGlobal = options.useGlobal ?? true;
34
+ this.versionRange = options.versionRange ?? DENO_VERSION_RANGE;
29
35
  }
30
36
  async downloadBinary() {
31
- var _a, _b, _c;
32
- await ((_a = this.onBeforeDownload) === null || _a === void 0 ? void 0 : _a.call(this));
37
+ await this.onBeforeDownload?.();
33
38
  await this.ensureCacheDirectory();
34
39
  this.logger.system(`Downloading Deno CLI to ${this.cacheDirectory}`);
35
40
  const binaryPath = await download(this.cacheDirectory, this.versionRange, this.logger);
@@ -39,12 +44,12 @@ export class DenoBridge {
39
44
  // that the tests catch it.
40
45
  if (downloadedVersion === undefined) {
41
46
  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));
47
+ await this.onAfterDownload?.(error);
43
48
  this.logger.system('Could not run downloaded Deno CLI', error);
44
49
  throw error;
45
50
  }
46
51
  await this.writeVersionFile(downloadedVersion);
47
- await ((_c = this.onAfterDownload) === null || _c === void 0 ? void 0 : _c.call(this));
52
+ await this.onAfterDownload?.();
48
53
  return binaryPath;
49
54
  }
50
55
  async getBinaryVersion(binaryPath) {
@@ -97,19 +102,18 @@ export class DenoBridge {
97
102
  return this.currentDownload;
98
103
  }
99
104
  static runWithBinary(binaryPath, args, { options, pipeOutput, stderr, stdout, }) {
100
- var _a, _b, _c, _d;
101
105
  const runDeno = execa(binaryPath, args, options);
102
106
  if (stderr) {
103
- (_a = runDeno.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
107
+ runDeno.stderr?.pipe(stderr);
104
108
  }
105
109
  else if (pipeOutput) {
106
- (_b = runDeno.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
110
+ runDeno.stderr?.pipe(process.stderr);
107
111
  }
108
112
  if (stdout) {
109
- (_c = runDeno.stdout) === null || _c === void 0 ? void 0 : _c.pipe(stdout);
113
+ runDeno.stdout?.pipe(stdout);
110
114
  }
111
115
  else if (pipeOutput) {
112
- (_d = runDeno.stdout) === null || _d === void 0 ? void 0 : _d.pipe(process.stdout);
116
+ runDeno.stdout?.pipe(process.stdout);
113
117
  }
114
118
  return runDeno;
115
119
  }
@@ -124,14 +128,14 @@ export class DenoBridge {
124
128
  async getBinaryPath(options) {
125
129
  const globalPath = await this.getGlobalBinary();
126
130
  if (globalPath !== undefined) {
127
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
131
+ if (!options?.silent) {
128
132
  this.logger.system('Using global installation of Deno CLI');
129
133
  }
130
134
  return { global: true, path: globalPath };
131
135
  }
132
136
  const cachedPath = await this.getCachedBinary();
133
137
  if (cachedPath !== undefined) {
134
- if (!(options === null || options === void 0 ? void 0 : options.silent)) {
138
+ if (!options?.silent) {
135
139
  this.logger.system('Using cached Deno CLI from', cachedPath);
136
140
  }
137
141
  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,22 @@ 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
+ importMap,
102
+ internalFunctions,
103
+ log: logger,
104
+ userFunctions,
105
+ });
103
106
  // Creating a final declarations array by combining the TOML file with the
104
107
  // deploy configuration API and the in-source configuration.
105
108
  const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations, featureFlags);
@@ -118,19 +121,54 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
118
121
  importMap: importMapSpecifier,
119
122
  layers: deployConfig.layers,
120
123
  });
121
- await (vendor === null || vendor === void 0 ? void 0 : vendor.cleanup());
124
+ await vendor?.cleanup();
122
125
  if (distImportMapPath) {
123
126
  await importMap.writeToFile(distImportMapPath);
124
127
  }
125
128
  return { functions, manifest };
126
129
  };
130
+ const getFunctionConfigs = async ({ basePath, deno, eszipPath, importMap, log, internalFunctions, userFunctions, }) => {
131
+ try {
132
+ const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
133
+ const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
134
+ // Creating a hash of function names to configuration objects.
135
+ const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
136
+ const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
137
+ return {
138
+ internalFunctions: internalFunctionsWithConfig,
139
+ userFunctions: userFunctionsWithConfig,
140
+ };
141
+ }
142
+ catch (err) {
143
+ if (!(err instanceof Error && err.cause === 'IMPORT_ASSERT') || !eszipPath) {
144
+ throw err;
145
+ }
146
+ // We failed to extract the configuration because there is an import assert
147
+ // in the function code, a deprecated feature that we used to support with
148
+ // Deno 1.x. To avoid a breaking change, we treat this error as a special
149
+ // case, using the generated ESZIP to extract the configuration. This works
150
+ // because import asserts are transpiled to import attributes.
151
+ const extractedESZIP = await extractESZIP(deno, eszipPath);
152
+ const configs = await Promise.all([...internalFunctions, ...userFunctions].map(async (func) => {
153
+ const relativePath = relative(basePath, func.path);
154
+ const functionPath = join(extractedESZIP.path, relativePath);
155
+ return [func.name, await getFunctionConfig({ functionPath, importMap, deno, log })];
156
+ }));
157
+ await extractedESZIP.cleanup();
158
+ return {
159
+ internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
160
+ userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
161
+ };
162
+ }
163
+ };
127
164
  const createFinalBundles = async (bundles, distDirectory, buildID) => {
128
165
  const renamingOps = bundles.map(async ({ extension, hash }) => {
129
166
  const tempBundlePath = join(distDirectory, `${buildID}${extension}`);
130
167
  const finalBundlePath = join(distDirectory, `${hash}${extension}`);
131
168
  await fs.rename(tempBundlePath, finalBundlePath);
169
+ return finalBundlePath;
132
170
  });
133
- await Promise.all(renamingOps);
171
+ return await Promise.all(renamingOps);
134
172
  };
135
173
  const getBasePath = (sourceDirectories, inputBasePath) => {
136
174
  // If there's a specific base path supplied, that takes precedence.
@@ -148,11 +186,11 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
148
186
  // declaration level. We want these properties to live at the function level
149
187
  // in their config object, so we translate that for backwards-compatibility.
150
188
  const mergeWithDeclarationConfig = ({ functionName, config, declarations }) => {
151
- const declaration = declarations === null || declarations === void 0 ? void 0 : declarations.find((decl) => decl.function === functionName);
189
+ const declaration = declarations?.find((decl) => decl.function === functionName);
152
190
  return {
153
191
  ...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,
192
+ name: declaration?.name || config.name,
193
+ generator: declaration?.generator || config.generator,
156
194
  };
157
195
  };
158
196
  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,22 +498,12 @@ 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,
518
508
  systemLogger,
519
509
  vendorDirectory: vendorDirectory.path,
@@ -524,6 +514,12 @@ test.skipIf(lt(denoVersion, '1.46.3') || gte(denoVersion, '2.0.0'))('Emits a sys
524
514
  const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
525
515
  expect(func1).toBe(`{"foo":"bar"}`);
526
516
  expect(systemLogger).toHaveBeenCalledWith(`Edge function uses import assertions: ${join(sourceDirectory, 'func1.ts')}`);
517
+ expect(manifest.routes[0]).toEqual({
518
+ function: 'func1',
519
+ pattern: '^/with-import-assert/?$',
520
+ excluded_patterns: [],
521
+ path: '/with-import-assert',
522
+ });
527
523
  await cleanup();
528
524
  await rm(vendorDirectory.path, { force: true, recursive: true });
529
525
  });
@@ -581,7 +577,7 @@ test('Loads edge functions from the Frameworks API', async () => {
581
577
  });
582
578
  await cleanup();
583
579
  });
584
- describe.skipIf(lt(denoVersion, '2.4.2'))('Produces a tarball bundle', () => {
580
+ describe.skipIf(lt(denoVersion, '2.4.3'))('Produces a tarball bundle', () => {
585
581
  test('With only local imports', async () => {
586
582
  const systemLogger = vi.fn();
587
583
  const { basePath, cleanup, distPath } = await useFixture('imports_node_builtin', { copyDirectory: true });
@@ -623,8 +619,7 @@ describe.skipIf(lt(denoVersion, '2.4.2'))('Produces a tarball bundle', () => {
623
619
  await cleanup();
624
620
  await rm(vendorDirectory.path, { force: true, recursive: true });
625
621
  });
626
- // TODO: https://github.com/denoland/deno/issues/30187
627
- test.todo('Using npm modules', async () => {
622
+ test('Using npm and remote modules', async () => {
628
623
  const systemLogger = vi.fn();
629
624
  const { basePath, cleanup, distPath } = await useFixture('imports_npm_module', { copyDirectory: true });
630
625
  const sourceDirectory = join(basePath, 'functions');
@@ -657,4 +652,4 @@ describe.skipIf(lt(denoVersion, '2.4.2'))('Produces a tarball bundle', () => {
657
652
  await cleanup();
658
653
  await rm(vendorDirectory.path, { force: true, recursive: true });
659
654
  });
660
- }, 10000);
655
+ }, 10_000);