diff --git a/.changeset/shy-cups-grin.md b/.changeset/shy-cups-grin.md new file mode 100644 index 000000000000..21eef56b7ab6 --- /dev/null +++ b/.changeset/shy-cups-grin.md @@ -0,0 +1,9 @@ +--- +"wrangler": minor +--- + +Update import resolution for files and package exports + +In an npm workspace environment, wrangler will now be able to successfully resolve package exports. + +Previously, wrangler would only be able to resolve modules in a relative `node_modules` directory and not the workspace root `node_modules` directory. diff --git a/fixtures/import-npm/README.md b/fixtures/import-npm/README.md new file mode 100644 index 000000000000..2db0d93bca63 --- /dev/null +++ b/fixtures/import-npm/README.md @@ -0,0 +1,3 @@ +# import-npm example + +This is an npm workspace within pnpm in order to test dependencies managed through npm can also be resolved by wrangler diff --git a/fixtures/import-npm/package-lock.json b/fixtures/import-npm/package-lock.json new file mode 100644 index 000000000000..7f3936d259bd --- /dev/null +++ b/fixtures/import-npm/package-lock.json @@ -0,0 +1,185 @@ +{ + "name": "import-npm", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "import-npm", + "workspaces": [ + "packages/*" + ] + }, + "../../packages/workers-tsconfig": { + "name": "@cloudflare/workers-tsconfig", + "version": "0.0.0", + "dev": true + }, + "../../packages/wrangler": { + "version": "3.90.0", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "workspace:*", + "@cloudflare/workers-shared": "workspace:*", + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@esbuild-plugins/node-modules-polyfill": "^0.2.2", + "blake3-wasm": "^2.1.5", + "chokidar": "^4.0.1", + "date-fns": "^4.1.0", + "esbuild": "0.17.19", + "itty-time": "^1.0.6", + "miniflare": "workspace:*", + "nanoid": "^3.3.3", + "path-to-regexp": "^6.3.0", + "resolve": "^1.22.8", + "selfsigned": "^2.0.1", + "source-map": "^0.6.1", + "unenv": "npm:unenv-nightly@2.0.0-20241121-161142-806b5c0", + "workerd": "1.20241106.1", + "xxhash-wasm": "^1.0.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "devDependencies": { + "@cloudflare/cli": "workspace:*", + "@cloudflare/eslint-config-worker": "workspace:*", + "@cloudflare/pages-shared": "workspace:^", + "@cloudflare/types": "6.18.4", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "^4.20241106.0", + "@cspotcode/source-map-support": "0.8.1", + "@iarna/toml": "^3.0.0", + "@microsoft/api-extractor": "^7.47.0", + "@sentry/node": "^7.86.0", + "@sentry/types": "^7.86.0", + "@sentry/utils": "^7.86.0", + "@types/body-parser": "^1.19.2", + "@types/command-exists": "^1.2.0", + "@types/express": "^4.17.13", + "@types/glob-to-regexp": "^0.4.1", + "@types/is-ci": "^3.0.0", + "@types/javascript-time-ago": "^2.0.3", + "@types/mime": "^3.0.4", + "@types/minimatch": "^5.1.2", + "@types/prompts": "^2.0.14", + "@types/resolve": "^1.20.6", + "@types/shell-quote": "^1.7.2", + "@types/signal-exit": "^3.0.1", + "@types/supports-color": "^8.1.1", + "@types/ws": "^8.5.7", + "@types/yargs": "^17.0.22", + "@vitest/ui": "catalog:default", + "@webcontainer/env": "^1.1.0", + "body-parser": "^1.20.0", + "chalk": "^5.2.0", + "cli-table3": "^0.6.3", + "cmd-shim": "^4.1.0", + "command-exists": "^1.2.9", + "concurrently": "^8.2.2", + "devtools-protocol": "^0.0.1182435", + "dotenv": "^16.0.0", + "execa": "^6.1.0", + "express": "^4.18.1", + "find-up": "^6.3.0", + "get-port": "^7.0.0", + "glob-to-regexp": "^0.4.1", + "http-terminator": "^3.2.0", + "https-proxy-agent": "7.0.2", + "ignore": "^5.2.0", + "is-ci": "^3.0.1", + "javascript-time-ago": "^2.5.4", + "md5-file": "5.0.0", + "mime": "^3.0.0", + "minimatch": "^5.1.0", + "mock-socket": "^9.3.1", + "msw": "2.4.3", + "open": "^8.4.0", + "p-queue": "^7.2.0", + "patch-console": "^1.0.0", + "pretty-bytes": "^6.0.0", + "prompts": "^2.4.2", + "semiver": "^1.1.0", + "shell-quote": "^1.8.1", + "signal-exit": "^3.0.7", + "strip-ansi": "^7.1.0", + "supports-color": "^9.2.2", + "timeago.js": "^4.0.2", + "ts-dedent": "^2.2.0", + "ts-json-schema-generator": "^1.5.0", + "undici": "catalog:default", + "update-check": "^1.5.4", + "vitest": "catalog:default", + "vitest-websocket-mock": "^0.4.0", + "ws": "^8.18.0", + "xdg-app-paths": "^8.3.0", + "yargs": "^17.7.2" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20241106.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "../import-wasm-static": {}, + "node_modules/@cloudflare/workers-tsconfig": { + "resolved": "../../packages/workers-tsconfig", + "link": true + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/import-example": { + "resolved": "packages/import-example", + "link": true + }, + "node_modules/import-wasm-static": { + "resolved": "../import-wasm-static", + "link": true + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/wrangler": { + "resolved": "../../packages/wrangler", + "link": true + }, + "packages/import-example": { + "dependencies": { + "import-wasm-static": "../../../../fixtures/import-wasm-static" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "../../../../packages/workers-tsconfig", + "undici": "^5.28.4", + "wrangler": "../../../../packages/wrangler" + } + } + } +} diff --git a/fixtures/import-npm/package.json b/fixtures/import-npm/package.json new file mode 100644 index 000000000000..eb8252f6b488 --- /dev/null +++ b/fixtures/import-npm/package.json @@ -0,0 +1,15 @@ +{ + "name": "import-npm", + "private": true, + "description": "", + "author": "", + "workspaces": [ + "packages/*" + ], + "scripts": { + "check:type": "rm -rf node_modules && npm install && npm run check:type --workspaces", + "test:ci": "npm install && npm run test:ci --workspaces", + "test:watch": "npm install && npm run test:watch --workspaces", + "type:tests": "rm -rf node_modules && npm install && npm run type:tests --workspaces" + } +} diff --git a/fixtures/import-npm/packages/import-example/package.json b/fixtures/import-npm/packages/import-example/package.json new file mode 100644 index 000000000000..7c364acc9d1d --- /dev/null +++ b/fixtures/import-npm/packages/import-example/package.json @@ -0,0 +1,21 @@ +{ + "name": "import-example", + "private": true, + "description": "", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "import-wasm-static": "../../../../fixtures/import-wasm-static" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "../../../../packages/workers-tsconfig", + "undici": "^5.28.4", + "wrangler": "../../../../packages/wrangler" + } +} diff --git a/fixtures/import-npm/packages/import-example/src/index.js b/fixtures/import-npm/packages/import-example/src/index.js new file mode 100644 index 000000000000..bf0f6d049d0d --- /dev/null +++ b/fixtures/import-npm/packages/import-example/src/index.js @@ -0,0 +1,19 @@ +// this is from the `import-wasm-static` fixture defined above +// and setup inside package.json to mimic an npm package +import multiply from "import-wasm-static/multiply.wasm"; +import otherMultiple from "import-wasm-static/wasm/not-exported.wasm"; + +export default { + async fetch(request) { + // just instantiate and return something + // we're really just testing the imports at the top of this file + const multiplyModule = await WebAssembly.instantiate(multiply); + const otherModule = await WebAssembly.instantiate(otherMultiple); + + const results = [ + multiplyModule.exports.multiply(7, 3), + otherModule.exports.multiply(7, 3), + ]; + return new Response(results.join(", ")); + }, +}; diff --git a/fixtures/import-npm/packages/import-example/tests/index.test.ts b/fixtures/import-npm/packages/import-example/tests/index.test.ts new file mode 100644 index 000000000000..d95e5578fc84 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tests/index.test.ts @@ -0,0 +1,26 @@ +import { resolve } from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../../../shared/src/run-wrangler-long-lived"; + +describe("wrangler correctly imports wasm files with npm resolution", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + // if the worker compiles, is running, and returns 21 (7 * 3) we can assume that the wasm module was imported correctly + it("responds", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toBe("21, 21"); + }); +}); diff --git a/fixtures/import-npm/packages/import-example/tests/tsconfig.json b/fixtures/import-npm/packages/import-example/tests/tsconfig.json new file mode 100644 index 000000000000..540c9d65408a --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts", "../../../../../node-types.d.ts"] +} diff --git a/fixtures/import-npm/packages/import-example/tsconfig.json b/fixtures/import-npm/packages/import-example/tsconfig.json new file mode 100644 index 000000000000..fbcafb8e45f2 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "CommonJS", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests", "../../../../node-types.d.ts"] +} diff --git a/fixtures/import-npm/packages/import-example/vitest.config.mts b/fixtures/import-npm/packages/import-example/vitest.config.mts new file mode 100644 index 000000000000..12e6a147fc9a --- /dev/null +++ b/fixtures/import-npm/packages/import-example/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/import-npm/packages/import-example/wrangler.toml b/fixtures/import-npm/packages/import-example/wrangler.toml new file mode 100644 index 000000000000..e79b15dd724a --- /dev/null +++ b/fixtures/import-npm/packages/import-example/wrangler.toml @@ -0,0 +1,4 @@ +name = "import-example" +compatibility_date = "2024-10-31" + +main = "src/index.js" diff --git a/fixtures/import-npm/turbo.json b/fixtures/import-npm/turbo.json new file mode 100644 index 000000000000..ac10a3dc52e8 --- /dev/null +++ b/fixtures/import-npm/turbo.json @@ -0,0 +1,11 @@ +{ + "extends": ["//"], + "tasks": { + "test": { + "dependsOn": ["wrangler#build"] + }, + "test:ci": { + "dependsOn": ["wrangler#build"] + } + } +} diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index df9671fdacf6..d2d85815f7b7 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -82,7 +82,6 @@ "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", "unenv": "npm:unenv-nightly@2.0.0-20241121-161142-806b5c0", diff --git a/packages/wrangler/src/__tests__/module-collection.test.ts b/packages/wrangler/src/__tests__/module-collection.test.ts deleted file mode 100644 index b12b0ea4ad2a..000000000000 --- a/packages/wrangler/src/__tests__/module-collection.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { extractPackageName } from "../deployment-bundle/module-collection"; - -describe("Module Collection", () => { - describe("extractPackageName", () => { - test.each` - importString | packageName - ${"wrangler"} | ${"wrangler"} - ${"wrangler/example"} | ${"wrangler"} - ${"wrangler/example.wasm"} | ${"wrangler"} - ${"@cloudflare/wrangler"} | ${"@cloudflare/wrangler"} - ${"@cloudflare/wrangler/example"} | ${"@cloudflare/wrangler"} - ${"@cloudflare/wrangler/example.wasm"} | ${"@cloudflare/wrangler"} - ${"./some/file"} | ${null} - ${"../some/file"} | ${null} - ${"/some/file"} | ${null} - `("$importString --> $packageName", ({ importString, packageName }) => { - expect(extractPackageName(importString)).toBe(packageName); - }); - }); -}); diff --git a/packages/wrangler/src/deployment-bundle/module-collection.ts b/packages/wrangler/src/deployment-bundle/module-collection.ts index 470540d8aab4..8087d0ac8a62 100644 --- a/packages/wrangler/src/deployment-bundle/module-collection.ts +++ b/packages/wrangler/src/deployment-bundle/module-collection.ts @@ -4,10 +4,8 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import globToRegExp from "glob-to-regexp"; import { sync as resolveSync } from "resolve"; -import { exports as resolveExports } from "resolve.exports"; import { UserError } from "../errors"; import { logger } from "../logger"; -import { getBuildConditions } from "./bundle"; import { findAdditionalModules, findAdditionalModuleWatchDirs, @@ -71,25 +69,6 @@ export const noopModuleCollector: ModuleCollector = { }, }; -// Extracts a package name from a string that may be a file path -// or a package name. Returns null if the string is not a valid -// Handles `wrangler`, `wrangler/example`, `wrangler/example.wasm`, -// `@cloudflare/wrangler`, `@cloudflare/wrangler/example`, etc. -export function extractPackageName(packagePath: string) { - if (packagePath.startsWith(".")) { - return null; - } - - const match = packagePath.match(/^(@[^/]+\/)?([^/]+)/); - - if (match) { - const scoped = match[1] || ""; - const packageName = match[2]; - return `${scoped}${packageName}`; - } - return null; -} - export function createModuleCollector(props: { entry: Entry; findAdditionalModules: boolean; @@ -262,6 +241,10 @@ export function createModuleCollector(props: { build.onResolve( { filter: globToRegExp(glob) }, async (args: esbuild.OnResolveArgs) => { + if (args.pluginData?.skip) { + return; + } + // take the file and massage it to a // transportable/manageable format @@ -284,46 +267,20 @@ export function createModuleCollector(props: { // Check if this file is possibly from an npm package // and if so, validate the import against the package.json exports // and resolve the file path to the correct file. - if (args.path.includes("/") && !args.path.startsWith(".")) { - // get npm package name from string, taking into account scoped packages - const packageName = extractPackageName(args.path); - if (!packageName) { - throw new Error( - `Unable to extract npm package name from ${args.path}` - ); - } - const packageJsonPath = path.join( - process.cwd(), - "node_modules", - packageName, - "package.json" - ); - // Try and read the npm package's package.json - // and then resolve the import against the package's exports - // and then finally override filePath if we find a match. - try { - const packageJson = JSON.parse( - await readFile(packageJsonPath, "utf8") - ); - const testResolved = resolveExports( - packageJson, - args.path.replace(`${packageName}/`, ""), - { - conditions: getBuildConditions(), - } - ); - if (testResolved) { - filePath = path.join( - process.cwd(), - "node_modules", - packageName, - testResolved[0] - ); - } - } catch (e) { - // We tried, now it'll just fall-through to the previous behaviour - // and ENOENT if the absolute file path doesn't exist. + try { + const resolved = await build.resolve(args.path, { + kind: "import-statement", + resolveDir: args.resolveDir, + pluginData: { + skip: true, + }, + }); + if (resolved.path) { + filePath = resolved.path; } + } catch (ex) { + // We tried, now it'll just fall-through to the previous behaviour + // and ENOENT if the absolute file path doesn't exist. } // Next try to resolve using the node module resolution algorithm diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 926be16eb345..9922bf22f96d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,8 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/import-npm: {} + fixtures/import-wasm-example: dependencies: import-wasm-static: @@ -1726,9 +1728,6 @@ importers: resolve: specifier: ^1.22.8 version: 1.22.8 - resolve.exports: - specifier: ^2.0.2 - version: 2.0.2 selfsigned: specifier: ^2.0.1 version: 2.1.1 @@ -7272,10 +7271,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.2: - resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} - engines: {node: '>=10'} - resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -14469,8 +14464,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.2: {} - resolve@1.22.8: dependencies: is-core-module: 2.13.0 @@ -15638,7 +15631,6 @@ snapshots: nanoid: 3.3.7 path-to-regexp: 6.3.0 resolve: 1.22.8 - resolve.exports: 2.0.2 selfsigned: 2.1.1 source-map: 0.6.1 unenv: unenv-nightly@2.0.0-20241111-080453-894aa31