@netlify/edge-bundler 14.8.3 → 14.8.4

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.
@@ -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 LEGACY_DENO_VERSION_RANGE = "1.39.0 - 2.2.4";
6
6
  export type OnBeforeDownloadHook = () => void | Promise<void>;
7
7
  export type OnAfterDownloadHook = (error?: Error) => void | Promise<void>;
8
8
  export interface DenoOptions {
@@ -9,11 +9,11 @@ import { getPathInHome } from './home_path.js';
9
9
  import { getLogger } from './logger.js';
10
10
  import { getBinaryExtension } from './platform.js';
11
11
  const DENO_VERSION_FILE = 'version.txt';
12
+ export const LEGACY_DENO_VERSION_RANGE = '1.39.0 - 2.2.4';
12
13
  // When updating DENO_VERSION_RANGE, ensure that the deno version
13
14
  // on the netlify/buildbot build image satisfies this range!
14
15
  // 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';
16
+ const DENO_VERSION_RANGE = '^2.4.2';
17
17
  export class DenoBridge {
18
18
  cacheDirectory;
19
19
  currentDownload;
@@ -35,7 +35,7 @@ export class DenoBridge {
35
35
  const useNextDeno = options.featureFlags?.edge_bundler_dry_run_generate_tarball ||
36
36
  options.featureFlags?.edge_bundler_generate_tarball ||
37
37
  options.featureFlags?.edge_bundler_deno_v2;
38
- this.versionRange = options.versionRange ?? (useNextDeno ? NEXT_DENO_VERSION_RANGE : DENO_VERSION_RANGE);
38
+ this.versionRange = options.versionRange ?? (useNextDeno ? DENO_VERSION_RANGE : LEGACY_DENO_VERSION_RANGE);
39
39
  }
40
40
  async downloadBinary() {
41
41
  await this.onBeforeDownload?.();
@@ -1,15 +1,15 @@
1
1
  import { promises as fs } from 'fs';
2
- import { join, relative } from 'path';
2
+ import { join } 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';
6
- import { DenoBridge } from './bridge.js';
6
+ import { DenoBridge, LEGACY_DENO_VERSION_RANGE, } from './bridge.js';
7
7
  import { getFunctionConfig } from './config.js';
8
8
  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, extension as eszipExtension, extract as extractESZIP } from './formats/eszip.js';
12
+ import { bundle as bundleESZIP } 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';
@@ -17,7 +17,7 @@ import { writeManifest } from './manifest.js';
17
17
  import { vendorNPMSpecifiers } from './npm_dependencies.js';
18
18
  import { ensureLatestTypes } from './types.js';
19
19
  import { nonNullable } from './utils/non_nullable.js';
20
- import { BundleError } from './bundle_error.js';
20
+ import { getPathInHome } from './home_path.js';
21
21
  export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], internalSrcFolder, onAfterDownload, onBeforeDownload, rootPath, userLogger, systemLogger, vendorDirectory, } = {}) => {
22
22
  const logger = getLogger(systemLogger, userLogger, debug);
23
23
  const featureFlags = getFlags(inputFeatureFlags);
@@ -110,13 +110,9 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
110
110
  // The final file name of the bundles contains a SHA256 hash of the contents,
111
111
  // which we can only compute now that the files have been generated. So let's
112
112
  // rename the bundles to their permanent names.
113
- const bundlePaths = await createFinalBundles(bundles, distDirectory, buildID);
114
- const eszipPath = bundlePaths.find((path) => path.endsWith(eszipExtension));
113
+ await createFinalBundles(bundles, distDirectory, buildID);
115
114
  const { internalFunctions: internalFunctionsWithConfig, userFunctions: userFunctionsWithConfig } = await getFunctionConfigs({
116
- basePath,
117
115
  deno,
118
- eszipPath,
119
- featureFlags,
120
116
  importMap,
121
117
  internalFunctions,
122
118
  log: logger,
@@ -146,45 +142,44 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
146
142
  }
147
143
  return { functions, manifest };
148
144
  };
149
- const getFunctionConfigs = async ({ basePath, deno, eszipPath, featureFlags, importMap, log, internalFunctions, userFunctions, }) => {
150
- try {
151
- const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
152
- const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })]);
153
- // Creating a hash of function names to configuration objects.
154
- const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
155
- const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
156
- return {
157
- internalFunctions: internalFunctionsWithConfig,
158
- userFunctions: userFunctionsWithConfig,
159
- };
160
- }
161
- catch (err) {
162
- if (!(err instanceof Error && err.cause === 'IMPORT_ASSERT') || !eszipPath || !featureFlags?.edge_bundler_deno_v2) {
163
- throw err;
164
- }
165
- log.user('WARNING: Import assertions are deprecated and will be removed soon. Refer to https://ntl.fyi/import-assert for more information.');
166
- try {
167
- // We failed to extract the configuration because there is an import assert
168
- // in the function code, a deprecated feature that we used to support with
169
- // Deno 1.x. To avoid a breaking change, we treat this error as a special
170
- // case, using the generated ESZIP to extract the configuration. This works
171
- // because import asserts are transpiled to import attributes.
172
- const extractedESZIP = await extractESZIP(deno, eszipPath);
173
- const configs = await Promise.all([...internalFunctions, ...userFunctions].map(async (func) => {
174
- const relativePath = relative(basePath, func.path);
175
- const functionPath = join(extractedESZIP.path, relativePath);
176
- return [func.name, await getFunctionConfig({ functionPath, importMap, deno, log })];
177
- }));
178
- await extractedESZIP.cleanup();
179
- return {
180
- internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
181
- userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
182
- };
183
- }
184
- catch (err) {
185
- throw new BundleError(new Error('An error occurred while building an edge function that uses an import assertion. Refer to https://ntl.fyi/import-assert for more information.'), { cause: err });
145
+ const getFunctionConfigs = async ({ deno, importMap, log, internalFunctions, userFunctions, }) => {
146
+ const functions = [...internalFunctions, ...userFunctions];
147
+ const results = await Promise.allSettled(functions.map(async (func) => {
148
+ return [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })];
149
+ }));
150
+ const legacyDeno = new DenoBridge({
151
+ cacheDirectory: getPathInHome('deno-cli-v1'),
152
+ useGlobal: false,
153
+ versionRange: LEGACY_DENO_VERSION_RANGE,
154
+ });
155
+ for (let i = 0; i < results.length; i++) {
156
+ const result = results[i];
157
+ const func = functions[i];
158
+ // We offer support for some features of Deno 1.x that have been removed
159
+ // from 2.x, such as import assertions and the `window` global. When we
160
+ // see that we failed to extract a config due to those edge cases, re-run
161
+ // the script with Deno 1.x so we can extract the config.
162
+ if (result.status === 'rejected' &&
163
+ result.reason instanceof Error &&
164
+ (result.reason.cause === 'IMPORT_ASSERT' || result.reason.cause === 'WINDOW_GLOBAL')) {
165
+ try {
166
+ const fallbackConfig = await getFunctionConfig({ functionPath: func.path, importMap, deno: legacyDeno, log });
167
+ results[i] = { status: 'fulfilled', value: [func.name, fallbackConfig] };
168
+ }
169
+ catch {
170
+ throw result.reason;
171
+ }
186
172
  }
187
173
  }
174
+ const failure = results.find((result) => result.status === 'rejected');
175
+ if (failure) {
176
+ throw failure.reason;
177
+ }
178
+ const configs = results.map((config) => config.value);
179
+ return {
180
+ internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
181
+ userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
182
+ };
188
183
  };
189
184
  const createFinalBundles = async (bundles, distDirectory, buildID) => {
190
185
  const renamingOps = bundles.map(async ({ extension, hash }) => {
@@ -84,10 +84,15 @@ export const getFunctionConfig = async ({ deno, functionPath, importMap, log, })
84
84
  };
85
85
  const handleConfigError = (functionPath, exitCode, stderr, log) => {
86
86
  let cause;
87
- if (stderr.includes('Import assertions are deprecated')) {
87
+ if (stderr.includes('Import assertions are deprecated') ||
88
+ stderr.includes(`SyntaxError: Unexpected identifier 'assert'`)) {
88
89
  log.system(`Edge function uses import assertions: ${functionPath}`);
89
90
  cause = 'IMPORT_ASSERT';
90
91
  }
92
+ if (stderr.includes('ReferenceError: window is not defined')) {
93
+ log.system(`Edge function uses the window global: ${functionPath}`);
94
+ cause = 'WINDOW_GLOBAL';
95
+ }
91
96
  switch (exitCode) {
92
97
  case ConfigExitCode.ImportError:
93
98
  log.user(stderr);
@@ -17,8 +17,4 @@ interface BundleESZIPOptions {
17
17
  vendorDirectory?: string;
18
18
  }
19
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
20
  export {};
@@ -1,6 +1,5 @@
1
1
  import { join } from 'path';
2
2
  import { pathToFileURL } from 'url';
3
- import tmp from 'tmp-promise';
4
3
  import { virtualRoot, virtualVendorRoot } from '../../shared/consts.js';
5
4
  import { BundleFormat } from '../bundle.js';
6
5
  import { wrapBundleError } from '../bundle_error.js';
@@ -49,13 +48,3 @@ const getESZIPPaths = () => {
49
48
  importMap: join(denoPath, 'vendor', 'import_map.json'),
50
49
  };
51
50
  };
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
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "14.8.3",
3
+ "version": "14.8.4",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -9,6 +9,7 @@
9
9
  "deno/**",
10
10
  "!deno/**/*.test.ts",
11
11
  "dist/**/*.js",
12
+ "!dist/**/*.test.js",
12
13
  "dist/**/*.d.ts",
13
14
  "shared/**"
14
15
  ],
@@ -64,7 +65,7 @@
64
65
  "better-ajv-errors": "^1.2.0",
65
66
  "common-path-prefix": "^3.0.0",
66
67
  "env-paths": "^3.0.0",
67
- "esbuild": "0.25.10",
68
+ "esbuild": "0.25.11",
68
69
  "execa": "^8.0.0",
69
70
  "find-up": "^7.0.0",
70
71
  "get-port": "^7.0.0",
@@ -79,5 +80,5 @@
79
80
  "urlpattern-polyfill": "8.0.2",
80
81
  "uuid": "^11.0.0"
81
82
  },
82
- "gitHead": "ba28f3d0b3bde3f5d43ad79e11fcc48f681dbc3c"
83
+ "gitHead": "eae1c7c9d0d7ab598aa676f72d298160a5fee92a"
83
84
  }
package/deno/extract.ts DELETED
@@ -1,7 +0,0 @@
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)
@@ -1,141 +0,0 @@
1
- import { Buffer } from 'buffer';
2
- import { rm } from 'fs/promises';
3
- import { createRequire } from 'module';
4
- import { platform, env } from 'process';
5
- import { PassThrough } from 'stream';
6
- import nock from 'nock';
7
- import semver from 'semver';
8
- import tmp from 'tmp-promise';
9
- import { test, expect } from 'vitest';
10
- import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js';
11
- import { getPlatformTarget } from './platform.js';
12
- const require = createRequire(import.meta.url);
13
- const archiver = require('archiver');
14
- const getMockDenoBridge = function (tmpDir, mockBinaryOutput) {
15
- const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '';
16
- const data = new PassThrough();
17
- const archive = archiver('zip', { zlib: { level: 9 } });
18
- archive.pipe(data);
19
- archive.append(Buffer.from(mockBinaryOutput.replace(/@@@latestVersion@@@/g, latestVersion)), {
20
- name: platform === 'win32' ? 'deno.exe' : 'deno',
21
- });
22
- archive.finalize();
23
- const target = getPlatformTarget();
24
- nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`);
25
- nock('https://dl.deno.land')
26
- .get(`/release/v${latestVersion}/deno-${target}.zip`)
27
- .reply(200, () => data);
28
- return new DenoBridge({
29
- cacheDirectory: tmpDir.path,
30
- useGlobal: false,
31
- });
32
- };
33
- test('Does not inherit environment variables if `extendEnv` is false', async () => {
34
- const tmpDir = await tmp.dir();
35
- const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
36
-
37
- if [ "$1" = "test" ]
38
- then
39
- env
40
- else
41
- echo "deno @@@latestVersion@@@"
42
- fi`);
43
- // The environment sets some variables so let us see what they are and remove them from the result
44
- const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: false });
45
- env.TADA = 'TUDU';
46
- const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: false });
47
- let output = result?.stdout ?? '';
48
- delete env.TADA;
49
- referenceOutput?.stdout.split('\n').forEach((line) => {
50
- output = output.replace(line.trim(), '');
51
- });
52
- output = output.trim().replace(/\n+/g, '\n');
53
- expect(output).toBe('LULU=LALA');
54
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
55
- });
56
- test('Does inherit environment variables if `extendEnv` is true', async () => {
57
- const tmpDir = await tmp.dir();
58
- const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
59
-
60
- if [ "$1" = "test" ]
61
- then
62
- env
63
- else
64
- echo "deno @@@latestVersion@@@"
65
- fi`);
66
- // The environment sets some variables so let us see what they are and remove them from the result
67
- const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
68
- env.TADA = 'TUDU';
69
- const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: true });
70
- let output = result?.stdout ?? '';
71
- delete env.TADA;
72
- referenceOutput?.stdout.split('\n').forEach((line) => {
73
- output = output.replace(line.trim(), '');
74
- });
75
- // lets remove holes, split lines and sort lines by name, as different OSes might order them different
76
- const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
77
- expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
78
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
79
- });
80
- test('Does inherit environment variables if `extendEnv` is not set', async () => {
81
- const tmpDir = await tmp.dir();
82
- const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
83
-
84
- if [ "$1" = "test" ]
85
- then
86
- env
87
- else
88
- echo "deno @@@latestVersion@@@"
89
- fi`);
90
- // The environment sets some variables so let us see what they are and remove them from the result
91
- const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
92
- env.TADA = 'TUDU';
93
- const result = await deno.run(['test'], { env: { LULU: 'LALA' } });
94
- let output = result?.stdout ?? '';
95
- delete env.TADA;
96
- referenceOutput?.stdout.split('\n').forEach((line) => {
97
- output = output.replace(line.trim(), '');
98
- });
99
- // lets remove holes, split lines and sort lines by name, as different OSes might order them different
100
- const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
101
- expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
102
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
103
- });
104
- test('Provides actionable error message when downloaded binary cannot be executed', async () => {
105
- const tmpDir = await tmp.dir();
106
- const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '';
107
- const data = new PassThrough();
108
- const archive = archiver('zip', { zlib: { level: 9 } });
109
- archive.pipe(data);
110
- // Create a binary that will fail to execute (invalid content)
111
- archive.append(Buffer.from('invalid binary content'), {
112
- name: platform === 'win32' ? 'deno.exe' : 'deno',
113
- });
114
- archive.finalize();
115
- const target = getPlatformTarget();
116
- nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`);
117
- nock('https://dl.deno.land')
118
- .get(`/release/v${latestVersion}/deno-${target}.zip`)
119
- .reply(200, () => data);
120
- const deno = new DenoBridge({
121
- cacheDirectory: tmpDir.path,
122
- useGlobal: false,
123
- });
124
- try {
125
- await deno.getBinaryPath();
126
- expect.fail('Should have thrown an error');
127
- }
128
- catch (error) {
129
- expect(error).toBeInstanceOf(Error);
130
- const errorMessage = error.message;
131
- expect(errorMessage).toContain('Failed to set up Deno for Edge Functions');
132
- expect(errorMessage).toMatch(/Error:/);
133
- expect(errorMessage).toMatch(/Downloaded to: .+deno(\.exe)?/);
134
- expect(errorMessage).toContain(tmpDir.path);
135
- expect(errorMessage).toMatch(/Platform: (darwin|linux|win32)\/(x64|arm64|ia32)/);
136
- expect(errorMessage).toContain('This may be caused by permissions, antivirus software, or platform incompatibility');
137
- expect(errorMessage).toContain('Try clearing the Deno cache directory and retrying');
138
- expect(errorMessage).toContain('https://ntl.fyi/install-deno');
139
- }
140
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
141
- });