From 691cd5870dddeac411f672c97eb1179333c2b03e Mon Sep 17 00:00:00 2001 From: jake champion Date: Thu, 3 Apr 2025 13:03:42 +0100 Subject: [PATCH] feat!: support package.json.exports field for npm dependencies used within Netlify Edge Functions 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. --- package-lock.json | 21 +- .../build/tests/monitor/snapshots/tests.js.md | 8 +- .../tests/monitor/snapshots/tests.js.snap | Bin 5012 -> 4973 bytes packages/edge-bundler/node/bundler.test.ts | 27 +++ .../edge-bundler/node/npm_dependencies.ts | 187 +++++++++--------- .../edge-bundler/node/server/server.test.ts | 8 +- packages/edge-bundler/node/server/server.ts | 4 - packages/edge-bundler/package.json | 2 +- .../functions/func1.ts | 5 + .../node_modules/@secret/magic/dist/hello.js | 3 + .../node_modules/@secret/magic/package.json | 6 + .../imports_npm_module_exports/package.json | 3 + 12 files changed, 167 insertions(+), 107 deletions(-) create mode 100644 packages/edge-bundler/test/fixtures/imports_npm_module_exports/functions/func1.ts create mode 100644 packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/dist/hello.js create mode 100644 packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/package.json create mode 100644 packages/edge-bundler/test/fixtures/imports_npm_module_exports/package.json diff --git a/package-lock.json b/package-lock.json index 271b433583..4e05e67f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22350,6 +22350,19 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "license": "Apache-2.0 AND MIT", + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/parse-json": { "version": "5.2.0", "license": "MIT", @@ -24201,6 +24214,12 @@ "node": ">=8" } }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "license": "ISC" + }, "node_modules/slice-ansi": { "version": "5.0.0", "dev": true, @@ -28312,7 +28331,6 @@ "license": "MIT", "dependencies": { "@import-maps/resolve": "^1.0.1", - "@vercel/nft": "0.27.7", "ajv": "^8.11.2", "ajv-errors": "^3.0.0", "better-ajv-errors": "^1.2.0", @@ -28328,6 +28346,7 @@ "node-stream-zip": "^1.15.0", "p-retry": "^5.1.1", "p-wait-for": "^5.0.0", + "parse-imports": "^2.2.1", "path-key": "^4.0.0", "semver": "^7.3.8", "tmp-promise": "^3.0.3", diff --git a/packages/build/tests/monitor/snapshots/tests.js.md b/packages/build/tests/monitor/snapshots/tests.js.md index 3830a8a710..061d23b038 100644 --- a/packages/build/tests/monitor/snapshots/tests.js.md +++ b/packages/build/tests/monitor/snapshots/tests.js.md @@ -1588,7 +1588,7 @@ Generated by [AVA](https://avajs.dev). ────────────────────────────────────────────────────────────────␊ ␊ Error message␊ - 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"}␊ + 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/":{}}"}␊ error: Uncaught (in promise) Error: Error: Could not find file: packages/build/tests/monitor/fixtures/edge_function_error/netlify/edge-functions/file.ts␊ const ret = new Error(getStringFromWasm0(arg0, arg1));␊ ^␊ @@ -1606,9 +1606,9 @@ 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 (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)",␊ + "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 (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",␊ + "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 /external/path at /external/path at /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path",␊ "severity": "info",␊ "unhandled": false,␊ "location": {␊ @@ -1619,7 +1619,7 @@ Generated by [AVA](https://avajs.dev). "pluginPackageJson": false,␊ "BUILD_ID": "0",␊ "other": {␊ - "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"␊ + "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 /external/path at /external/path at /external/path at __wbg_adapter_0 /external/path at real /external/path at eventLoopTick /external/path"␊ }␊ }` diff --git a/packages/build/tests/monitor/snapshots/tests.js.snap b/packages/build/tests/monitor/snapshots/tests.js.snap index c1aee46f958e9c56d4b55bab4a3394932002b8e4..00538540fa3753179c7ff29195760155b28e7f64 100644 GIT binary patch literal 4973 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 rz7K?hPvfEgTNtj{A-DA1e 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 diff --git a/packages/edge-bundler/node/bundler.test.ts b/packages/edge-bundler/node/bundler.test.ts index 4358bd7277..0883ac6396 100644 --- a/packages/edge-bundler/node/bundler.test.ts +++ b/packages/edge-bundler/node/bundler.test.ts @@ -506,6 +506,33 @@ test('Loads npm modules from bare specifiers', async () => { await rm(vendorDirectory.path, { force: true, recursive: true }) }) +test('Loads npm modules which use package.json.exports', async () => { + const { basePath, cleanup, distPath } = await useFixture('imports_npm_module_exports') + const sourceDirectory = join(basePath, 'functions') + const declarations: Declaration[] = [ + { + function: 'func1', + path: '/func1', + }, + ] + const vendorDirectory = await tmp.dir() + + await bundle([sourceDirectory], distPath, declarations, { + basePath, + vendorDirectory: vendorDirectory.path, + }) + + const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8') + const manifest = JSON.parse(manifestFile) + const bundlePath = join(distPath, manifest.bundles[0].asset) + const { func1 } = await runESZIP(bundlePath, vendorDirectory.path) + + expect(func1).toBe('hello') + + await cleanup() + await rm(vendorDirectory.path, { force: true, recursive: true }) +}) + test('Loads npm modules in a monorepo setup', async () => { const systemLogger = vi.fn() const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module') diff --git a/packages/edge-bundler/node/npm_dependencies.ts b/packages/edge-bundler/node/npm_dependencies.ts index f3e4aa6cbc..e57af5fd14 100644 --- a/packages/edge-bundler/node/npm_dependencies.ts +++ b/packages/edge-bundler/node/npm_dependencies.ts @@ -4,10 +4,9 @@ import path from 'path' import { fileURLToPath, pathToFileURL } from 'url' import { resolve, ParsedImportMap } from '@import-maps/resolve' -import { nodeFileTrace, resolve as nftResolve } from '@vercel/nft' import { build } from 'esbuild' import { findUp } from 'find-up' -import getPackageName from 'get-package-name' +import { parseImports } from 'parse-imports' import tmp from 'tmp-promise' import { ImportMap } from './import_map.js' @@ -16,6 +15,10 @@ import { pathsBetween } from './utils/fs.js' const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.cts', '.ctsx', '.mts', '.mtsx']) +const slugifyFileName = (specifier: string) => { + return specifier.replace(/\//g, '_') +} + const slugifyPackageName = (specifier: string) => { if (!specifier.startsWith('@')) return specifier const [scope, pkg] = specifier.split('/') @@ -70,9 +73,20 @@ const getTypesPath = async (packageJsonPath: string): Promise => { +function packageName(specifier: string) { + if (!specifier.startsWith('@')) return specifier.split('/')[0] + const [scope, pkg] = specifier.split('/') + return `${scope}/${pkg}` +} + +const safelyDetectTypes = async (pkg: string, basePath: string): Promise => { try { - return await getTypesPath(packageJsonPath) + const json = await findUp(`node_modules/${packageName(pkg)}/package.json`, { + cwd: basePath, + }) + if (json) { + return await getTypesPath(json) + } } catch { return undefined } @@ -105,101 +119,91 @@ interface GetNPMSpecifiersOptions { rootPath: string } +async function compileTypeScript(file: string): Promise { + const compiled = await build({ + bundle: false, + entryPoints: [file], + logLevel: 'silent', + platform: 'node', + write: false, + }) + + return compiled.outputFiles[0].text +} + +async function parseImportsForFile(file: string, rootPath: string) { + const source = TYPESCRIPT_EXTENSIONS.has(path.extname(file)) + ? await compileTypeScript(file) + : await fs.readFile(file, 'utf-8') + + return await parseImports(source, { + resolveFrom: rootPath, + }) +} + /** * Parses a set of functions and returns a list of specifiers that correspond * to npm modules. */ const getNPMSpecifiers = async ({ basePath, functions, importMap, environment, rootPath }: GetNPMSpecifiersOptions) => { const baseURL = pathToFileURL(basePath) - const { reasons } = await nodeFileTrace(functions, { - base: rootPath, - processCwd: basePath, - readFile: async (filePath: string) => { - // If this is a TypeScript file, we need to compile in before we can - // parse it. - if (TYPESCRIPT_EXTENSIONS.has(path.extname(filePath))) { - const compiled = await build({ - bundle: false, - entryPoints: [filePath], - logLevel: 'silent', - platform: 'node', - write: false, - }) - - return compiled.outputFiles[0].text - } - - return fs.readFile(filePath, 'utf8') - }, - resolve: async (specifier, ...args) => { - // Start by checking whether the specifier matches any import map defined - // by the user. - const { matched, resolvedImport } = resolve(specifier, importMap, baseURL) - - // If it does, the resolved import is the specifier we'll evaluate going - // forward. - if (matched && resolvedImport.protocol === 'file:') { - const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/') - - return nftResolve(newSpecifier, ...args) - } - - return nftResolve(specifier, ...args) - }, - }) const npmSpecifiers: { specifier: string; types?: string }[] = [] - const npmSpecifiersWithExtraneousFiles = new Set() - - for (const [filePath, reason] of reasons.entries()) { - const parents = [...reason.parents] - const isExtraneousFile = reason.type.every((type) => type === 'asset') - - // An extraneous file is a dependency that was traced by NFT and marked - // as not being statically imported. We can't process dynamic importing - // at runtime, so we gather the list of modules that may use these files - // so that we can warn users about this caveat. - if (isExtraneousFile) { - parents.forEach((path) => { - const specifier = getPackageName(path) - - if (specifier) { - npmSpecifiersWithExtraneousFiles.add(specifier) + for (const func of functions) { + const imports = await parseImportsForFile(func, rootPath) + + for (const i of imports) { + // The non-null assertion is required because typescript can not infer that `moduleSpecifier.value` can be narrowed to a string. + // The narrowing is possible because `moduleSpecifier.value` will always be a string when `moduleSpecifier.isConstant` is true. + const specifier = i.moduleSpecifier.isConstant ? i.moduleSpecifier.value! : i.moduleSpecifier.code + switch (i.moduleSpecifier.type) { + case 'absolute': { + npmSpecifiers.push( + ...(await getNPMSpecifiers({ basePath, functions: [specifier], importMap, environment, rootPath })), + ) + break } - }) - } - - // every dependency will have its `package.json` in `reasons` exactly once. - // by only looking at this file, we save us from doing duplicate work. - const isPackageJson = filePath.endsWith('package.json') - if (!isPackageJson) continue - - const packageName = getPackageName(filePath) - if (packageName === undefined) continue - - const isDirectDependency = parents.some((parentPath) => { - if (!parentPath.startsWith(`node_modules${path.sep}`)) return true - // typically, edge functions have no direct dependency on the `package.json` of a module. - // it's the impl files that depend on `package.json`, so we need to check the parents of - // the `package.json` file as well to see if the module is a direct dependency. - const parents = [...(reasons.get(parentPath)?.parents ?? [])] - return parents.some((parentPath) => !parentPath.startsWith(`node_modules${path.sep}`)) - }) - - // We're only interested in capturing the specifiers that are first-level - // dependencies. Because we'll bundle all modules in a subsequent step, - // any transitive dependencies will be handled then. - if (isDirectDependency) { - npmSpecifiers.push({ - specifier: packageName, - types: environment === 'development' ? await safelyDetectTypes(path.join(basePath, filePath)) : undefined, - }) + case 'relative': { + const filePath = path.join(path.dirname(func), specifier) + npmSpecifiers.push( + ...(await getNPMSpecifiers({ basePath, functions: [filePath], importMap, environment, rootPath })), + ) + break + } + case 'package': { + // node: prefixed imports are detected as packages instead of as builtins + // we don't want to try and bundle builtins so we ignore node: prefixed imports + if (specifier.startsWith('node:')) { + break + } + + const { matched, resolvedImport } = resolve(specifier, importMap, baseURL) + if (matched) { + if (resolvedImport.protocol === 'file:') { + const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/') + npmSpecifiers.push( + ...(await getNPMSpecifiers({ basePath, functions: [newSpecifier], importMap, environment, rootPath })), + ) + } + } else if (!resolvedImport?.protocol?.startsWith('http')) { + const t = await safelyDetectTypes(specifier, basePath) + npmSpecifiers.push({ + specifier: specifier, + types: t, + }) + } + break + } + case 'builtin': + case 'invalid': + case 'unknown': { + // We don't bundle these types of modules + break + } + } } } - return { - npmSpecifiers, - npmSpecifiersWithExtraneousFiles: [...npmSpecifiersWithExtraneousFiles], - } + return npmSpecifiers } interface VendorNPMSpecifiersOptions { @@ -231,7 +235,7 @@ export const vendorNPMSpecifiers = async ({ // Otherwise, create a random temporary directory. const temporaryDirectory = directory ? { path: directory } : await tmp.dir() - const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers({ + const npmSpecifiers = await getNPMSpecifiers({ basePath, functions, importMap: importMap.getContentsWithURLObjects(), @@ -244,8 +248,8 @@ export const vendorNPMSpecifiers = async ({ // specifier, and each of these files will become entry points to esbuild. const ops = await Promise.all( npmSpecifiers.map(async ({ specifier, types }) => { - const code = `import * as mod from "${specifier}"; export default mod.default; export * from "${specifier}";` - const filePath = path.join(temporaryDirectory.path, `bundled-${slugifyPackageName(specifier)}.js`) + const code = `import * as mod from "${specifier}";\nexport default mod.default;\nexport * from "${specifier}";` + const filePath = path.join(temporaryDirectory.path, `bundled-${slugifyFileName(specifier)}.js`) await fs.writeFile(filePath, code) @@ -341,7 +345,6 @@ export const vendorNPMSpecifiers = async ({ cleanup, directory: temporaryDirectory.path, importMap: newImportMap, - npmSpecifiersWithExtraneousFiles, outputFiles, } } diff --git a/packages/edge-bundler/node/server/server.test.ts b/packages/edge-bundler/node/server/server.test.ts index 3ce5816bc3..53d892b86a 100644 --- a/packages/edge-bundler/node/server/server.test.ts +++ b/packages/edge-bundler/node/server/server.test.ts @@ -47,7 +47,7 @@ test('Starts a server and serves requests for edge functions', async () => { importMapPaths, } - const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server( + const { features, functionsConfig, graph, success } = await server( functions, { very_secret_secret: 'i love netlify', @@ -57,7 +57,6 @@ test('Starts a server and serves requests for edge functions', async () => { expect(features).toEqual({ npmModules: true }) expect(success).toBe(true) 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) { @@ -101,7 +100,7 @@ test('Starts a server and serves requests for edge functions', async () => { const idBarrelFile = await readFile(join(servePath, 'bundled-id.js'), 'utf-8') expect(idBarrelFile).toContain(`/// `) - const identidadeBarrelFile = await readFile(join(servePath, 'bundled-pt-committee__identidade.js'), 'utf-8') + const identidadeBarrelFile = await readFile(join(servePath, 'bundled-@pt-committee_identidade.js'), 'utf-8') expect(identidadeBarrelFile).toContain( `/// `, ) @@ -139,7 +138,7 @@ test('Serves edge functions in a monorepo setup', async () => { importMapPaths, } - const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server( + const { features, functionsConfig, graph, success } = await server( functions, { very_secret_secret: 'i love netlify', @@ -150,7 +149,6 @@ test('Serves edge functions in a monorepo setup', async () => { expect(features).toEqual({ npmModules: true }) expect(success).toBe(true) expect(functionsConfig).toEqual([{ path: '/func1' }]) - expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1']) for (const key in functions) { const graphEntry = graph?.modules.some( diff --git a/packages/edge-bundler/node/server/server.ts b/packages/edge-bundler/node/server/server.ts index 0ec1a402e2..43f420bcc8 100644 --- a/packages/edge-bundler/node/server/server.ts +++ b/packages/edge-bundler/node/server/server.ts @@ -97,8 +97,6 @@ const prepareServer = ({ const importMap = new ImportMap() await importMap.addFiles(options.importMapPaths ?? [], logger) - const npmSpecifiersWithExtraneousFiles: string[] = [] - // we keep track of the files that are relevant to the user's code, so we can clean up leftovers from past executions later const relevantFiles = [stage2Path] @@ -115,7 +113,6 @@ const prepareServer = ({ if (vendor) { features.npmModules = true importMap.add(vendor.importMap) - npmSpecifiersWithExtraneousFiles.push(...vendor.npmSpecifiersWithExtraneousFiles) relevantFiles.push(...vendor.outputFiles) } @@ -165,7 +162,6 @@ const prepareServer = ({ features, functionsConfig, graph, - npmSpecifiersWithExtraneousFiles, success, } } diff --git a/packages/edge-bundler/package.json b/packages/edge-bundler/package.json index 515b29c3de..56920acfb7 100644 --- a/packages/edge-bundler/package.json +++ b/packages/edge-bundler/package.json @@ -58,7 +58,6 @@ }, "dependencies": { "@import-maps/resolve": "^1.0.1", - "@vercel/nft": "0.27.7", "ajv": "^8.11.2", "ajv-errors": "^3.0.0", "better-ajv-errors": "^1.2.0", @@ -74,6 +73,7 @@ "node-stream-zip": "^1.15.0", "p-retry": "^5.1.1", "p-wait-for": "^5.0.0", + "parse-imports": "^2.2.1", "path-key": "^4.0.0", "semver": "^7.3.8", "tmp-promise": "^3.0.3", diff --git a/packages/edge-bundler/test/fixtures/imports_npm_module_exports/functions/func1.ts b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/functions/func1.ts new file mode 100644 index 0000000000..c25fc996c3 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/functions/func1.ts @@ -0,0 +1,5 @@ +import hello from '@secret/magic/sub-path' + +export default async () => { + return new Response(hello()) +} diff --git a/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/dist/hello.js b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/dist/hello.js new file mode 100644 index 0000000000..85e31430e8 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/dist/hello.js @@ -0,0 +1,3 @@ +export default function hello() { + return 'hello' +} \ No newline at end of file diff --git a/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/package.json b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/package.json new file mode 100644 index 0000000000..d68422a1bc --- /dev/null +++ b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/node_modules/@secret/magic/package.json @@ -0,0 +1,6 @@ +{ + "name": "@secret/magic", + "exports": { + "./sub-path": "./dist/hello.js" + } +} \ No newline at end of file diff --git a/packages/edge-bundler/test/fixtures/imports_npm_module_exports/package.json b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/packages/edge-bundler/test/fixtures/imports_npm_module_exports/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}