Skip to content

Commit aab5b2d

Browse files
authored
feat!: support package.json.exports field for npm dependencies used within Netlify Edge Functions (#6167)
The current edge-bundler attempts to import a dependency via it's package-name, for example if the edge-function was importing `@secret/magic/sdk/meow`, then the edge-bundler would attempt to import `@secret/magic` during it's bundling process. This hits an issue if the dependency being imported is using the packge.json.exports field and has not allowed the bare package-name to be imported. We can see this issue with package's such as `@modelcontextprotocol/sdk` which has an exports field defined like so: ``` "exports": { "./*": { "import": "./dist/esm/*", "require": "./dist/cjs/*" } }, ``` This patch reimplements the edge-bundler's logic so that it uses the same import as that used within the edge-function itself, going back to our previous example of importing `@secret/magic/sdk/meow` in the edge-function, the edge-bundler will now also import `@secret/magic/sdk/meow` during it's bundling process. This solves the issue mentioned about a package not exporting it's bare package-name. The patch has changed how edge-bundler parses edge-functions for their imports, it no longer uses vercel/nft as that package finds the files being used, but the files may not be directly importable due to a package.json.exports field definition. This has been replaced with `parse-imports`, which does a lexical scan of the files to find the import tokens. This approach is useful in that we can now detect all the different types of imports, such as if they are static or dynamic imports. For dynamic imports we can also detect if the import-specifier is a constant or not - this is useful because if it is a constant then we are able to bundle it, if it is not a constant, we could decide to report that the import may not work. The previous approach using vercel/nft did have the ability to find paths which are used but not imported, such as paths used within `fs.readFile` etc, vercel/nft would mark those as "assets". edge-bundler would keep track of the dependencies which had "assets" and mark them as containing 'extraneous files'. The CLI, if invoked with the `dev` command would then print out a note like this: >The following npm modules, which are directly or indirectly imported by an edge function, >may not be supported: dictionary. For more information, visit https://ntl.fyi/edge-functions-npm. This note was only shown for the `dev` command and not for `build` or `deploy`, a build and a deployment were still possible to take place. This patch has not implemented this note for the `dev` command, I don't think this is an issue.
1 parent 752f9a1 commit aab5b2d

File tree

12 files changed

+167
-107
lines changed

12 files changed

+167
-107
lines changed

package-lock.json

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/build/tests/monitor/snapshots/tests.js.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,7 +1588,7 @@ Generated by [AVA](https://avajs.dev).
15881588
────────────────────────────────────────────────────────────────␊
15891589
15901590
Error message␊
1591-
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"}␊
1591+
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/":{/"@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/":{}}"}␊
15921592
error: Uncaught (in promise) Error: Error: Could not find file: packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/file.ts␊
15931593
const ret = new Error(getStringFromWasm0(arg0, arg1));␊
15941594
^␊
@@ -1606,9 +1606,9 @@ Generated by [AVA](https://avajs.dev).
16061606
Error monitoring payload:␊
16071607
{␊
16081608
"errorClass": "functionsBundling",␊
1609-
"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 <anonymous> (wasm://wasm/HEXADECIMAL_ID:1:80)/n at <anonymous> (wasm://wasm/HEXADECIMAL_ID:1:80)/n at <anonymous> (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)",␊
1609+
"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//":{//"@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//":{}}/"}/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 <anonymous> (wasm://wasm/HEXADECIMAL_ID:1:80)/n at <anonymous> (wasm://wasm/HEXADECIMAL_ID:1:80)/n at <anonymous> (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)",␊
16101610
"context": "Bundling of edge function failed",␊
1611-
"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 <anonymous> /external/path at <anonymous> /external/path at <anonymous> /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path",␊
1611+
"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"/"}/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 <anonymous> /external/path at <anonymous> /external/path at <anonymous> /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path",␊
16121612
"severity": "info",␊
16131613
"unhandled": false,␊
16141614
"location": {␊
@@ -1619,7 +1619,7 @@ Generated by [AVA](https://avajs.dev).
16191619
"pluginPackageJson": false,␊
16201620
"BUILD_ID": "0",␊
16211621
"other": {␊
1622-
"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 <anonymous> /external/path at <anonymous> /external/path at <anonymous> /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path"␊
1622+
"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"/"}/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 <anonymous> /external/path at <anonymous> /external/path at <anonymous> /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path"␊
16231623
}␊
16241624
}`
16251625

-39 Bytes
Binary file not shown.

packages/edge-bundler/node/bundler.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,33 @@ test('Loads npm modules from bare specifiers', async () => {
506506
await rm(vendorDirectory.path, { force: true, recursive: true })
507507
})
508508

509+
test('Loads npm modules which use package.json.exports', async () => {
510+
const { basePath, cleanup, distPath } = await useFixture('imports_npm_module_exports')
511+
const sourceDirectory = join(basePath, 'functions')
512+
const declarations: Declaration[] = [
513+
{
514+
function: 'func1',
515+
path: '/func1',
516+
},
517+
]
518+
const vendorDirectory = await tmp.dir()
519+
520+
await bundle([sourceDirectory], distPath, declarations, {
521+
basePath,
522+
vendorDirectory: vendorDirectory.path,
523+
})
524+
525+
const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
526+
const manifest = JSON.parse(manifestFile)
527+
const bundlePath = join(distPath, manifest.bundles[0].asset)
528+
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path)
529+
530+
expect(func1).toBe('hello')
531+
532+
await cleanup()
533+
await rm(vendorDirectory.path, { force: true, recursive: true })
534+
})
535+
509536
test('Loads npm modules in a monorepo setup', async () => {
510537
const systemLogger = vi.fn()
511538
const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module')

0 commit comments

Comments
 (0)