From 3723d105caf53f80641efec5f499e9de3ef83b01 Mon Sep 17 00:00:00 2001 From: jake champion Date: Thu, 4 Dec 2025 23:12:44 +0000 Subject: [PATCH] fix(edge-bundler): add support for Deno 2.x import assertions compatibility when using tarball bundling flow Updates the bundling process to handle Deno 2.x's deprecation of import assertions by automatically converting `assert` syntax to `with` syntax. This ensures compatibility when bundling Edge Functions that use import assertions, falling back to a temporary copy of source files with converted syntax when needed. --- package-lock.json | 253 +++++++++++------- packages/edge-bundler/node/bundler.test.ts | 20 ++ .../import-assertions-to-attributes.test.ts | 189 +++++++++++++ .../import-assertions-to-attributes.ts | 120 +++++++++ packages/edge-bundler/node/formats/tarball.ts | 103 +++++-- packages/edge-bundler/package.json | 7 + .../functions/data.json | 3 + .../functions/default.json | 3 + .../functions/exported.json | 3 + .../functions/import-assertions.tsx | 78 ++++++ 10 files changed, 660 insertions(+), 119 deletions(-) create mode 100644 packages/edge-bundler/node/formats/import-assertions-to-attributes.test.ts create mode 100644 packages/edge-bundler/node/formats/import-assertions-to-attributes.ts create mode 100644 packages/edge-bundler/test/fixtures/with_import_assertions/functions/data.json create mode 100644 packages/edge-bundler/test/fixtures/with_import_assertions/functions/default.json create mode 100644 packages/edge-bundler/test/fixtures/with_import_assertions/functions/exported.json create mode 100644 packages/edge-bundler/test/fixtures/with_import_assertions/functions/import-assertions.tsx diff --git a/package-lock.json b/package-lock.json index 17d3851d78..0c1c14fe10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,19 +47,44 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -79,12 +104,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -93,6 +118,38 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", @@ -1591,18 +1648,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1614,16 +1666,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1631,10 +1673,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2444,6 +2485,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3399,6 +3441,7 @@ "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3587,6 +3630,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3596,6 +3640,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz", "integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api": "^1.0.0" }, @@ -4823,8 +4868,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.44.2", @@ -4838,8 +4882,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.44.2", @@ -4853,8 +4896,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.44.2", @@ -4868,8 +4910,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.44.2", @@ -4883,8 +4924,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.44.2", @@ -4898,8 +4938,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.44.2", @@ -4913,8 +4952,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.44.2", @@ -4928,8 +4966,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.44.2", @@ -4943,8 +4980,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.44.2", @@ -4958,8 +4994,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.44.2", @@ -4973,8 +5008,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.44.2", @@ -4988,8 +5022,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.44.2", @@ -5003,8 +5036,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.44.2", @@ -5018,8 +5050,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.44.2", @@ -5033,8 +5064,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.44.2", @@ -5048,8 +5078,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.44.2", @@ -5063,8 +5092,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.44.2", @@ -5078,8 +5106,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.44.2", @@ -5093,8 +5120,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.44.2", @@ -5108,8 +5134,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -5358,6 +5383,26 @@ "@types/readdir-glob": "*" } }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -5485,6 +5530,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5615,6 +5661,7 @@ "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.1", "@typescript-eslint/types": "8.35.1", @@ -6368,6 +6415,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6471,6 +6519,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7542,6 +7591,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -9277,6 +9327,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -10861,7 +10912,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -10879,7 +10929,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -10897,7 +10946,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -10915,7 +10963,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -10933,7 +10980,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -10951,7 +10997,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -10969,7 +11014,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -10987,7 +11031,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -11005,7 +11048,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11023,7 +11065,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11041,7 +11082,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11059,7 +11099,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11077,7 +11116,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11095,7 +11133,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11113,7 +11150,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11131,7 +11167,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11149,7 +11184,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -11167,7 +11201,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -11185,7 +11218,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -11203,7 +11235,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -11221,7 +11252,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -11239,7 +11269,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -11257,7 +11286,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -11275,7 +11303,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -11293,7 +11320,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -11311,7 +11337,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -11364,6 +11389,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -13919,7 +13945,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -15465,6 +15491,18 @@ "dev": true, "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -18200,6 +18238,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -19834,6 +19873,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21001,6 +21041,7 @@ "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -21173,7 +21214,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/seek-bzip": { @@ -23049,7 +23090,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "2.2.1", @@ -23181,6 +23223,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23488,6 +23531,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -24895,6 +24939,10 @@ "version": "14.9.0", "license": "MIT", "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.26.5", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@import-maps/resolve": "^2.0.0", "ajv": "^8.11.2", "ajv-errors": "^3.0.0", @@ -24918,12 +24966,15 @@ }, "devDependencies": { "@netlify/edge-functions-bootstrap": "^3.1.0", + "@types/babel__generator": "^7.27.0", + "@types/babel__traverse": "^7.28.0", "@types/node": "^18.19.111", "@types/semver": "^7.3.9", "@vitest/coverage-v8": "^3.0.0", "archiver": "^7.0.0", "chalk": "^5.4.0", "cpy": "^11.1.0", + "dedent": "^1.5.3", "nock": "^14.0.0", "npm-run-all2": "^6.0.0", "typescript": "^5.0.0", diff --git a/packages/edge-bundler/node/bundler.test.ts b/packages/edge-bundler/node/bundler.test.ts index b354cb333b..6726543ea8 100644 --- a/packages/edge-bundler/node/bundler.test.ts +++ b/packages/edge-bundler/node/bundler.test.ts @@ -855,6 +855,26 @@ describe.skipIf(lt(denoVersion, '2.4.3'))( await rm(vendorDirectory.path, { force: true, recursive: true }) }) + test('Tarball bundling succeeds when edge functions use import assertions', async () => { + const fixtures = ['with_deno_1x_features', 'with_import_assertions'] as const + + for (const fixtureName of fixtures) { + const { basePath, cleanup, distPath } = await useFixture(fixtureName, { copyDirectory: true }) + const sourceDirectory = join(basePath, 'functions') + + await expect( + bundle([sourceDirectory], distPath, [], { + basePath, + featureFlags: { + edge_bundler_generate_tarball: true, + }, + }), + ).resolves.toBeDefined() + + await cleanup() + } + }) + describe('Dry-run tarball generation flag enabled', () => { test('Logs success message when tarball generation succeeded', async () => { const systemLogger = vi.fn() diff --git a/packages/edge-bundler/node/formats/import-assertions-to-attributes.test.ts b/packages/edge-bundler/node/formats/import-assertions-to-attributes.test.ts new file mode 100644 index 0000000000..8fb974059c --- /dev/null +++ b/packages/edge-bundler/node/formats/import-assertions-to-attributes.test.ts @@ -0,0 +1,189 @@ +import { describe, test, expect } from 'vitest' +import dedent from 'dedent' +import { transformImportAssertionsToAttributes } from './import-assertions-to-attributes.js' + +describe('transformImportAssertionsToAttributes', () => { + const cases = [ + { + name: 'rewrites static import assertions to `with` syntax', + input: "import data from './data.json' assert { type: 'json' }", + expected: "import data from './data.json' with { type: 'json' };", + }, + { + name: 'rewrites export named assertions', + input: "export { data } from './data.json' assert { type: 'json' }", + expected: "export { data } from './data.json' with { type: 'json' };", + }, + { + name: 'rewrites export all assertions', + input: "export * from './data.json' assert { type: 'json' }", + expected: "export * from './data.json' with { type: 'json' };", + }, + { + name: 'rewrites dynamic import assertions with identifier keys', + input: "await import('./foo.json', { assert: { type: 'json' } })", + expected: dedent` + await import('./foo.json', { + with: { + type: 'json' + } + }); + `, + }, + { + name: 'rewrites dynamic import assertions with string literal keys', + input: 'await import("./foo.json", { "assert": { type: "json" } })', + expected: dedent` + await import("./foo.json", { + "with": { + type: "json" + } + }); + `, + }, + { + name: 'leaves dynamic imports without options untouched', + input: "await import('./foo.json');", + expected: "await import('./foo.json');", + }, + { + name: 'skips dynamic imports when options are not an object expression', + input: "await import('./foo.json', null);", + expected: "await import('./foo.json', null);", + }, + { + name: 'leaves static imports without assertions untouched', + input: "import data from './data.json';", + expected: "import data from './data.json';", + }, + { + name: 'leaves static imports already using `with` syntax untouched', + input: "import data from './data.json' with { type: 'json' };", + expected: "import data from './data.json' with { type: 'json' };", + }, + { + name: 'rewrites multiple import assertions', + input: "import data from './data.json' assert { type: 'json', foo: 'bar' }", + expected: "import data from './data.json' with { type: 'json', foo: 'bar' };", + }, + { + name: 'leaves export named without assertions untouched', + input: "export { data } from './data.json';", + expected: "export { data } from './data.json';", + }, + { + name: 'leaves export all without assertions untouched', + input: "export * from './data.json';", + expected: "export * from './data.json';", + }, + { + name: 'handles dynamic imports with `with` already present', + input: "await import('./foo.json', { with: { type: 'json' } });", + expected: dedent` + await import('./foo.json', { + with: { + type: 'json' + } + }); + `, + }, + { + name: 'preserves other properties alongside rewritten assert in dynamic imports', + input: "await import('./foo.json', { assert: { type: 'json' }, cache: true })", + expected: dedent` + await import('./foo.json', { + with: { + type: 'json' + }, + cache: true + }); + `, + }, + { + name: 'skips spread elements in dynamic import options', + input: "const opts = { type: 'json' }; await import('./foo.json', { ...opts });", + expected: dedent` + const opts = { + type: 'json' + }; + await import('./foo.json', { + ...opts + }); + `, + }, + { + name: 'handles TypeScript code with import assertions', + input: "import data from './data.json' assert { type: 'json' };\nconst x: string = data.name;", + expected: dedent` + import data from './data.json' with { type: 'json' }; + const x: string = data.name; + `, + }, + { + name: 'handles JSX code with import assertions', + input: "import data from './data.json' assert { type: 'json' };\nconst el =
{data.name}
;", + expected: dedent` + import data from './data.json' with { type: 'json' }; + const el =
{data.name}
; + `, + }, + { + name: 'handles multiple imports in the same file', + input: dedent` + import a from './a.json' assert { type: 'json' }; + import b from './b.json' assert { type: 'json' }; + import c from './c.js'; + `, + expected: dedent` + import a from './a.json' with { type: 'json' }; + import b from './b.json' with { type: 'json' }; + import c from './c.js'; + `, + }, + { + name: 'handles mixed static and dynamic imports', + input: dedent` + import data from './data.json' assert { type: 'json' }; + const other = await import('./other.json', { assert: { type: 'json' } }); + `, + expected: dedent` + import data from './data.json' with { type: 'json' }; + const other = await import('./other.json', { + with: { + type: 'json' + } + }); + `, + }, + { + name: 'does not modify non-assert properties in dynamic import options', + input: "await import('./foo.json', { cache: 'force-cache' });", + expected: dedent` + await import('./foo.json', { + cache: 'force-cache' + }); + `, + }, + { + name: 'handles computed property keys in dynamic import options (skips them)', + input: "const key = 'assert'; await import('./foo.json', { [key]: { type: 'json' } });", + expected: dedent` + const key = 'assert'; + await import('./foo.json', { + [key]: { + type: 'json' + } + }); + `, + }, + { + name: 'rewrites default export with assertions', + input: "export { default } from './data.json' assert { type: 'json' };", + expected: "export { default } from './data.json' with { type: 'json' };", + }, + ] as const + + test.each(cases)('$name', ({ input, expected }) => { + expect(transformImportAssertionsToAttributes(input)).toBe(expected) + }) +}) diff --git a/packages/edge-bundler/node/formats/import-assertions-to-attributes.ts b/packages/edge-bundler/node/formats/import-assertions-to-attributes.ts new file mode 100644 index 0000000000..2b6bd1df29 --- /dev/null +++ b/packages/edge-bundler/node/formats/import-assertions-to-attributes.ts @@ -0,0 +1,120 @@ +import { parse } from '@babel/parser' +import traverse, { type NodePath } from '@babel/traverse' +import generate, { type GeneratorResult } from '@babel/generator' +import { + isExpression, + isIdentifier, + isImport, + isObjectExpression, + isObjectProperty, + isStringLiteral, +} from '@babel/types' +import type { + CallExpression, + Expression, + ExportAllDeclaration, + ExportNamedDeclaration, + Identifier, + ImportAttribute, + ImportDeclaration, + ImportExpression, + ObjectExpression, + ObjectMember, + ObjectProperty, + SpreadElement, + StringLiteral, +} from '@babel/types' + +type HasImportAssertions = { + assertions?: ImportAttribute[] | null + attributes?: ImportAttribute[] | null +} + +type ImportExpressionWithOptions = ImportExpression & { + options?: Expression | null +} + +const isImportAttributesObject = (value: Expression | null | undefined): value is ObjectExpression => + isObjectExpression(value) + +const isImportAttributesProperty = ( + value: ObjectMember | SpreadElement, +): value is ObjectProperty & { key: Identifier | StringLiteral } => + isObjectProperty(value) && (isIdentifier(value.key) || isStringLiteral(value.key)) + +const rewriteImportOptions = (options: Expression | null | undefined): void => { + if (!isImportAttributesObject(options)) return + + for (const prop of options.properties) { + if (!isImportAttributesProperty(prop)) continue + + if (isIdentifier(prop.key) && prop.key.name === 'assert') { + prop.key.name = 'with' + } else if (isStringLiteral(prop.key) && prop.key.value === 'assert') { + prop.key.value = 'with' + } + } +} + +const moveAssertionsToAttributes = (node: HasImportAssertions): void => { + const { assertions, attributes } = node + if (!assertions?.length) return + + node.attributes = attributes?.length + ? // Preserve any existing attributes before appending the migrated assertions. + [...attributes, ...assertions] + : Array.from(assertions) + + node.assertions = [] +} + +/** + * Transform `assert` import attributes to `with` import attributes. + * + * - Static imports / re-exports: uses Babel's `assertions` -> `attributes` fields. + * - Dynamic imports: rewrites `{ assert: { ... } }` to `{ with: { ... } }`. + */ +export function transformImportAssertionsToAttributes(code: string): string { + const ast = parse(code, { + sourceType: 'module', + plugins: ['jsx', 'typescript', ['importAttributes', { deprecatedAssertSyntax: true }]], + }) + + traverse(ast, { + ImportDeclaration(path: NodePath) { + moveAssertionsToAttributes(path.node) + }, + + ExportNamedDeclaration(path: NodePath) { + moveAssertionsToAttributes(path.node) + }, + + ExportAllDeclaration(path: NodePath) { + moveAssertionsToAttributes(path.node) + }, + + ImportExpression(path: NodePath) { + rewriteImportOptions(path.node.options) + }, + + CallExpression(path: NodePath) { + if (!isImport(path.node.callee)) return + + const [, options] = path.node.arguments + + if (isExpression(options)) { + rewriteImportOptions(options) + } + }, + }) + + const output = generate( + ast, + { + importAttributesKeyword: 'with', + }, + code, + ) as GeneratorResult + + return output.code +} diff --git a/packages/edge-bundler/node/formats/tarball.ts b/packages/edge-bundler/node/formats/tarball.ts index 155bda6f59..c0190b0a46 100644 --- a/packages/edge-bundler/node/formats/tarball.ts +++ b/packages/edge-bundler/node/formats/tarball.ts @@ -1,6 +1,8 @@ import { promises as fs } from 'fs' import path from 'path' +import { transformImportAssertionsToAttributes } from './import-assertions-to-attributes.js' +import cpy from 'cpy' import commonPathPrefix from 'common-path-prefix' import * as tar from 'tar' import tmp from 'tmp-promise' @@ -43,7 +45,7 @@ export const bundle = async ({ importMap, vendorDirectory, }: BundleTarballOptions): Promise => { - const bundleDir = await tmp.dir({ unsafeCleanup: true }) + let bundleDir = await tmp.dir({ unsafeCleanup: true }) const cleanup = [bundleDir.cleanup] let denoDir = vendorDirectory ? path.join(vendorDirectory, 'deno_dir') : undefined @@ -80,23 +82,46 @@ export const bundle = async ({ manifest.functions[func.name] = getUnixPath(bundledPath) } - await deno.run( - [ - 'bundle', - '--import-map', - importMap.withNodeBuiltins().toDataURL(), - '--quiet', - '--code-splitting', - '--allow-import', - '--node-modules-dir=manual', - '--outdir', - bundleDir.path, - ...functions.map((func) => func.path), - ], - { - cwd: basePath, - }, - ) + const runBundle = async (entryPaths: string[]) => { + await deno.run( + [ + 'bundle', + '--import-map', + importMap.withNodeBuiltins().toDataURL(), + '--quiet', + '--code-splitting', + '--allow-import', + '--node-modules-dir=manual', + '--outdir', + bundleDir.path, + ...entryPaths, + ], + { + cwd: basePath, + }, + ) + } + + try { + await runBundle(entryPoints) + } catch (error) { + if (!isImportAssertionError(error)) { + throw error + } + + // Deno 2.x errors on import assertions, so retry with a temporary copy that rewrites them to `with`. + const compatSourceDir = await createCompatSourceCopy(commonPath) + cleanup.push(compatSourceDir.cleanup) + + bundleDir = await tmp.dir({ unsafeCleanup: true }) + cleanup.push(bundleDir.cleanup) + + const compatEntryPoints = entryPoints.map((entryPoint) => + path.join(compatSourceDir.path, path.relative(commonPath, entryPoint)), + ) + + await runBundle(compatEntryPoints) + } const manifestPath = path.join(bundleDir.path, '___netlify-edge-functions.json') const manifestContents = JSON.stringify(manifest) @@ -141,3 +166,45 @@ export const bundle = async ({ hash, } } + +const IMPORT_ASSERTION_ERROR_MESSAGES = ['Import assertions are deprecated', `Unexpected identifier 'assert'`] + +const isImportAssertionError = (error: unknown) => { + if (!(error instanceof Error)) { + return false + } + + const stderr = + typeof (error as { stderr?: unknown }).stderr === 'string' ? (error as { stderr?: unknown }).stderr : '' + const shortMessage = + typeof (error as { shortMessage?: unknown }).shortMessage === 'string' + ? (error as { shortMessage?: string }).shortMessage + : '' + const combinedMessage = [error.message, shortMessage, stderr].join('\n') + + return IMPORT_ASSERTION_ERROR_MESSAGES.some((message) => combinedMessage.includes(message)) +} + +async function* walk(dir: string): AsyncGenerator { + for (const entry of await fs.readdir(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) yield* walk(full) + else if (/\.(mjs|cjs|js|ts|tsx|jsx)$/.test(entry.name)) yield full + } +} + +const createCompatSourceCopy = async (commonPath: string) => { + const compatSourceDir = await tmp.dir({ unsafeCleanup: true }) + await cpy(path.join(commonPath, '**'), compatSourceDir.path, { + dot: true, + }) + + for await (const file of walk(compatSourceDir.path)) { + const code = await fs.readFile(file, 'utf8') + const next = transformImportAssertionsToAttributes(code) + if (next !== code) { + await fs.writeFile(file, next, 'utf8') + } + } + return compatSourceDir +} diff --git a/packages/edge-bundler/package.json b/packages/edge-bundler/package.json index a5b68ec54d..e2db142be5 100644 --- a/packages/edge-bundler/package.json +++ b/packages/edge-bundler/package.json @@ -43,12 +43,15 @@ }, "devDependencies": { "@netlify/edge-functions-bootstrap": "^3.1.0", + "@types/babel__generator": "^7.27.0", + "@types/babel__traverse": "^7.28.0", "@types/node": "^18.19.111", "@types/semver": "^7.3.9", "@vitest/coverage-v8": "^3.0.0", "archiver": "^7.0.0", "chalk": "^5.4.0", "cpy": "^11.1.0", + "dedent": "^1.5.3", "nock": "^14.0.0", "npm-run-all2": "^6.0.0", "typescript": "^5.0.0", @@ -58,6 +61,10 @@ "node": ">=18.14.0" }, "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.26.5", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@import-maps/resolve": "^2.0.0", "ajv": "^8.11.2", "ajv-errors": "^3.0.0", diff --git a/packages/edge-bundler/test/fixtures/with_import_assertions/functions/data.json b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/data.json new file mode 100644 index 0000000000..6439a1489d --- /dev/null +++ b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/data.json @@ -0,0 +1,3 @@ +{ + "value": "data" +} diff --git a/packages/edge-bundler/test/fixtures/with_import_assertions/functions/default.json b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/default.json new file mode 100644 index 0000000000..34fd7cd177 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/default.json @@ -0,0 +1,3 @@ +{ + "value": "default" +} diff --git a/packages/edge-bundler/test/fixtures/with_import_assertions/functions/exported.json b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/exported.json new file mode 100644 index 0000000000..96cf673580 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/exported.json @@ -0,0 +1,3 @@ +{ + "value": "exported" +} diff --git a/packages/edge-bundler/test/fixtures/with_import_assertions/functions/import-assertions.tsx b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/import-assertions.tsx new file mode 100644 index 0000000000..dc45cc65d9 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/with_import_assertions/functions/import-assertions.tsx @@ -0,0 +1,78 @@ +/** @jsx h */ + +function h(type: any, props: any, ...children: any[]) { + return { type, props: props || {}, children }; +} + +import staticAssertJson from './data.json' assert { type: 'json' } +const staticAssertJsonValue: string = staticAssertJson.value +void staticAssertJsonValue + +const jsxElement =
{staticAssertJsonValue}
+void jsxElement +import dataAlreadyWith from './data.json' with { type: 'json' } + +export * from './exported.json' assert { type: 'json' } +export { default as defaultAssertJson } from './default.json' assert { type: 'json' } +export { default as defaultWithJson } from './default.json' with { type: 'json' } + +const dynamicAssertIdentifier = await import('./data.json', { assert: { type: 'json' } }) +const dynamicWithIdentifier = await import('./data.json', { with: { type: 'json' } }) + +const dynamicAssertStringLiteral = await import('./data.json', { "assert": { type: "json" } }) +const dynamicWithStringLiteral = await import('./data.json', { "with": { type: "json" } }) + +const importAssertAlready = await import('./data.json', { assert: { type: 'json' }, foo: 'bar' }) +const importWithAlready = await import('./data.json', { with: { type: 'json' }, foo: 'bar' }) + +await import('./data.json', { assert: { type: 'json' } }) +await import('./data.json', { with: { type: 'json' } }) +const importNullOptions = async () => await import('./data.json', null as unknown as Record) +void importNullOptions + +const cacheOnlyImport = async () => await import('./data.json', { cache: 'force-cache' }) +void cacheOnlyImport + + +export default async () => { + const [ + assertIdentifier, + withIdentifier, + assertStringLiteral, + withStringLiteral, + assertAlready, + withAlready, + nullOptions, + cacheOnly, + alreadyWith + ] = await Promise.all([ + dynamicAssertIdentifier, + dynamicWithIdentifier, + dynamicAssertStringLiteral, + dynamicWithStringLiteral, + importAssertAlready, + importWithAlready, + importNullOptions(), + cacheOnlyImport(), + dataAlreadyWith + ]) + + const payload = { + assertIdentifier: assertIdentifier.value, + withIdentifier: withIdentifier.value, + assertStringLiteral: assertStringLiteral.value, + withStringLiteral: withStringLiteral.value, + assertAlready: assertAlready.value, + withAlready: withAlready.value, + nullOptions: nullOptions.value, + cacheOnly: cacheOnly.value, + alreadyWith: alreadyWith.value, + } + + return new Response(JSON.stringify(payload), { + headers: { + 'content-type': 'application/json', + }, + }) +} +