From 4141a58d3adfc356cc93b73f7ae8242976e56a10 Mon Sep 17 00:00:00 2001 From: David Volquartz Lebech Date: Sun, 4 Oct 2026 08:23:58 +0200 Subject: [PATCH 1/5] fix: support bare Node built-ins and .wasm/font files in Netlify edge functions The migration from esbuild to rolldown (#15432) regressed two cases in the edge function bundle: - `resolve.alias` rewrites `crypto` to `node:crypto`, but rolldown then tries to resolve `node:crypto` itself and errors before `external` is checked. Replace the alias with a `resolveId` plugin that marks it external. - The esbuild `copy` loaders for `.wasm` and font files were dropped, so rolldown tried to parse them as JS. Restore them via `moduleTypes`. Co-Authored-By: Claude Opus 5.5 --- .changeset/quiet-bees-copy.md | 5 +++++ packages/adapter-netlify/index.js | 21 +++++++++++++++--- .../edge/_test_dependencies/wasm-lib/add.wasm | Bin 0 -> 41 bytes .../edge/_test_dependencies/wasm-lib/index.js | 10 +++++++++ .../_test_dependencies/wasm-lib/package.json | 7 ++++++ .../test/apps/edge/build.test.js | 16 +++++++++++++ .../test/apps/edge/package.json | 1 + .../edge/src/routes/node-builtin/+server.js | 6 +++++ .../test/apps/edge/src/routes/wasm/+server.js | 5 +++++ .../test/apps/edge/test/test.js | 12 ++++++++++ pnpm-lock.yaml | 8 +++++++ 11 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 .changeset/quiet-bees-copy.md create mode 100644 packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/add.wasm create mode 100644 packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/index.js create mode 100644 packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/package.json create mode 100644 packages/adapter-netlify/test/apps/edge/src/routes/node-builtin/+server.js create mode 100644 packages/adapter-netlify/test/apps/edge/src/routes/wasm/+server.js diff --git a/.changeset/quiet-bees-copy.md b/.changeset/quiet-bees-copy.md new file mode 100644 index 000000000000..5764528fde04 --- /dev/null +++ b/.changeset/quiet-bees-copy.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: support bare Node built-in imports and `.wasm`/font files in edge functions diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 0057a2b40511..85b0ffcb2c2f 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -413,9 +413,24 @@ const rolldown_config = { // Node built-ins are allowed, but must be prefixed with `node:` // https://docs.netlify.com/edge-functions/api/#runtime-environment external: builtinModules.map((id) => `node:${id}`), - resolve: { - alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])) - } + moduleTypes: { + '.wasm': 'copy', + '.woff': 'copy', + '.woff2': 'copy', + '.ttf': 'copy', + '.eot': 'copy', + '.otf': 'copy' + }, + plugins: [ + { + // `resolve.alias` doesn't work here because rolldown tries to resolve + // the aliased `node:` specifier before checking `external` + name: 'node-builtins', + resolveId(id) { + if (builtinModules.includes(id)) return { id: `node:${id}`, external: true }; + } + } + ] }; /** diff --git a/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/add.wasm b/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/index.js b/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/index.js new file mode 100644 index 000000000000..88dfd3ce116a --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/index.js @@ -0,0 +1,10 @@ +// imports a .wasm file directly, like resvg/satori-based packages +/** + * @param {number} a + * @param {number} b + * @returns {Promise} + */ +export async function add(a, b) { + const wasm = await import('./add.wasm'); + return wasm.add(a, b); +} diff --git a/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/package.json b/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/package.json new file mode 100644 index 000000000000..9c2afd4faef7 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib/package.json @@ -0,0 +1,7 @@ +{ + "name": "e2e-test-dep-wasm", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": "./index.js" +} diff --git a/packages/adapter-netlify/test/apps/edge/build.test.js b/packages/adapter-netlify/test/apps/edge/build.test.js index 0803ca86a838..c786b5d0feda 100644 --- a/packages/adapter-netlify/test/apps/edge/build.test.js +++ b/packages/adapter-netlify/test/apps/edge/build.test.js @@ -35,3 +35,19 @@ test('treeshakes component from the server bundle if SSR is turned off', () => { const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); expect(edge_function).not.toContain('this should never appear in the server bundle'); }); + +test('bare Node built-in imports are prefixed with `node:`', () => { + const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); + expect(edge_function).toMatch(/from ["']node:crypto["']/); + expect(edge_function).not.toMatch(/from ["']crypto["']/); +}); + +test('.wasm files are copied alongside the edge function', () => { + const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); + const wasm_import = edge_function.match(/import\(["'](\.\/[^"']+\.wasm)["']\)/)?.[1]; + + expect(wasm_import).toBeDefined(); + expect(fs.existsSync(path.resolve(path.dirname(edge_function_path), wasm_import ?? ''))).toBe( + true + ); +}); diff --git a/packages/adapter-netlify/test/apps/edge/package.json b/packages/adapter-netlify/test/apps/edge/package.json index 429898785187..1761d243ebb2 100644 --- a/packages/adapter-netlify/test/apps/edge/package.json +++ b/packages/adapter-netlify/test/apps/edge/package.json @@ -12,6 +12,7 @@ "devDependencies": { "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", + "e2e-test-dep-wasm": "file:./_test_dependencies/wasm-lib", "svelte": "catalog:", "vite": "catalog:", "vitest": "catalog:" diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/node-builtin/+server.js b/packages/adapter-netlify/test/apps/edge/src/routes/node-builtin/+server.js new file mode 100644 index 000000000000..26340cae52fc --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/node-builtin/+server.js @@ -0,0 +1,6 @@ +// a bare (non `node:`-prefixed) built-in import, as found in many dependencies +import { createHash } from 'crypto'; + +export function GET() { + return new Response(createHash('sha256').update('hello').digest('hex')); +} diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/wasm/+server.js b/packages/adapter-netlify/test/apps/edge/src/routes/wasm/+server.js new file mode 100644 index 000000000000..f2c941ac8a17 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/wasm/+server.js @@ -0,0 +1,5 @@ +import { add } from 'e2e-test-dep-wasm'; + +export async function GET() { + return new Response(String(await add(1, 2))); +} diff --git a/packages/adapter-netlify/test/apps/edge/test/test.js b/packages/adapter-netlify/test/apps/edge/test/test.js index 35c80a26e652..8a9986301ad6 100644 --- a/packages/adapter-netlify/test/apps/edge/test/test.js +++ b/packages/adapter-netlify/test/apps/edge/test/test.js @@ -33,3 +33,15 @@ test('page renders on the client if SSR is turned off', async ({ page }) => { await page.goto('/treeshake-server'); await expect(page.locator('p')).toHaveText('this should never appear in the server bundle'); }); + +test('bare Node built-in imports work', async ({ request }) => { + const response = await request.get('/node-builtin'); + expect(await response.text()).toBe( + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' + ); +}); + +test('.wasm imports work', async ({ request }) => { + const response = await request.get('/wasm'); + expect(await response.text()).toBe('3'); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beaf86858e38..d4b2b047c69c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -464,6 +464,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: 'catalog:' version: 7.3.1(svelte@5.57.1(@typescript-eslint/types@8.70.1))(vite@8.3.0(@types/node@22.20.4)(esbuild@0.28.1)(yaml@2.9.1)) + e2e-test-dep-wasm: + specifier: file:./_test_dependencies/wasm-lib + version: file:packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib svelte: specifier: 'catalog:' version: 5.57.1(@typescript-eslint/types@8.70.1) @@ -3543,6 +3546,9 @@ packages: peerDependencies: '@sveltejs/kit': '*' + e2e-test-dep-wasm@file:packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib: + resolution: {directory: packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib, type: directory} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -6146,6 +6152,8 @@ snapshots: dependencies: '@sveltejs/kit': link:packages/kit + e2e-test-dep-wasm@file:packages/adapter-netlify/test/apps/edge/_test_dependencies/wasm-lib: {} + emoji-regex@8.0.0: {} enhanced-resolve@5.25.1: From 8a0a623a5007d232083824f5b67f6d8f9c5d996d Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 6 Oct 2026 00:02:39 +0800 Subject: [PATCH 2/5] try mirroring Netlify's edge-bundler esbuild platform --- packages/adapter-netlify/index.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 85b0ffcb2c2f..db4b22d21a44 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -402,7 +402,12 @@ function to_import_specifier(path) { /** @satisfies {import('rolldown').BuildOptions} */ const rolldown_config = { - platform: 'browser', + // Mirror Netlify's edge-bundler behaviour for externalising Node.js built-ins + // see https://github.com/netlify/build/blob/main/packages/edge-bundler/node/npm_dependencies.ts#L289-L309 + platform: 'node', + resolve: { + mainFields: ['module', 'browser', 'main'], + }, output: { sourcemap: true, codeSplitting: false @@ -410,9 +415,8 @@ const rolldown_config = { transform: { target: 'es2022' }, - // Node built-ins are allowed, but must be prefixed with `node:` - // https://docs.netlify.com/edge-functions/api/#runtime-environment - external: builtinModules.map((id) => `node:${id}`), + // Rolldown's default does not handle these files automatically + // see https://github.com/rolldown/rolldown/blob/main/crates/rolldown/src/utils/prepare_build_context.rs#L320-L336 moduleTypes: { '.wasm': 'copy', '.woff': 'copy', @@ -420,17 +424,7 @@ const rolldown_config = { '.ttf': 'copy', '.eot': 'copy', '.otf': 'copy' - }, - plugins: [ - { - // `resolve.alias` doesn't work here because rolldown tries to resolve - // the aliased `node:` specifier before checking `external` - name: 'node-builtins', - resolveId(id) { - if (builtinModules.includes(id)) return { id: `node:${id}`, external: true }; - } - } - ] + } }; /** From 945f1b78dae17c018af3014160260d981511a535 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 6 Oct 2026 00:11:38 +0800 Subject: [PATCH 3/5] fix --- packages/adapter-netlify/index.js | 13 ++++++------- .../adapter-netlify/test/apps/edge/build.test.js | 6 ------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index db4b22d21a44..b5a59dd246ed 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -4,7 +4,6 @@ import crypto from 'node:crypto'; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { builtinModules } from 'node:module'; import process from 'node:process'; import { build } from 'rolldown'; import { matches, s } from './utils.js'; @@ -402,12 +401,12 @@ function to_import_specifier(path) { /** @satisfies {import('rolldown').BuildOptions} */ const rolldown_config = { - // Mirror Netlify's edge-bundler behaviour for externalising Node.js built-ins - // see https://github.com/netlify/build/blob/main/packages/edge-bundler/node/npm_dependencies.ts#L289-L309 - platform: 'node', - resolve: { - mainFields: ['module', 'browser', 'main'], - }, + // Mirror Netlify's edge-bundler behaviour for externalising Node.js built-ins + // see https://github.com/netlify/build/blob/main/packages/edge-bundler/node/npm_dependencies.ts#L289-L309 + platform: 'node', + resolve: { + mainFields: ['module', 'browser', 'main'] + }, output: { sourcemap: true, codeSplitting: false diff --git a/packages/adapter-netlify/test/apps/edge/build.test.js b/packages/adapter-netlify/test/apps/edge/build.test.js index c786b5d0feda..559bcf2021f5 100644 --- a/packages/adapter-netlify/test/apps/edge/build.test.js +++ b/packages/adapter-netlify/test/apps/edge/build.test.js @@ -36,12 +36,6 @@ test('treeshakes component from the server bundle if SSR is turned off', () => { expect(edge_function).not.toContain('this should never appear in the server bundle'); }); -test('bare Node built-in imports are prefixed with `node:`', () => { - const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); - expect(edge_function).toMatch(/from ["']node:crypto["']/); - expect(edge_function).not.toMatch(/from ["']crypto["']/); -}); - test('.wasm files are copied alongside the edge function', () => { const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); const wasm_import = edge_function.match(/import\(["'](\.\/[^"']+\.wasm)["']\)/)?.[1]; From ebcd5354efee0d141ced97db4ab3fcd257464bba Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 6 Oct 2026 00:41:20 +0800 Subject: [PATCH 4/5] revert --- packages/adapter-netlify/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index b5a59dd246ed..2720a559d032 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -4,6 +4,7 @@ import crypto from 'node:crypto'; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { builtinModules } from 'node:module'; import process from 'node:process'; import { build } from 'rolldown'; import { matches, s } from './utils.js'; @@ -404,9 +405,6 @@ const rolldown_config = { // Mirror Netlify's edge-bundler behaviour for externalising Node.js built-ins // see https://github.com/netlify/build/blob/main/packages/edge-bundler/node/npm_dependencies.ts#L289-L309 platform: 'node', - resolve: { - mainFields: ['module', 'browser', 'main'] - }, output: { sourcemap: true, codeSplitting: false @@ -414,6 +412,13 @@ const rolldown_config = { transform: { target: 'es2022' }, + // Node built-ins are allowed, but must be prefixed with `node:` + // https://docs.netlify.com/edge-functions/api/#runtime-environment + external: builtinModules.map((id) => `node:${id}`), + resolve: { + alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])), + mainFields: ['module', 'browser', 'main'] + }, // Rolldown's default does not handle these files automatically // see https://github.com/rolldown/rolldown/blob/main/crates/rolldown/src/utils/prepare_build_context.rs#L320-L336 moduleTypes: { From 931571d894928daccbc4df12bd6bfd8e20c22d3f Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 6 Oct 2026 00:44:27 +0800 Subject: [PATCH 5/5] remove redundant --- packages/adapter-netlify/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 2720a559d032..80b7ddbe7bc8 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -414,7 +414,6 @@ const rolldown_config = { }, // Node built-ins are allowed, but must be prefixed with `node:` // https://docs.netlify.com/edge-functions/api/#runtime-environment - external: builtinModules.map((id) => `node:${id}`), resolve: { alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])), mainFields: ['module', 'browser', 'main']