diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 972b56a3f5..8548fdd4cb 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -50,12 +50,12 @@ jobs: os: [ubuntu-24.04, macos-14, windows-2022] node-version: ['22'] # Must include the minimum deno version from the `DENO_VERSION_RANGE` constant in `node/bridge.ts`. - deno-version: ['v1.39.0', 'v1.46.3'] + deno-version: ['v1.39.0', 'v2.2.4'] include: - os: ubuntu-24.04 # Earliest supported version node-version: '14.16.0' - deno-version: 'v1.46.3' + deno-version: 'v2.2.4' fail-fast: false steps: # Sets an output parameter if this is a release PR @@ -159,7 +159,7 @@ jobs: - name: Setup Deno uses: denoland/setup-deno@v1 with: - deno-version: v1.46.3 + deno-version: v2.2.4 if: ${{ !steps.release-check.outputs.IS_RELEASE }} - name: Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 diff --git a/package-lock.json b/package-lock.json index 84a08f626f..9a5f0f9d54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27905,7 +27905,7 @@ "node-fetch": "^3.3.1", "typescript": "^5.0.0", "unionfs": "^4.4.0", - "vite": "^6.2.3", + "vite": "^6.0.0", "vitest": "^0.34.0" }, "engines": { @@ -28629,7 +28629,7 @@ "rollup-plugin-node-polyfills": "^0.2.1", "tmp-promise": "^3.0.2", "typescript": "^5.0.0", - "vite": "^6.2.3", + "vite": "^6.0.0", "vitest": "^0.34.0" }, "engines": { @@ -28955,7 +28955,7 @@ "@types/node": "^14.18.53", "@vitest/ui": "^0.34.0", "typescript": "^5.0.0", - "vite": "^6.2.3", + "vite": "^6.0.0", "vitest": "^0.34.0" }, "engines": { @@ -28996,7 +28996,7 @@ "@types/node": "^14.18.53", "@vitest/ui": "^0.34.0", "typescript": "^5.0.0", - "vite": "^6.2.3", + "vite": "^6.0.0", "vitest": "^0.34.0" }, "engines": { diff --git a/packages/build/tests/edge_functions/snapshots/tests.js.md b/packages/build/tests/edge_functions/snapshots/tests.js.md index 255087b288..40c0f9a283 100644 --- a/packages/build/tests/edge_functions/snapshots/tests.js.md +++ b/packages/build/tests/edge_functions/snapshots/tests.js.md @@ -511,59 +511,6 @@ Generated by [AVA](https://avajs.dev). publish: packages/build/tests/edge_functions/fixtures/functions_invalid␊ publishOrigin: default` -## handles failure when validating Edge Functions - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/edge_functions/fixtures/functions_validation_failed␊ - ␊ - > Config file␊ - No config file was defined: using default values.␊ - ␊ - > Context␊ - production␊ - ␊ - 1. Edge Functions bundling ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Packaging Edge Functions from netlify/edge-functions directory:␊ - - functions-1␊ - ␊ - Bundling of edge function failed ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Error message␊ - Validation of Edge Functions manifest failed␊ - FORMAT pattern needs to be a regex that starts with ^ followed by / and ends with $ without any additional slashes before and afterwards␊ - ␊ - 13 | {␊ - 14 | "function": "functions-1",␊ - > 15 | "pattern": "^hello//?$"␊ - | ^^^^^^^^^^^^ 👈🏽 format pattern needs to be a regex that starts with ^ followed by / and ends with $ without any additional slashes before and afterwards␊ - 16 | }␊ - 17 | ],␊ - 18 | "post_cache_routes": [],␊ - ␊ - Error location␊ - While bundling edge function␊ - ␊ - Resolved config␊ - build:␊ - edge_functions: packages/build/tests/edge_functions/fixtures/functions_validation_failed/netlify/edge-functions␊ - publish: packages/build/tests/edge_functions/fixtures/functions_validation_failed␊ - publishOrigin: default` - ## bundles Edge Functions via runCoreSteps function > Snapshot 1 diff --git a/packages/build/tests/edge_functions/snapshots/tests.js.snap b/packages/build/tests/edge_functions/snapshots/tests.js.snap index a4e0da73db..95e31f7be3 100644 Binary files a/packages/build/tests/edge_functions/snapshots/tests.js.snap and b/packages/build/tests/edge_functions/snapshots/tests.js.snap differ diff --git a/packages/build/tests/monitor/snapshots/tests.js.md b/packages/build/tests/monitor/snapshots/tests.js.md index f1c5fbd1e0..3830a8a710 100644 --- a/packages/build/tests/monitor/snapshots/tests.js.md +++ b/packages/build/tests/monitor/snapshots/tests.js.md @@ -1606,7 +1606,7 @@ Generated by [AVA](https://avajs.dev). Error monitoring payload:␊ {␊ "errorClass": "functionsBundling",␊ - "errorMessage": "Command failed with exit code 1: deno run --allow-all --no-config --import-map=packages/edge-bundler/deno/vendor/import_map.json --quiet packages/edge-bundler/deno/bundle.ts {/"basePath/":/"packages/build/tests/monitor/fixtures/edge_function_error",/"destPath/":/"packages/build/tests/monitor/fixtures/edge_function_error/.netlify/edge-functions-dist/HEXADECIMAL_ID.eszip",/"externals/":[],/"functions/":[{/"name/":/"trouble/",/"path/":/"packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/trouble.ts"}],/"importMapData/":/"{//"imports//":{//"builtins//"://"node:builtins//",//"@netlify/edge-functions//"://"https://edge.netlify.com/v1.0.0/index.ts//",//"netlify:edge//"://"https://edge.netlify.com/v1.0.0/index.ts?v=legacy//"},//"scopes//":{}}/",/"vendorDirectory/":/"/external/path"}/nerror: Uncaught (in promise) Error: Error: Could not find file: packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/file.ts/n const ret = new Error(getStringFromWasm0(arg0, arg1));/n ^/n at __wbg_new_HEXADECIMAL_ID (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:80)/n at (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm_bg.wasm:1:80)/n at (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm_bg.wasm:1:80)/n at (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm_bg.wasm:1:80)/n at __wbg_adapter_40 (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:8)/n at real (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:80)/n at eventLoopTick (ext:core/01_core.js:80:7)",␊ + "errorMessage": "Command failed with exit code 1: deno run --allow-all --no-config --import-map=packages/edge-bundler/deno/vendor/import_map.json --quiet packages/edge-bundler/deno/bundle.ts {/"basePath/":/"packages/build/tests/monitor/fixtures/edge_function_error",/"destPath/":/"packages/build/tests/monitor/fixtures/edge_function_error/.netlify/edge-functions-dist/HEXADECIMAL_ID.eszip",/"externals/":[],/"functions/":[{/"name/":/"trouble/",/"path/":/"packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/trouble.ts"}],/"importMapData/":/"{//"imports//":{//"builtins//"://"node:builtins//",//"@netlify/edge-functions//"://"https://edge.netlify.com/v1.0.0/index.ts//",//"netlify:edge//"://"https://edge.netlify.com/v1.0.0/index.ts?v=legacy//"},//"scopes//":{}}/",/"vendorDirectory/":/"/external/path"}/nerror: Uncaught (in promise) Error: Error: Could not find file: packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/file.ts/n const ret = new Error(getStringFromWasm0(arg0, arg1));/n ^/n at __wbg_new_HEXADECIMAL_ID (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:80)/n at (wasm://wasm/HEXADECIMAL_ID:1:80)/n at (wasm://wasm/HEXADECIMAL_ID:1:80)/n at (wasm://wasm/HEXADECIMAL_ID:1:80)/n at __wbg_adapter_40 (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:8)/n at real (packages/edge-bundler/deno/vendor/deno.land/x/eszip@v1.0.0/eszip_wasm.generated.js:80:80)/n at eventLoopTick (ext:core/01_core.js:80:7)",␊ "context": "Bundling of edge function failed",␊ "groupingHash": "Bundling of edge function failed/nCommand failed with exit code 0: deno run --allow-all --no-config /external/path --quiet /external/path {/"/":/"/",/"/":/"/",/"/":[],/"/":[{/"/":/"/",/"/":/"/"}],/"/":/"/"/external/path"/"/external/path"/"/external/path"/"/external/path"/"/external/path"/"/external/path"/"/external/path"/"/external/path"/",/"/":/"/"}/nerror: Uncaught (in promise) Error: Error: Could not find file: /external/path const ret = new Error(getStringFromWasm0(arg0, arg0));/n ^/n at __wbg_new_hex /external/path at /external/path at /external/path at /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path",␊ "severity": "info",␊ diff --git a/packages/build/tests/monitor/snapshots/tests.js.snap b/packages/build/tests/monitor/snapshots/tests.js.snap index f88c034c3b..c1aee46f95 100644 Binary files a/packages/build/tests/monitor/snapshots/tests.js.snap and b/packages/build/tests/monitor/snapshots/tests.js.snap differ diff --git a/packages/edge-bundler/deno/lib/common.ts b/packages/edge-bundler/deno/lib/common.ts index a3f6ced471..e7ea78efc8 100644 --- a/packages/edge-bundler/deno/lib/common.ts +++ b/packages/edge-bundler/deno/lib/common.ts @@ -42,7 +42,7 @@ const loadWithRetry = (specifier: string, delay = 1000, maxTry = 3) => { maxTry, }); } catch (error) { - if (isTooManyTries(error)) { + if (isTooManyTries(error as Error)) { console.error(`Loading ${specifier} failed after ${maxTry} tries.`); } throw error; diff --git a/packages/edge-bundler/node/__snapshots__/bundler.test.ts.snap b/packages/edge-bundler/node/__snapshots__/bundler.test.ts.snap new file mode 100644 index 0000000000..03955441d4 --- /dev/null +++ b/packages/edge-bundler/node/__snapshots__/bundler.test.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Adds a custom error property to user errors during bundling 1`] = ` +"error: Uncaught (in promise) Error: The module's source code could not be parsed: Unexpected eof at file:///root/functions/func1.ts:1:27 + + export default async () => + ~ + const ret = new Error(getStringFromWasm0(arg0, arg1)); + ^ + at (file://build/packages/edge-bundler/deno/vendor/deno.land/x/eszip@v0.55.2/eszip_wasm.generated.js:443:19)" +`; diff --git a/packages/edge-bundler/node/bridge.ts b/packages/edge-bundler/node/bridge.ts index a597bd3275..bcb7e6603f 100644 --- a/packages/edge-bundler/node/bridge.ts +++ b/packages/edge-bundler/node/bridge.ts @@ -16,7 +16,7 @@ const DENO_VERSION_FILE = 'version.txt' // When updating DENO_VERSION_RANGE, ensure that the deno version // on the netlify/buildbot build image satisfies this range! // https://github.com/netlify/buildbot/blob/f9c03c9dcb091d6570e9d0778381560d469e78ad/build-image/noble/Dockerfile#L410 -const DENO_VERSION_RANGE = '1.39.0 - 1.46.3' +const DENO_VERSION_RANGE = '1.39.0 - 2.2.4' type OnBeforeDownloadHook = () => void | Promise type OnAfterDownloadHook = (error?: Error) => void | Promise @@ -99,7 +99,7 @@ class DenoBridge { return binaryPath } - private async getBinaryVersion(binaryPath: string) { + async getBinaryVersion(binaryPath: string) { try { const { stdout } = await execa(binaryPath, ['--version']) const version = stdout.match(/^deno ([\d.]+)/) @@ -202,11 +202,13 @@ class DenoBridge { await fs.mkdir(this.cacheDirectory, { recursive: true }) } - async getBinaryPath() { + async getBinaryPath(options?: { silent?: boolean }) { const globalPath = await this.getGlobalBinary() if (globalPath !== undefined) { - this.logger.system('Using global installation of Deno CLI') + if (!options?.silent) { + this.logger.system('Using global installation of Deno CLI') + } return { global: true, path: globalPath } } @@ -214,7 +216,9 @@ class DenoBridge { const cachedPath = await this.getCachedBinary() if (cachedPath !== undefined) { - this.logger.system('Using cached Deno CLI from', cachedPath) + if (!options?.silent) { + this.logger.system('Using cached Deno CLI from', cachedPath) + } return { global: false, path: cachedPath } } diff --git a/packages/edge-bundler/node/bundler.test.ts b/packages/edge-bundler/node/bundler.test.ts index b1e23a57fa..4358bd7277 100644 --- a/packages/edge-bundler/node/bundler.test.ts +++ b/packages/edge-bundler/node/bundler.test.ts @@ -104,16 +104,13 @@ test('Adds a custom error property to user errors during bundling', async () => await bundle([sourceDirectory], distPath, declarations, { basePath }) } catch (error) { expect(error).toBeInstanceOf(BundleError) - const [messageBeforeStack] = (error as BundleError).message.split('at (file://') - expect(messageBeforeStack).toMatchInlineSnapshot(` - "error: Uncaught (in promise) Error: The module's source code could not be parsed: Unexpected eof at file:///root/functions/func1.ts:1:27 - - export default async () => - ~ - const ret = new Error(getStringFromWasm0(arg0, arg1)); - ^ - " - `) + const messageBeforeStack = (error as BundleError).message + expect( + messageBeforeStack + .replace(/file:\/\/\/(.*?\/)(build\/packages\/edge-bundler\/deno\/vendor\/deno\.land\/x\/eszip.*)/, 'file://$2') + // eslint-disable-next-line no-control-regex + .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''), + ).toMatchSnapshot() expect((error as BundleError).customErrorInfo).toEqual({ location: { format: 'eszip', diff --git a/packages/edge-bundler/node/config.ts b/packages/edge-bundler/node/config.ts index 40b865b299..b65370257b 100644 --- a/packages/edge-bundler/node/config.ts +++ b/packages/edge-bundler/node/config.ts @@ -2,6 +2,7 @@ import { promises as fs } from 'fs' import { join } from 'path' import { pathToFileURL } from 'url' +import { SemVer } from 'semver' import tmp from 'tmp-promise' import { DenoBridge } from './bridge.js' @@ -95,22 +96,25 @@ export const getFunctionConfig = async ({ // The extractor will use its exit code to signal different error scenarios, // based on the list of exit codes we send as an argument. We then capture // the exit code to know exactly what happened and guide people accordingly. + const version = new SemVer((await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)) || '') + const { exitCode, stderr, stdout } = await deno.run( [ 'run', '--allow-env', + version.major >= 2 ? '--allow-import' : '', '--allow-net', '--allow-read', `--allow-write=${collector.path}`, - '--quiet', `--import-map=${importMap.toDataURL()}`, '--no-config', '--node-modules-dir=false', + '--quiet', extractorPath, pathToFileURL(func.path).href, pathToFileURL(collector.path).href, JSON.stringify(ConfigExitCode), - ], + ].filter(Boolean), { rejectOnExitCode: false }, ) diff --git a/packages/edge-bundler/node/server/server.test.ts b/packages/edge-bundler/node/server/server.test.ts index 5d4d484f67..3ce5816bc3 100644 --- a/packages/edge-bundler/node/server/server.test.ts +++ b/packages/edge-bundler/node/server/server.test.ts @@ -59,10 +59,9 @@ test('Starts a server and serves requests for edge functions', async () => { expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]) expect(npmSpecifiersWithExtraneousFiles).toEqual(['dictionary']) + const modules = graph?.modules.filter(({ kind, mediaType }) => kind === 'esm' && mediaType === 'TypeScript') for (const key in functions) { - const graphEntry = graph?.modules.some( - ({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path, - ) + const graphEntry = modules?.some(({ local }) => local === functions[key].path) expect(graphEntry).toBe(true) } diff --git a/packages/edge-bundler/node/server/server.ts b/packages/edge-bundler/node/server/server.ts index cd2c8af690..0ec1a402e2 100644 --- a/packages/edge-bundler/node/server/server.ts +++ b/packages/edge-bundler/node/server/server.ts @@ -1,6 +1,7 @@ import type { WriteStream } from 'fs' import { readdir, unlink } from 'fs/promises' import { join } from 'path' +import { pathToFileURL } from 'url' import { DenoBridge, OnAfterDownloadHook, OnBeforeDownloadHook, ProcessRef } from '../bridge.js' import { getFunctionConfig, FunctionConfig } from '../config.js' @@ -125,7 +126,7 @@ const prepareServer = ({ // the `stage2Path` file as well as all of their dependencies. // Consumers such as the CLI can use this information to watch all the // relevant files and issue an isolate restart when one of them changes. - const { stdout } = await deno.run(['info', '--json', stage2Path]) + const { stdout } = await deno.run(['info', '--json', pathToFileURL(stage2Path).href]) graph = JSON.parse(stdout) } catch { diff --git a/packages/edge-bundler/test/util.ts b/packages/edge-bundler/test/util.ts index 2b8d204772..12c71b7d91 100644 --- a/packages/edge-bundler/test/util.ts +++ b/packages/edge-bundler/test/util.ts @@ -99,7 +99,7 @@ const runESZIP = async (eszipPath: string, vendorDirectory?: string) => { await fs.rename(stage2Path, `${stage2Path}.js`) // Run function that imports the extracted stage 2 and invokes each function. - const evalCommand = execa('deno', ['eval', '--no-check', '--import-map', importMapPath, inspectFunction(stage2Path)]) + const evalCommand = execa('deno', ['eval', '--import-map', importMapPath, inspectFunction(stage2Path)]) evalCommand.stderr?.pipe(stderr)