diff --git a/lerna.json b/lerna.json index bdfd0386c650..0d7773a7c159 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.4.0-canary.31" + "version": "16.4.0-canary.32" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 5ec6f3869de2..cba79da5f1ca 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/devlow-bench/package.json b/packages/devlow-bench/package.json index 975c9765b37f..a67324f10aa3 100644 --- a/packages/devlow-bench/package.json +++ b/packages/devlow-bench/package.json @@ -1,6 +1,6 @@ { "name": "@vercel/devlow-bench", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "Benchmarking tool for the developer workflow", "repository": { "type": "git", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 6335d59fd00e..71572654a38c 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.4.0-canary.31", + "@next/eslint-plugin-next": "16.4.0-canary.32", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 300eed797af8..96f010580e99 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 8df9824eaa81..a78eb0b28f2e 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index bb8915030571..44a4e067b9d7 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 2c9b4373609d..227d7d11aca6 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 3499fe182163..39d34f894f1f 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 48d37af8a78a..cc3d72f986a1 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index daa642478fa9..d397518712af 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index 1f02e311dc3c..951d479bb5a8 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 501ae32f2312..cac33410c6f1 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 678d389f9a6d..802128fba60f 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 45a2fa4ef10b..a362d50cac07 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 163124c2063b..f8f1dad759c7 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index f31784109268..000b03bd85d8 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 936f9b4bca3c..7fa00e0f3cc8 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 2ff94d008a2c..d4766b565699 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "16.4.0-canary.31", + "@next/env": "16.4.0-canary.32", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -164,11 +164,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.42.5", "@napi-rs/triples": "1.2.0", - "@next/font": "16.4.0-canary.31", - "@next/polyfill-module": "16.4.0-canary.31", - "@next/polyfill-nomodule": "16.4.0-canary.31", - "@next/react-refresh-utils": "16.4.0-canary.31", - "@next/swc": "16.4.0-canary.31", + "@next/font": "16.4.0-canary.32", + "@next/polyfill-module": "16.4.0-canary.32", + "@next/polyfill-nomodule": "16.4.0-canary.32", + "@next/react-refresh-utils": "16.4.0-canary.32", + "@next/swc": "16.4.0-canary.32", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.61.0", "@rspack/core": "1.6.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 9ba08397915b..70d2c21fd1b1 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index f37fb3f76b7a..e3f25a9cf5b4 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.4.0-canary.31", + "next": "16.4.0-canary.32", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30fffaf7bc1c..0d3a0076d07e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1012,7 +1012,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1095,7 +1095,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-env '@swc/helpers': specifier: 0.5.23 @@ -1216,19 +1216,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../font '@next/polyfill-module': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../react-refresh-utils '@next/swc': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1944,7 +1944,7 @@ importers: devDependencies: '@napi-rs/cli': specifier: 3.7.2 - version: 3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) + version: 3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) cross-env: specifier: 6.0.3 version: 6.0.3 @@ -1971,7 +1971,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next outdent: specifier: 0.8.0 @@ -21842,7 +21842,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/cli@3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': + '@napi-rs/cli@3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': dependencies: '@inquirer/prompts': 8.5.2(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) '@napi-rs/cross-toolchain': 1.0.3 @@ -21857,7 +21857,7 @@ snapshots: semver: 7.8.5 typanion: 3.14.0 optionalDependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.9.2 transitivePeerDependencies: - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' diff --git a/test/e2e/app-dir/nx-handling/.npmrc b/test/e2e/app-dir/nx-handling/.npmrc deleted file mode 100644 index 521a9f7c0773..000000000000 --- a/test/e2e/app-dir/nx-handling/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps=true diff --git a/test/e2e/app-dir/nx-handling/nx-handling.test.ts b/test/e2e/app-dir/nx-handling/nx-handling.test.ts index 548144c0abd6..e94cbc8cc3fd 100644 --- a/test/e2e/app-dir/nx-handling/nx-handling.test.ts +++ b/test/e2e/app-dir/nx-handling/nx-handling.test.ts @@ -4,14 +4,12 @@ describe('nx-handling', () => { const { next } = nextTestSetup({ skipDeployment: true, files: __dirname, - installCommand: 'npm i', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + buildCommand: 'pnpm run build', + startCommand: isNextDev ? 'pnpm run dev' : 'pnpm run start', packageJson: { name: '@nx-next/source', version: '0.0.0', private: true, - packageManager: 'npm@10.9.2', scripts: { build: 'rm -rf dist; nx run next-nx-test:build', dev: 'nx run next-nx-test:dev', diff --git a/test/e2e/app-dir/nx-handling/package.json b/test/e2e/app-dir/nx-handling/package.json index 4c2d75109faf..f239bbd47ed6 100644 --- a/test/e2e/app-dir/nx-handling/package.json +++ b/test/e2e/app-dir/nx-handling/package.json @@ -6,7 +6,7 @@ "build": "nx run next-nx-test:build", "dev": "nx run next-nx-test:dev", "start": "nx run next-nx-test:serve:production", - "repro": "rm -rf dist && npm run build && npm run start" + "repro": "rm -rf dist && pnpm run build && pnpm run start" }, "private": true, "dependencies": { @@ -27,6 +27,5 @@ }, "workspaces": [ "apps/*" - ], - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + ] } diff --git a/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml b/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml new file mode 100644 index 000000000000..8ab3e17a0de1 --- /dev/null +++ b/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'apps/*' diff --git a/test/e2e/filesystem-cache/evict-after-snapshot.test.ts b/test/e2e/filesystem-cache/evict-after-snapshot.test.ts index a25e8900c97b..0eb6c9485e96 100644 --- a/test/e2e/filesystem-cache/evict-after-snapshot.test.ts +++ b/test/e2e/filesystem-cache/evict-after-snapshot.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' import { retry, waitFor } from 'next-test-utils' // Eviction requires the dev server (HMR) and persistent caching (Turbopack). @@ -8,25 +9,20 @@ import { retry, waitFor } from 'next-test-utils' // @force-gate !deploy // @force-gate dev describe('evict-after-snapshot', () => { - const envVars = [ - 'ENABLE_CACHING=1', - 'TURBO_ENGINE_IGNORE_DIRTY=1', - 'TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000', + const env = { + ENABLE_CACHING: '1', + TURBO_ENGINE_IGNORE_DIRTY: '1', + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // Persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold. - 'TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0', - 'ENABLE_EVICTION=1', - ].join(' ') + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + ENABLE_EVICTION: '1', + } const { next } = nextTestSetup({ files: __dirname, - packageJson: { - scripts: { - dev: `${envVars} next dev`, - }, - }, - installCommand: 'npm i', - startCommand: 'npm run dev', + overrideFiles: getPnpmRealpathWorkaround(), + env, }) async function waitForSnapshotAndEviction() { diff --git a/test/e2e/filesystem-cache/filesystem-cache.test.ts b/test/e2e/filesystem-cache/filesystem-cache.test.ts index f925e531ade5..031961ec3c33 100644 --- a/test/e2e/filesystem-cache/filesystem-cache.test.ts +++ b/test/e2e/filesystem-cache/filesystem-cache.test.ts @@ -5,6 +5,7 @@ import fs from 'fs/promises' import { existsSync } from 'fs' import path from 'path' import { parseTraceEvents } from '../../lib/parse-trace-file' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' async function getDirectorySize(dirPath: string): Promise { try { @@ -39,32 +40,22 @@ for (const cacheEnabled of [false, true]) { delete process.env.NEXT_PUBLIC_ENV_VAR }) - let envVars = [ - `ENABLE_CACHING=${cacheEnabled ? '1' : ''}`, + const env = { + ENABLE_CACHING: cacheEnabled ? '1' : '', // Make it easier to run in development, test directories are cleared between runs already so this is safe. - `TURBO_ENGINE_IGNORE_DIRTY=1`, + TURBO_ENGINE_IGNORE_DIRTY: '1', // decrease the idle timeout to make the test more reliable - `TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000`, + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold - `TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0`, - ].join(' ') + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + } const { next, isTurbopack } = nextTestSetup({ files: __dirname, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: `${envVars} next build`, - dev: `${envVars} next dev`, - start: 'next start', - }, - }, - // We need to use npm here as pnpms symlinks trigger a weird bug (kernel bug?) - installCommand: 'npm i', - // Next is always started with caching, but this can disable it for the followup restarts - buildCommand: `npm run build`, - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + overrideFiles: getPnpmRealpathWorkaround(), + // Pass the cache setting through every harness-managed build and restart. + env, }) beforeAll(() => { diff --git a/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts b/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts index 1842e6d8fbf7..40417fb14b59 100644 --- a/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts +++ b/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup, isNextDev } from 'e2e-utils' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' import { waitFor } from 'next-test-utils' import fs from 'fs/promises' import path from 'path' @@ -30,31 +31,22 @@ const STATS_RELATIVE_PATH = '.next/warm-restart-task-stats.json' // @force-gate !deploy // @force-gate turbopack describe('warm-restart task statistics', () => { - const env = [ - 'ENABLE_CACHING=1', - 'TURBO_ENGINE_IGNORE_DIRTY=1', - 'TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000', + const env = { + ENABLE_CACHING: '1', + TURBO_ENGINE_IGNORE_DIRTY: '1', + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // Persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold. - 'TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0', - `NEXT_TURBOPACK_TASK_STATISTICS=${STATS_RELATIVE_PATH}`, + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + NEXT_TURBOPACK_TASK_STATISTICS: STATS_RELATIVE_PATH, // Wait for turbo-tasks to persist the cache before the dev process exits. - 'NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN=1', - ].join(' ') + NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN: '1', + } const { next } = nextTestSetup({ files: __dirname, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: `${env} next build`, - dev: `${env} next dev`, - start: 'next start', - }, - }, - installCommand: 'npm i', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + overrideFiles: getPnpmRealpathWorkaround(), + env, }) beforeAll(() => { diff --git a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts index 18db69d87705..df105c740c6c 100644 --- a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts +++ b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts @@ -1,9 +1,12 @@ -import { isNextDev, nextTestSetup } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { renderViaHTTP } from 'next-test-utils' describe('handle-non-hoisted-swc-helpers', () => { const { next } = nextTestSetup({ files: { + '.npmrc': `# The helper move below needs real package directories, not pnpm symlinks. +node-linker=hoisted +`, 'pages/index.js': ` export default function Page() { return

hello world

@@ -20,18 +23,8 @@ describe('handle-non-hoisted-swc-helpers', () => { } `, }, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: 'next build', - dev: 'next dev', - start: 'next start', - }, - }, installCommand: - 'npm install; mkdir -p node_modules/next/node_modules/@swc; mv node_modules/@swc/helpers node_modules/next/node_modules/@swc/', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + 'pnpm install && mkdir -p node_modules/next/node_modules/@swc && mv node_modules/@swc/helpers node_modules/next/node_modules/@swc/', dependencies: {}, }) diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index 2836852667e5..fe6941ece7a7 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -139,6 +139,49 @@ async function applyWorkspaceOverrides(installDir, isolationRoot, overrides) { await fs.writeFile(workspaceFile, yaml.dump(workspaceConfig)) } +/** + * pnpm's hoisted linker does not expose the `@pkg+name@file` virtual-store + * path used by the default linker. Verify the exact local tarball through the + * lockfile instead. + * + * @param {string} installDir + * @param {string} packageName + * @param {string} expectedTarballPath + * @returns {Promise} + */ +async function lockfileResolvesLocalTarball( + installDir, + packageName, + expectedTarballPath +) { + const lockfile = /** @type {Record} */ ( + yaml.load( + await fs.readFile(path.join(installDir, 'pnpm-lock.yaml'), 'utf8') + ) + ) + const expectedRealpath = await fs.realpath(expectedTarballPath) + + for (const [key, pkg] of Object.entries(lockfile.packages || {})) { + const tarball = pkg?.resolution?.tarball + if ( + !key.startsWith(`${packageName}@file:`) || + typeof tarball !== 'string' || + !tarball.startsWith('file:') + ) { + continue + } + + const resolvedTarball = path.resolve( + installDir, + tarball.slice('file:'.length) + ) + if ((await fs.realpath(resolvedTarball)) === expectedRealpath) { + return true + } + } + return false +} + /** * @param {import('next/dist/trace').Span} parentSpan * @returns {Promise>} @@ -368,7 +411,9 @@ async function createNextInstall({ .traceAsyncFn(() => installDependencies(installDir, tmpDir)) // `@next/env` is a dependency of `next`, so it only resolves to the - // local tarball if the overrides were applied. + // local tarball if the overrides were applied. Every generic isolated + // install reaches this guard, but the lockfile fallback short-circuits + // off when the default linker exposes its virtual-store path. if (!combinedDependencies['@next/env']) { const envDir = await fs.realpath( path.join( @@ -376,7 +421,18 @@ async function createNextInstall({ '../@next/env' ) ) - if (!envDir.includes('@next+env@file')) { + const envTarballPath = pkgPaths.get('@next/env') + if ( + !envDir.includes('@next+env@file') && + !( + envTarballPath && + (await lockfileResolvesLocalTarball( + installDir, + '@next/env', + envTarballPath + )) + ) + ) { throw new Error( `@next/env resolved from the npm registry instead of the local tarball (${envDir}), ` + 'the workspace overrides were not applied to the install' diff --git a/test/lib/pnpm-realpath-workaround.ts b/test/lib/pnpm-realpath-workaround.ts new file mode 100644 index 000000000000..40b288521c8c --- /dev/null +++ b/test/lib/pnpm-realpath-workaround.ts @@ -0,0 +1,20 @@ +const PNPM_HOISTED_CONFIG = `# Work around the Node.js realpath bug fixed in 24.21.0 and 26.8.0. +# https://github.com/nodejs/node/pull/65113 +node-linker=hoisted +` + +function hasNodeRealpathFix(nodeVersion: string): boolean { + const [major, minor] = nodeVersion.split('.').map(Number) + return ( + (major === 24 && minor >= 21) || major > 26 || (major === 26 && minor >= 8) + ) +} + +export function getPnpmRealpathWorkaround( + nodeVersion = process.versions.node +): Record | undefined { + if (hasNodeRealpathFix(nodeVersion)) { + return undefined + } + return { '.npmrc': PNPM_HOISTED_CONFIG } +} diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts b/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts new file mode 100644 index 000000000000..58ac0b09de0b --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts @@ -0,0 +1,7 @@ +import Page from './page' +import './missing.css' + +// This reference back to the page puts the graph's explicit entry in a cycle. +void Page + +export const message = 'module graph cycle' diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx b/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx new file mode 100644 index 000000000000..7c3f422f0039 --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx b/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx new file mode 100644 index 000000000000..fc07a2615b9c --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx @@ -0,0 +1,5 @@ +import { message } from './dependency' + +export default function Page() { + return

{message}

+} diff --git a/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts b/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts new file mode 100644 index 000000000000..e4f5738a310b --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts b/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts new file mode 100644 index 000000000000..930b1a53f891 --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('turbopack-module-graph-cycle', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + it('reports compilation issues for a module cycle without panicking', async () => { + const { exitCode, cliOutput } = await next.build() + + expect(exitCode).toBe(1) + expect(cliOutput).toContain("Can't resolve './missing.css'") + expect(cliOutput).not.toContain('there must be a path to a root') + expect(cliOutput).not.toContain('Module graph is missing an entry point') + }) +}) diff --git a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs index eb1242fc4bac..74f9e149442b 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs @@ -5,10 +5,7 @@ use std::{collections::HashSet, mem::take, sync::Mutex}; use anyhow::Result; -use turbo_tasks::{ - Invalidator, ReadRef, Vc, get_invalidator, - unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, -}; +use turbo_tasks::{Invalidator, ReadRef, Vc, get_invalidator, with_turbo_tasks}; use turbo_tasks_testing::{Registration, register, run_once}; static REGISTRATION: Registration = register!(); @@ -16,7 +13,6 @@ static REGISTRATION: Registration = register!(); #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_read_ref() { run_once(®ISTRATION, async || { - unmark_top_level_task_may_leak_eventually_consistent_state(); let counter = Counter::cell(Counter { value: Mutex::new((0, Default::default())), }); @@ -38,7 +34,8 @@ async fn test_read_ref() { // However, `local_counter_value` will point to the value of `counter_value` // at the time it was turned into a trait reference (just like a `ReadRef` // would). - let local_counter_value = ReadRef::cell(counter_value.await?).get_value(); + let local_counter_value = + ReadRef::cell(counter_value.strongly_consistent().await?).get_value(); counter.await?.incr(); diff --git a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs index dd44ed3706e8..64748e1cecbf 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs @@ -5,10 +5,7 @@ use std::{collections::HashSet, mem::take, sync::Mutex}; use anyhow::Result; -use turbo_tasks::{ - Invalidator, TraitRef, Vc, get_invalidator, - unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, -}; +use turbo_tasks::{Invalidator, ResolvedVc, TraitRef, Vc, get_invalidator, with_turbo_tasks}; use turbo_tasks_testing::{Registration, register, run_once}; static REGISTRATION: Registration = register!(); @@ -16,8 +13,19 @@ static REGISTRATION: Registration = register!(); #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn trait_ref() { run_once(®ISTRATION, async || { - unmark_top_level_task_may_leak_eventually_consistent_state(); - let counter = Counter::cell(Counter { + #[turbo_tasks::function(operation, root)] + fn counter_trait_operation(counter: ResolvedVc) -> Vc> { + Vc::upcast(*counter) + } + + #[turbo_tasks::function(operation, root)] + fn counter_value_trait_operation( + counter: ResolvedVc, + ) -> Vc> { + Vc::upcast(counter.get_value()) + } + + let counter = Counter::resolved_cell(Counter { value: Mutex::new((0, Default::default())), }); @@ -32,8 +40,8 @@ async fn trait_ref() { assert_eq!(*counter_value.strongly_consistent().await?, 1); // `ref_counter` will still point to the same `counter` instance as `counter`. - let trait_ref_counter = Vc::upcast::>(counter) - .into_trait_ref() + let trait_ref_counter = counter_trait_operation(counter) + .read_trait_strongly_consistent() .await?; let ref_counter = TraitRef::cell(trait_ref_counter.clone()); let ref_counter_value = ref_counter.get_value(); @@ -42,8 +50,8 @@ async fn trait_ref() { // at the time it was turned into a trait reference (just like a `ReadRef` // would). let local_counter_value = TraitRef::cell( - Vc::upcast::>(counter_value) - .into_trait_ref() + counter_value_trait_operation(counter) + .read_trait_strongly_consistent() .await?, ) .get_value(); diff --git a/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs b/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs index b3e777954d5b..c1015926345a 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs @@ -270,17 +270,25 @@ pub async fn compute_binding_usage_info( ExportUsage::Passthrough { namespace_object_may_escape, } => { - if *namespace_object_may_escape { - partial_namespace_modules.insert(target); - } + // Passthrough edges always carry namespace provenance that already reached + // their parent. Some edges (for example a forwarded CommonJS namespace) + // additionally expose the target's original property names themselves. + let namespace_changed = if *namespace_object_may_escape + || partial_namespace_modules.contains(&parent) + { + partial_namespace_modules.insert(target) + } else { + false + }; let passthrough_usage = used_exports .get(&parent) .context("parent module must have usage info")? .clone(); - used_exports + let usage_changed = used_exports .entry(target) .or_default() - .add_usage_info(&passthrough_usage) + .add_usage_info(&passthrough_usage); + namespace_changed || usage_changed } export_usage => { if matches!(export_usage, ExportUsage::PartialNamespaceObject(_)) { @@ -292,8 +300,8 @@ pub async fn compute_binding_usage_info( } }; if changed || is_first_visit { - // First visit, or the used exports changed. This can cause more imports to get - // used downstream. + // First visit, or the used exports/namespace provenance changed. Either can + // cause more imports to become used downstream. Ok(GraphTraversalAction::Continue) } else { Ok(GraphTraversalAction::Skip) @@ -485,13 +493,21 @@ mod tests { use crate::resolve::ExportUsage; #[test] - fn resolved_usage_merges_exports() { - let mut usage = ModuleExportUsageInfo::Exports([rcstr!("first")].into_iter().collect()); - let additional = ModuleExportUsageInfo::Exports([rcstr!("second")].into_iter().collect()); + fn resolved_usage_join_is_monotonic() { + let mut usage = ModuleExportUsageInfo::Evaluation; + let first = ModuleExportUsageInfo::Exports([rcstr!("first")].into_iter().collect()); + let second = ModuleExportUsageInfo::Exports([rcstr!("second")].into_iter().collect()); - assert!(usage.add_usage_info(&additional)); + assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation)); + assert!(usage.add_usage_info(&first)); assert!(usage.is_export_used(&rcstr!("first"))); + assert!(!usage.add_usage_info(&first)); + assert!(usage.add_usage_info(&second)); assert!(usage.is_export_used(&rcstr!("second"))); + assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation)); + assert!(usage.add_usage_info(&ModuleExportUsageInfo::All)); + assert!(!usage.add_usage_info(&second)); + assert!(matches!(usage, ModuleExportUsageInfo::All)); } #[test] diff --git a/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs b/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs index 69e6008d6023..820340929a87 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs @@ -534,7 +534,9 @@ pub async fn compute_merged_modules(module_graph: Vc) -> Result, @@ -310,7 +314,26 @@ pub struct SingleModuleGraph { modules: FxHashMap>, NodeIndex>, #[turbo_tasks(trace_ignore)] - pub entries: GraphEntries, + entries: GraphEntries, + + /// Derived from `entries` and `modules`. Both are immutable after graph construction, and node + /// indices are stable because graph nodes are never removed. + #[turbo_tasks(debug_ignore, trace_ignore)] + #[bincode(skip, default = "OnceLock::new")] + entry_nodes: OnceLock>, +} + +impl Clone for SingleModuleGraph { + fn clone(&self) -> Self { + Self { + graph: self.graph.clone(), + number_of_modules: self.number_of_modules, + modules: self.modules.clone(), + entries: self.entries.clone(), + // Never carry derived state into a clone that might be modified before being stored. + entry_nodes: OnceLock::new(), + } + } } #[derive( @@ -502,6 +525,7 @@ impl SingleModuleGraph { number_of_modules, modules, entries: entries.clone(), + entry_nodes: OnceLock::new(), } .cell(); @@ -520,16 +544,23 @@ impl SingleModuleGraph { }) } - /// Returns true if the given module is in this graph and is an entry module + fn entry_nodes(&self) -> &FxHashSet { + self.entry_nodes.get_or_init(|| { + self.entries + .all_modules() + .filter_map(|module| self.modules.get(&module).copied()) + .collect() + }) + } + + /// Returns true if the given module is in this graph and is an entry module. + /// + /// Entry modules are tracked explicitly because an entry can have incoming edges when it is + /// part of a module cycle. pub fn has_entry_module(&self, module: ResolvedVc>) -> bool { - if let Some(index) = self.modules.get(&module) { - self.graph - .edges_directed(*index, Direction::Incoming) - .next() - .is_none() - } else { - false - } + self.modules + .get(&module) + .is_some_and(|index| self.entry_nodes().contains(index)) } /// Iterate over graph entry points @@ -712,6 +743,9 @@ impl ImportTracer for ModuleGraphImportTracer { let graph = &*self.await?.graph.await?; let reversed_graph = Reversed(&graph.graph.0); + // A graph entry may have incoming edges when it participates in a cycle, so roots cannot + // be inferred from graph topology alone. + let root_nodes = graph.entry_nodes(); return Ok(ImportTraces::cell(ImportTraces( modules .iter() @@ -721,11 +755,11 @@ impl ImportTracer for ModuleGraphImportTracer { // from a different graph than graph`. Just error out. bail!("inconsistent read?") }; - // compute the path from this index to a root of the graph. - let Some((_, path)) = petgraph::algo::astar( + // Compute the path from this index to an explicit root of the graph. + let path = match petgraph::algo::astar( &reversed_graph, module_idx, - |n| reversed_graph.neighbors(n).next().is_none(), + |n| root_nodes.contains(&n), // Edge weights |e| match e.weight().chunking_type { // Prefer following normal imports/requires when we can @@ -746,8 +780,31 @@ impl ImportTracer for ModuleGraphImportTracer { // solution would be a hand written implementation of dijkstras so we can // hoist redundant work out of this loop. |_| 0, - ) else { - unreachable!("there must be a path to a root"); + ) { + Some((_, path)) => path, + None => { + let module = graph + .graph + .node_weight(module_idx) + .expect("module index must be present in the graph") + .module(); + AnalyzeIssue::new( + IssueSeverity::Bug, + module.ident(), + Vc::cell(rcstr!("Module graph is missing an entry point")), + StyledString::Text(rcstr!( + "The module cannot reach any of the explicit entry points in \ + its module graph." + )) + .cell(), + None, + None, + ) + .to_resolved() + .await? + .emit(); + vec![module_idx] + } }; // Represent the path as a sequence of AssetIdents @@ -1995,12 +2052,160 @@ pub mod tests { use crate::{ asset::{Asset, AssetContent}, ident::AssetIdent, + issue::{CollectibleIssuesExt, IssueSeverity}, module::{Module, ModuleSideEffects}, module_graph::chunk_group_info::EntryHeuristics, reference::{ModuleReference, ModuleReferences}, resolve::ModuleResolveResult, }; + #[turbo_tasks::value(shared)] + struct ImportTraceTestResult { + has_entry: bool, + traces: Vec>, + missing_traces: Vec>, + } + + #[turbo_tasks::function(operation, root)] + async fn import_trace_test_operation(rootless: bool) -> Result> { + let fs = VirtualFileSystem::new_with_name(rcstr!("test")); + let root = fs.root().await?; + let repo = TestRepo::new( + &root, + [ + ("entry.js", vec!["dependency.js"]), + ("dependency.js", vec!["entry.js"]), + ], + ); + let entry = Vc::upcast::>(MockModule::new(root.join("entry.js")?, repo)) + .to_resolved() + .await?; + let graph = SingleModuleGraph::new_with_entries( + GraphEntries::resolved_cell(GraphEntries::new( + vec![ChunkGroupEntry::Entry { + modules: vec![entry], + heuristics: EntryHeuristics::default(), + }], + vec![], + )), + false, + false, + ) + .connect() + .to_resolved() + .await?; + let graph = if rootless { + // Initialize the source graph's cache before cloning to ensure clones reset derived + // state instead of retaining entry indices that could become stale after modification. + let _ = graph.await?.entry_nodes(); + let mut graph = (*graph.await?).clone(); + graph.entries = GraphEntries::default(); + graph.resolved_cell() + } else { + graph + }; + + let has_entry = graph.await?.has_entry_module(entry); + let tracer = ModuleGraphImportTracer::new(*graph); + let traces = tracer + .get_traces(root.join("dependency.js")?) + .await? + .0 + .iter() + .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect()) + .collect(); + let missing_traces = tracer + .get_traces(root.join("missing.js")?) + .await? + .0 + .iter() + .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect()) + .collect(); + + Ok(ImportTraceTestResult { + has_entry, + traces, + missing_traces, + } + .cell()) + } + + #[turbo_tasks::value(shared)] + struct ImportTraceIssues { + issues: Vec<(IssueSeverity, RcStr)>, + } + + #[turbo_tasks::function(operation, root)] + async fn import_trace_issues_operation( + trace_operation: OperationVc, + ) -> Result> { + let _ = trace_operation.connect().await?; + let issues = trace_operation + .peek_issues() + .iter() + .map(async |issue| { + let issue = issue.into_trait_ref().await?; + Ok(( + issue.severity(), + issue.title().await?.to_unstyled_string().into(), + )) + }) + .try_join() + .await?; + Ok(ImportTraceIssues { issues }.cell()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_import_trace_uses_explicit_entry_as_cycle_root() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + tt.run_once(async { + let result = import_trace_test_operation(false) + .read_strongly_consistent() + .await?; + assert!(result.has_entry); + assert_eq!( + result.traces, + vec![vec![rcstr!("dependency.js"), rcstr!("entry.js")]] + ); + assert!(result.missing_traces.is_empty()); + Ok(()) + }) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_cloned_rootless_import_trace_resets_cache_and_emits_bug_issue() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + tt.run_once(async { + let trace_operation = import_trace_test_operation(true); + let result = trace_operation.read_strongly_consistent().await?; + assert!(!result.has_entry); + assert_eq!(result.traces, vec![vec![rcstr!("dependency.js")]]); + assert!(result.missing_traces.is_empty()); + + let issues = import_trace_issues_operation(trace_operation) + .read_strongly_consistent() + .await?; + assert_eq!( + issues.issues, + vec![( + IssueSeverity::Bug, + rcstr!("Module graph is missing an entry point") + )] + ); + Ok(()) + }) + .await + .unwrap(); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_traverse_dfs_from_entries_diamond() { run_graph_test( diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index fdd9fdaba540..9e5e6c7d85c1 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -177,10 +177,13 @@ pub enum ExportUsage { /// Only side effects are used. Evaluation, /// Use the same exports that are used from the referencing module. This is used by transparent - /// module proxies that forward their export surface to another module. + /// module proxies and re-exports that forward their export surface to another module. + /// + /// Namespace provenance that reached the referencing module is forwarded independently of the + /// used names. This keeps multi-hop namespace reads safe for export-name mangling. Passthrough { - /// The forwarded exports are read through a namespace object, so their original names may - /// be observed even when the individual used names are known. + /// Whether this edge itself exposes a namespace object's original property names, even if + /// the referencing module was only consumed through statically known named exports. namespace_object_may_escape: bool, }, } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs index 5c7ccfc9adad..2156a209727b 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs @@ -32,7 +32,7 @@ use crate::{ SpecifiedModuleType, analyzer::{ Bump, ConstantString, ConstantValue, ObjectPart, - cjs_ast::is_global, + cjs_ast::{is_global, is_module_dot_exports}, graph::{AssignmentScope, AssignmentScopes, EvalContext}, is_unresolved, is_unresolved_id, }, @@ -1533,6 +1533,22 @@ impl Visit for Analyzer<'_> { node.visit_children_with(self); } + fn visit_assign_expr(&mut self, node: &AssignExpr) { + if node.op == AssignOp::Assign + && let AssignTarget::Simple(SimpleAssignTarget::Member(target)) = &node.left + && is_module_dot_exports(target, self.unresolved_mark) + && let Some(call) = as_require_call(&node.right, self.unresolved_mark) + { + self.data.cjs_imports.resolved.insert( + call.span.lo, + ExportUsage::Passthrough { + namespace_object_may_escape: true, + }, + ); + } + node.visit_children_with(self); + } + fn visit_expr_stmt(&mut self, node: &ExprStmt) { // A bare `require("…")` statement discards its result → evaluation only. if let Some(call) = as_require_call(&node.expr, self.unresolved_mark) { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs b/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs index ec74c434f843..142b0485be0e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs @@ -7,6 +7,7 @@ use swc_core::{ ArrayLit, EsVersion, Expr, KeyValueProp, Lit, ObjectLit, Prop, PropName, Regex, Str, }, parser::{Syntax, parse_file_as_expr}, + visit::fields::PropField, }, quote, }; @@ -37,12 +38,40 @@ impl ConstantValueCodeGen { _chunking_context: Vc>, ) -> Result { let value = self.value.clone(); + let mut visitors = Vec::new(); + let mut ast_path = self.path.0.clone(); - let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| { - *expr = value_to_expr(&value); - }); + if matches!( + ast_path.last(), + Some(swc_core::ecma::visit::AstParentKind::Prop( + PropField::Shorthand + )) + ) { + ast_path.pop(); + visitors.push(create_visitor!( + exact, + ast_path, + visit_mut_prop, + |prop: &mut Prop| { + if let Prop::Shorthand(ident) = prop { + *prop = Prop::KeyValue(KeyValueProp { + key: PropName::Ident(ident.clone().into()), + value: value_to_expr(&value).into(), + }); + } + } + )); + } else { + visitors.push(create_visitor!( + self.path, + visit_mut_expr, + |expr: &mut Expr| { + *expr = value_to_expr(&value); + } + )); + } - Ok(CodeGeneration::visitors(vec![visitor])) + Ok(CodeGeneration::visitors(visitors)) } } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index cb528c32f99c..f3a160464121 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -443,6 +443,23 @@ pub struct EsmAssetReference { extras: Option>, } +/// Construction options for an [`EsmAssetReference`]. +/// +/// The module, origin, and request stay as constructor arguments because they identify the +/// reference. The remaining behavior is named here so call sites don't depend on argument order. +pub struct EsmAssetReferenceOptions { + pub issue_source: IssueSource, + pub annotations: Option, + pub export_name: Option, + pub import_usage: ImportUsage, + pub import_externals: bool, + pub module_fragments_enabled: bool, + /// Explicit export-usage passthrough mode for syntax-driven forwarding. `Some(false)` forwards + /// used names without exposing the target namespace's original property names. + pub export_usage_passthrough: Option, + pub resolve_override: Option>>, +} + /// Optional extra state for an [`EsmAssetReference`] that is rarely present: the few values /// extracted from `ImportAnnotations` (the full `ImportAnnotations` — a `BTreeMap` plus several /// `Option`s — is not retained) plus a `resolve_override` from matched inner assets. @@ -470,17 +487,28 @@ struct EsmReferenceExtras { module_type: Option, /// The chunking-type annotation (drives `chunking_type`). chunking_type: Option, - /// Whether the importing module's used exports should be forwarded to the target. - export_usage_passthrough: bool, + /// Whether the importing module's used exports should be forwarded to the target. The boolean + /// records whether this edge itself exposes the target namespace's original property names. + export_usage_passthrough: Option, /// A module to resolve to directly, bypassing resolution (from a matched inner asset). resolve_override: Option>>, } +fn merge_export_usage_passthrough( + explicit: Option, + annotation_passthrough: bool, +) -> Option { + // An annotation is an explicit request to expose the namespace's original names, so it must + // not be weakened by a syntax-driven passthrough mode on the same reference. + annotation_passthrough.then_some(true).or(explicit) +} + impl EsmReferenceExtras { /// Builds the extras from import annotations and a resolve override, returning `None` (rather /// than an all-empty box) when nothing relevant is present — the common case. fn new( annotations: Option<&ImportAnnotations>, + export_usage_passthrough: Option, resolve_override: Option>>, ) -> Option> { let extras = EsmReferenceExtras { @@ -491,28 +519,59 @@ impl EsmReferenceExtras { .and_then(|a| a.module_type()) .map(|m| RcStr::from(&*m.to_string_lossy())), chunking_type: annotations.and_then(|a| a.chunking_type()), - export_usage_passthrough: annotations.is_some_and(|a| a.export_usage_passthrough()), + export_usage_passthrough: merge_export_usage_passthrough( + export_usage_passthrough, + annotations.is_some_and(|a| a.export_usage_passthrough()), + ), resolve_override, }; (extras != EsmReferenceExtras::default()).then(|| Box::new(extras)) } } +#[cfg(test)] +mod tests { + use super::merge_export_usage_passthrough; + + #[test] + fn annotation_passthrough_cannot_be_weakened() { + assert_eq!(merge_export_usage_passthrough(None, false), None); + assert_eq!( + merge_export_usage_passthrough(Some(false), false), + Some(false) + ); + assert_eq!( + merge_export_usage_passthrough(Some(true), false), + Some(true) + ); + assert_eq!(merge_export_usage_passthrough(None, true), Some(true)); + assert_eq!( + merge_export_usage_passthrough(Some(false), true), + Some(true) + ); + assert_eq!(merge_export_usage_passthrough(Some(true), true), Some(true)); + } +} + impl EsmAssetReference { - #[allow(clippy::too_many_arguments)] async fn new_inner( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, is_pure_import: bool, ) -> Result { + let EsmAssetReferenceOptions { + issue_source, + annotations, + export_name, + import_usage, + import_externals, + module_fragments_enabled, + export_usage_passthrough, + resolve_override, + } = options; + // Apply any annotation-driven transition eagerly so the stored origin is final and the // `annotations` don't need to be retained on the reference. let origin = if let Some(transition) = annotations.as_ref().and_then(|a| a.transition()) { @@ -534,64 +593,34 @@ impl EsmAssetReference { import_externals, module_fragments_enabled, is_pure_import, - extras: EsmReferenceExtras::new(annotations.as_ref(), resolve_override), + extras: EsmReferenceExtras::new( + annotations.as_ref(), + export_usage_passthrough, + resolve_override, + ), }) } - #[allow(clippy::too_many_arguments)] pub async fn new( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, ) -> Result { Self::new_inner( - module, - origin, - request, - issue_source, - annotations, - export_name, - import_usage, - import_externals, - module_fragments_enabled, - resolve_override, - /* is_pure_import */ false, + module, origin, request, options, /* is_pure_import */ false, ) .await } - #[allow(clippy::too_many_arguments)] pub async fn new_pure( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, ) -> Result { Self::new_inner( - module, - origin, - request, - issue_source, - annotations, - export_name, - import_usage, - import_externals, - module_fragments_enabled, - resolve_override, - /* is_pure_import */ true, + module, origin, request, options, /* is_pure_import */ true, ) .await } @@ -739,22 +768,22 @@ impl ModuleReference for EsmAssetReference { } fn binding_usage(&self) -> BindingUsage { + let export_usage_passthrough = self + .extras + .as_deref() + .and_then(|extras| extras.export_usage_passthrough); BindingUsage { import: self.import_usage.clone(), - export: match &self.export_name { + export: match (&self.export_name, export_usage_passthrough) { // Evaluation references preserve their side-effect-only semantics even when the // corresponding import forwards export usage. - Some(ModulePart::Evaluation) => ExportUsage::Evaluation, - _ if self - .extras - .as_deref() - .is_some_and(|extras| extras.export_usage_passthrough) => - { - ExportUsage::Passthrough { - namespace_object_may_escape: true, - } + (Some(ModulePart::Evaluation), _) => ExportUsage::Evaluation, + (_, Some(namespace_object_may_escape)) => ExportUsage::Passthrough { + namespace_object_may_escape, + }, + (Some(ModulePart::Export(export_name)), _) => { + ExportUsage::Named(export_name.clone()) } - Some(ModulePart::Export(export_name)) => ExportUsage::Named(export_name.clone()), _ => ExportUsage::All, }, } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs index 96fe9eb88a7d..c118ca5dbc35 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod module_item; pub(crate) mod url; pub use self::{ - base::EsmAssetReference, + base::{EsmAssetReference, EsmAssetReferenceOptions}, binding::EsmBinding, dynamic::EsmAsyncAssetReference, export::{EsmExport, EsmExports, FoundExportType, Liveness}, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs index 3f114f8fe65a..36b8761bcc06 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs @@ -1,4 +1,5 @@ use anyhow::{Result, bail}; +use rustc_hash::FxHashSet; use swc_core::{ common::source_map::SmallPos, ecma::ast::{Expr, Ident, ImportDecl, MemberProp, Program, Stmt}, @@ -20,7 +21,7 @@ use crate::{ parse::ParseResult, references::{ TURBOPACK_HELPER_WTF8, - esm::{EsmAssetReference, EsmExports}, + esm::{EsmAssetReference, EsmAssetReferenceOptions, EsmExports}, type_issue::SpecifiedModuleTypeIssue, }, runtime_functions::{TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE}, @@ -80,6 +81,8 @@ pub async fn compute_ecmascript_module_exports( let mut esm_reexport_reference_idxs: Vec = vec![]; let mut esm_evaluation_reference_idxs: Vec = vec![]; + let namespace_reexports: FxHashSet = + eval_context.imports.reexport_namespaces().collect(); let span = tracing::trace_span!("esm import references"); let import_references = async { @@ -100,41 +103,48 @@ pub async fn compute_ecmascript_module_exports( module, ResolvedVc::upcast(module), RcStr::from(&*r.module_path.to_string_lossy()), - IssueSource::from_swc_offsets(source, r.span.lo.to_u32(), r.span.hi.to_u32()), - r.annotations.as_ref().map(|a| (**a).clone()), - match &r.imported_symbol { - &ImportedSymbol::ModuleEvaluation => { - should_add_evaluation = true; - Some(ModulePart::evaluation()) - } - ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), - ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { - if !options.module_fragments_enabled { - bail!( - "Internal imports only exist in reexports only mode when \ - importing {:?} from {}", - r.imported_symbol, - r.module_path.to_string_lossy() - ); - } - if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { + EsmAssetReferenceOptions { + issue_source: IssueSource::from_swc_offsets( + source, + r.span.lo.to_u32(), + r.span.hi.to_u32(), + ), + annotations: r.annotations.as_ref().map(|a| (**a).clone()), + export_name: match &r.imported_symbol { + &ImportedSymbol::ModuleEvaluation => { should_add_evaluation = true; + Some(ModulePart::evaluation()) } - Some(ModulePart::internal(*part_id)) - } - ImportedSymbol::Exports => { - options.module_fragments_enabled.then(ModulePart::exports) - } + ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), + ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { + if !options.module_fragments_enabled { + bail!( + "Internal imports only exist in reexports only mode when \ + importing {:?} from {}", + r.imported_symbol, + r.module_path.to_string_lossy() + ); + } + if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { + should_add_evaluation = true; + } + Some(ModulePart::internal(*part_id)) + } + ImportedSymbol::Exports => { + options.module_fragments_enabled.then(ModulePart::exports) + } + }, + import_usage: eval_context + .imports + .import_usage + .get(&i) + .cloned() + .unwrap_or_default(), + import_externals, + module_fragments_enabled: options.module_fragments_enabled, + export_usage_passthrough: namespace_reexports.contains(&i).then_some(false), + resolve_override, }, - eval_context - .imports - .import_usage - .get(&i) - .cloned() - .unwrap_or_default(), - import_externals, - options.module_fragments_enabled, - resolve_override, ) .await? .resolved_cell(); diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 7b9dfe8b0f3a..2ce234390ebd 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -134,9 +134,9 @@ use crate::{ dynamic_expression::DynamicExpression, emit_collect::{CollectReference, EmitReference}, esm::{ - EsmAssetReference, EsmAsyncAssetReference, EsmBinding, ImportMetaBinding, - ImportMetaRef, UrlAssetReference, UrlRewriteBehavior, base::EsmAssetReferences, - module_id::EsmModuleIdAssetReference, + EsmAssetReference, EsmAssetReferenceOptions, EsmAsyncAssetReference, EsmBinding, + ImportMetaBinding, ImportMetaRef, UrlAssetReference, UrlRewriteBehavior, + base::EsmAssetReferences, module_id::EsmModuleIdAssetReference, }, exports::{EcmascriptExportsAnalysis, compute_ecmascript_module_exports}, exports_info::{ExportsInfoBinding, ExportsInfoRef}, @@ -3739,19 +3739,22 @@ async fn handle_free_var_reference( state.origin }, request.clone(), - IssueSource::from_swc_offsets( - state.source, - span.lo.to_u32(), - span.hi.to_u32(), - ), - Default::default(), - export.clone().map(ModulePart::export), - // TODO This could be optimized. E.g. referencing `Buffer` in some top - // level function could set ImportUsage properly here - ImportUsage::TopLevel, - state.import_externals, - state.module_fragments_enabled, - None, + EsmAssetReferenceOptions { + issue_source: IssueSource::from_swc_offsets( + state.source, + span.lo.to_u32(), + span.hi.to_u32(), + ), + annotations: Default::default(), + export_name: export.clone().map(ModulePart::export), + // TODO This could be optimized. E.g. referencing `Buffer` in some top + // level function could set ImportUsage properly here + import_usage: ImportUsage::TopLevel, + import_externals: state.import_externals, + module_fragments_enabled: state.module_fragments_enabled, + export_usage_passthrough: None, + resolve_override: None, + }, ) .await? .resolved_cell()) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js new file mode 100644 index 000000000000..1a3ca11a653e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js @@ -0,0 +1 @@ +export * from './forwarded-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js new file mode 100644 index 000000000000..eb310bb6014f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js @@ -0,0 +1,6 @@ +import * as forwarded from './forwarded-barrel' + +export function readForwardedNamespace() { + const { veryLongForwardedExportName } = forwarded + return veryLongForwardedExportName +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js new file mode 100644 index 000000000000..d40a6c78c50c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js @@ -0,0 +1 @@ +export const veryLongForwardedExportName = 'forwarded-value' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js index 63ca1d873642..5fe7b8447bff 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js @@ -1,4 +1,10 @@ import { getEnums } from './provider' +import { multiHopExportsInfo, readMultiHopDiamond } from './multi-hop-provider' +import { readForwardedNamespace } from './forwarded-provider' +import { + forwardedExportsInfo, + veryLongMangleableForwardedExportName, +} from './mangleable-forwarded-barrel' import { enumsNs } from './reexport' import { getCjs } from './cjs-provider' import { read } from './destr' @@ -47,6 +53,26 @@ it('should keep an escaped CommonJS namespace interop correct', () => { expect(ns.CJS_B).toBe('cjs-b') }) +it('should preserve namespace reads through an export-star forwarding edge', () => { + expect(readForwardedNamespace()).toBe('forwarded-value') +}) + +it('should preserve namespace names through a multi-hop diamond', () => { + expect(readMultiHopDiamond()).toEqual(['multi-hop', 'multi-hop']) + expect(multiHopExportsInfo.veryLongMultiHopExportName.canMangle).toBe(false) + expect(multiHopExportsInfo.veryLongMultiHopExportName.mangledName).toBeNull() +}) + +it('should still mangle ordinary named reads through an export-star forwarding edge', () => { + expect(veryLongMangleableForwardedExportName).toBe('mangleable-forwarded') + expect( + forwardedExportsInfo.veryLongMangleableForwardedExportName.canMangle + ).toBe(true) + expect( + forwardedExportsInfo.veryLongMangleableForwardedExportName.mangledName + ).not.toBe('veryLongMangleableForwardedExportName') +}) + it('should still mangle a sibling module that does not escape', () => { expect(someLongExportName).toBe('mangled-1') expect(anotherLongExportName).toBe('mangled-2') diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js new file mode 100644 index 000000000000..545ad5d0c607 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js @@ -0,0 +1 @@ +export * from './mangleable-forwarded-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js new file mode 100644 index 000000000000..c48fe7f665de --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js @@ -0,0 +1,6 @@ +export const veryLongMangleableForwardedExportName = 'mangleable-forwarded' + +export const forwardedExportsInfo = { + veryLongMangleableForwardedExportName: + __webpack_exports_info__.veryLongMangleableForwardedExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js new file mode 100644 index 000000000000..3521b680ed5e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js @@ -0,0 +1 @@ +export * from './multi-hop-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js new file mode 100644 index 000000000000..02f98bc5ae56 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js @@ -0,0 +1 @@ +export * from './multi-hop-1' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js new file mode 100644 index 000000000000..a313bc01aca2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js @@ -0,0 +1,9 @@ +import * as namespacePath from './multi-hop-2' +import { veryLongMultiHopExportName as namedPath } from './multi-hop-source' + +export { multiHopExportsInfo } from './multi-hop-source' + +export function readMultiHopDiamond() { + const { veryLongMultiHopExportName } = namespacePath + return [veryLongMultiHopExportName, namedPath] +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js new file mode 100644 index 000000000000..5d3ff238c65d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js @@ -0,0 +1,6 @@ +export const veryLongMultiHopExportName = 'multi-hop' + +export const multiHopExportsInfo = { + veryLongMultiHopExportName: + __webpack_exports_info__.veryLongMultiHopExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js new file mode 100644 index 000000000000..0ab554ce5155 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js @@ -0,0 +1,14 @@ +import { FIRST, SECOND } from 'shorthand-constants' + +function readConstants() { + return { FIRST, SECOND } +} + +it('inlines analyzer-aware constants in shorthand properties', () => { + expect(readConstants()).toEqual({ FIRST: 'first', SECOND: 'second' }) + + const source = readConstants.toString() + expect(source).toContain('FIRST:') + expect(source).toContain('SECOND:') + expect(source).toContain('TURBOPACK compile-time value') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js new file mode 100644 index 000000000000..838b5037fe4c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js @@ -0,0 +1,2 @@ +export const FIRST = 'first' +export const SECOND = 'second' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json new file mode 100644 index 000000000000..a43829151e14 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json @@ -0,0 +1,3 @@ +{ + "sideEffects": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json new file mode 100644 index 000000000000..08185d7558d7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json @@ -0,0 +1,5 @@ +{ + "scopeHoisting": false, + "mangleExportNames": false, + "crossModuleConstants": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js new file mode 100644 index 000000000000..9c091ae7e51d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js @@ -0,0 +1,3 @@ +export * from './source' +export const shadowed = 'local' +export default 'local-default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js new file mode 100644 index 000000000000..db20ad3c24d6 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js @@ -0,0 +1,8 @@ +import value, { shadowed, shadowedUsed, defaultUsed } from './barrel' + +it('does not forward locally shadowed or default names through export star', () => { + expect(value).toBe('local-default') + expect(shadowed).toBe('local') + expect(shadowedUsed).toBe(false) + expect(defaultUsed).toBe(false) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js new file mode 100644 index 000000000000..17df716f695f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js @@ -0,0 +1,5 @@ +export const shadowed = 'source' +export default 'source-default' + +export const shadowedUsed = __webpack_exports_info__.shadowed.used +export const defaultUsed = __webpack_exports_info__.default.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js new file mode 100644 index 000000000000..85e56520d9ba --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js @@ -0,0 +1 @@ +export * from './base' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-base.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/base.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-base.js rename to turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/base.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js new file mode 100644 index 000000000000..654e842c8aaf --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js @@ -0,0 +1,9 @@ +import { m, mUsed, nUsed } from './barrel' + +it('should keep narrowing exports through an `export *` barrel', () => { + expect(m).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(mUsed).toBe(true) + expect(nUsed).toBe(false) + } +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js index 04ea9d21c335..660a7408f37c 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js @@ -1,11 +1,6 @@ -it('A: should tree-shake through a 4-level CJS star-reexport chain', () => { - const d = require('./a-l4') - expect(d.a).toBe(1) - if (process.env.NODE_ENV === 'production') { - expect(d.aUsed).toBe(true) - expect(d.bUsed).toBe(false) // b is never read across the chain - } -}) +// Runtime semantics for these cases are covered by the enabled +// `reexport-forwarding-nested-runtime` fixture. This fixture remains skipped only because the +// used/unused precision assertions below are not implemented yet. it('B: should tree-shake a nested namespace reexported through CJS', () => { const m = require('./b-mid') @@ -25,20 +20,6 @@ it('C: should tree-shake a sub-namespace star-reexport (ids != [])', () => { } }) -it('D: should tree-shake a CJS star-reexport of an ESM `export *` barrel', () => { - const d = require('./d-cjs') - expect(d.m).toBe(1) - if (process.env.NODE_ENV === 'production') { - expect(d.mUsed).toBe(true) - expect(d.nUsed).toBe(false) - } -}) - -it('E: should handle circular CJS star-reexports at runtime', () => { - const one = require('./e-one') - expect(one.second).toBe(2) -}) - it('F: should tree-shake a depth-2 nested namespace reexported through CJS', () => { const m = require('./f-cjs') expect(m.mid.deep.x).toBe(1) @@ -48,19 +29,6 @@ it('F: should tree-shake a depth-2 nested namespace reexported through CJS', () } }) -it('G: should tree-shake a diamond of CJS reexports with disjoint usage', () => { - const a = require('./g-p1').a - const b = require('./g-p2').b - expect(a).toBe(1) - expect(b).toBe(2) - if (process.env.NODE_ENV === 'production') { - // each property is pulled through a different reexport path - expect(require('./g-p1').aUsed).toBe(true) - expect(require('./g-p2').bUsed).toBe(true) - expect(require('./g-p1').cUsed).toBe(false) // c used through neither path - } -}) - it('J: should tree-shake when ids unwrap a namespace after a star hop', () => { const m = require('./j-hop2') expect(m.w).toBe(1) @@ -69,10 +37,3 @@ it('J: should tree-shake when ids unwrap a namespace after a star hop', () => { expect(m.zUsed).toBe(false) } }) - -it('H: should resolve mutually circular CJS star-reexports at runtime', () => { - const a = require('./h-a') - expect(a.onlyB).toBe('B') - expect(a.alsoB).toBe('B2') - expect('onlyA' in a).toBe(false) // overwritten by module.exports = require("./h-b") -}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js new file mode 100644 index 000000000000..676890e46d86 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js @@ -0,0 +1,23 @@ +import sourceDefault, { named } from './forwarder' +import * as namespace from './forwarder' +import './side-effect-forwarder' + +it('preserves ESM default, named, and namespace interop through a CJS forwarder', () => { + expect(sourceDefault).toBe('default') + expect(named).toBe('named') + expect(namespace.named).toBe('named') + expect(namespace.other).toBe('other') + expect(namespace.default).toBe('default') +}) + +it('forwards opaque whole-namespace usage and evaluates the target once', () => { + const opaque = Object(require('./forwarder')) + expect(Object.keys(opaque)).toEqual( + expect.arrayContaining(['default', 'named', 'other']) + ) + expect(globalThis.__forwarded_side_effects).toBe(1) +}) + +it('preserves evaluation through a forwarder when no exports are read', () => { + expect(globalThis.__side_effect_only_forwarded).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js new file mode 100644 index 000000000000..8aee6d5ad46e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js @@ -0,0 +1 @@ +module.exports = require('./side-effect-source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js new file mode 100644 index 000000000000..93167d0434f2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js @@ -0,0 +1,4 @@ +globalThis.__side_effect_only_forwarded = + (globalThis.__side_effect_only_forwarded ?? 0) + 1 + +export const neverRead = 'never-read' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js new file mode 100644 index 000000000000..95f272b07c93 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js @@ -0,0 +1,6 @@ +globalThis.__forwarded_side_effects ??= 0 +globalThis.__forwarded_side_effects++ + +export const named = 'named' +export const other = 'other' +export default 'default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json new file mode 100644 index 000000000000..58db730e83cd --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js new file mode 100644 index 000000000000..76c965069bed --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js @@ -0,0 +1,7 @@ +import { used, usedInfo, unusedInfo } from './forwarder' + +it('supports an ESM named import through a CommonJS whole-module reexport', () => { + expect(used).toBe('used') + expect(usedInfo).toBe(true) + expect(unusedInfo).toBe(false) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js new file mode 100644 index 000000000000..e5ae8848e014 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js @@ -0,0 +1,5 @@ +export const used = 'used' +export const unused = 'unused' + +export const usedInfo = __webpack_exports_info__.used.used +export const unusedInfo = __webpack_exports_info__.unused.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js new file mode 100644 index 000000000000..c98fa3c2b952 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js @@ -0,0 +1,4 @@ +export const x = 1 +export const y = 2 +export const xUsed = __webpack_exports_info__.x.used +export const yUsed = __webpack_exports_info__.y.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js new file mode 100644 index 000000000000..5a247da56d51 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js @@ -0,0 +1 @@ +module.exports = require('./b-ns') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js new file mode 100644 index 000000000000..db62ca44fe99 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js @@ -0,0 +1 @@ +export * as inner from './b-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js new file mode 100644 index 000000000000..ce7273ae6136 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js @@ -0,0 +1,4 @@ +export const p = 1 +export const q = 2 +export const pUsed = __webpack_exports_info__.p.used +export const qUsed = __webpack_exports_info__.q.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js new file mode 100644 index 000000000000..50fdee61a9dd --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js @@ -0,0 +1 @@ +module.exports = require('./c-ns').bag diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js new file mode 100644 index 000000000000..243d43e208fe --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js @@ -0,0 +1 @@ +export * as bag from './c-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js new file mode 100644 index 000000000000..15446cb46fe1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js @@ -0,0 +1 @@ +module.exports = require('./f-outer') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js new file mode 100644 index 000000000000..0bf45b414466 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js @@ -0,0 +1 @@ +export * as deep from './f-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js new file mode 100644 index 000000000000..c98fa3c2b952 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js @@ -0,0 +1,4 @@ +export const x = 1 +export const y = 2 +export const xUsed = __webpack_exports_info__.x.used +export const yUsed = __webpack_exports_info__.y.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js new file mode 100644 index 000000000000..dc4b91342a1e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js @@ -0,0 +1 @@ +export * as mid from './f-inner' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js new file mode 100644 index 000000000000..2001fa74f63a --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js @@ -0,0 +1,15 @@ +it('preserves a nested namespace reexported through CJS', () => { + expect(require('./b-mid').inner.x).toBe(1) +}) + +it('preserves a namespace member assigned through CJS', () => { + expect(require('./c-mid').p).toBe(1) +}) + +it('preserves a depth-two nested namespace reexported through CJS', () => { + expect(require('./f-cjs').mid.deep.x).toBe(1) +}) + +it('preserves namespace unwrapping after a forwarding hop', () => { + expect(require('./j-hop2').w).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js new file mode 100644 index 000000000000..946d811250e8 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js @@ -0,0 +1 @@ +module.exports = require('./j-ns') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js new file mode 100644 index 000000000000..f669b25b82cf --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js @@ -0,0 +1 @@ +module.exports = require('./j-hop1').box diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js new file mode 100644 index 000000000000..c5184fcce3ab --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js @@ -0,0 +1,4 @@ +export const w = 1 +export const z = 2 +export const wUsed = __webpack_exports_info__.w.used +export const zUsed = __webpack_exports_info__.z.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js new file mode 100644 index 000000000000..a8ad4960868b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js @@ -0,0 +1 @@ +export * as box from './j-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js new file mode 100644 index 000000000000..676890e46d86 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js @@ -0,0 +1,23 @@ +import sourceDefault, { named } from './forwarder' +import * as namespace from './forwarder' +import './side-effect-forwarder' + +it('preserves ESM default, named, and namespace interop through a CJS forwarder', () => { + expect(sourceDefault).toBe('default') + expect(named).toBe('named') + expect(namespace.named).toBe('named') + expect(namespace.other).toBe('other') + expect(namespace.default).toBe('default') +}) + +it('forwards opaque whole-namespace usage and evaluates the target once', () => { + const opaque = Object(require('./forwarder')) + expect(Object.keys(opaque)).toEqual( + expect.arrayContaining(['default', 'named', 'other']) + ) + expect(globalThis.__forwarded_side_effects).toBe(1) +}) + +it('preserves evaluation through a forwarder when no exports are read', () => { + expect(globalThis.__side_effect_only_forwarded).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js new file mode 100644 index 000000000000..8aee6d5ad46e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js @@ -0,0 +1 @@ +module.exports = require('./side-effect-source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js new file mode 100644 index 000000000000..93167d0434f2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js @@ -0,0 +1,4 @@ +globalThis.__side_effect_only_forwarded = + (globalThis.__side_effect_only_forwarded ?? 0) + 1 + +export const neverRead = 'never-read' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js new file mode 100644 index 000000000000..95f272b07c93 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js @@ -0,0 +1,6 @@ +globalThis.__forwarded_side_effects ??= 0 +globalThis.__forwarded_side_effects++ + +export const named = 'named' +export const other = 'other' +export default 'default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js new file mode 100644 index 000000000000..3bfa0db43482 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js @@ -0,0 +1,2 @@ +module.exports = null +module.exports ??= require('./value') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js new file mode 100644 index 000000000000..aea9ffe3c074 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js @@ -0,0 +1,11 @@ +it('does not classify shadowed module and require bindings as forwarding', () => { + expect(require('./shadowed').value).toBe('shadowed') +}) + +it('preserves a require member assigned to module.exports', () => { + expect(require('./member').value).toBe('inner') +}) + +it('preserves non-simple module.exports assignments', () => { + expect(require('./compound').value).toBe('value') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js new file mode 100644 index 000000000000..e5715e61211c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js @@ -0,0 +1 @@ +module.exports = require('./value').inner diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js new file mode 100644 index 000000000000..072f11d48ac1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js @@ -0,0 +1,6 @@ +function create(module, require) { + module.exports = require('./ignored') + return module.exports +} + +module.exports = create({}, () => ({ value: 'shadowed' })) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js new file mode 100644 index 000000000000..b8bb20dc51b7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js @@ -0,0 +1 @@ +module.exports = { value: 'value', inner: { value: 'inner' } } diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l2.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l2.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l2.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l3.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l3.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l3.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l3.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l4.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l4.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l4.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l4.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-src.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-src.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-src.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-src.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-barrel.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-barrel.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-barrel.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js new file mode 100644 index 000000000000..4deb9f5985f4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js @@ -0,0 +1,4 @@ +export const m = 1 +export const n = 2 +export const mUsed = __webpack_exports_info__.m.used +export const nUsed = __webpack_exports_info__.n.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-cjs.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-cjs.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-cjs.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-cjs.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-one.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-one.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-one.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-one.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-two.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-two.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-two.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-two.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-leaf.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-leaf.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-leaf.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p1.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p1.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p1.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p1.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p2.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p2.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p2.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-a.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-a.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-a.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-a.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-b.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-b.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-b.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-b.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js new file mode 100644 index 000000000000..955c41cb31b4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js @@ -0,0 +1,42 @@ +it('A: should tree-shake through a 4-level CJS star-reexport chain', () => { + const d = require('./a-l4') + expect(d.a).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(d.aUsed).toBe(true) + expect(d.bUsed).toBe(false) // b is never read across the chain + } +}) + +it('D: should tree-shake a CJS star-reexport of an ESM `export *` barrel', () => { + const d = require('./d-cjs') + expect(d.m).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(d.mUsed).toBe(true) + expect(d.nUsed).toBe(false) + } +}) + +it('E: should handle circular CJS star-reexports at runtime', () => { + const one = require('./e-one') + expect(one.second).toBe(2) +}) + +it('G: should tree-shake a diamond of CJS reexports with disjoint usage', () => { + const a = require('./g-p1').a + const b = require('./g-p2').b + expect(a).toBe(1) + expect(b).toBe(2) + if (process.env.NODE_ENV === 'production') { + // each property is pulled through a different reexport path + expect(require('./g-p1').aUsed).toBe(true) + expect(require('./g-p2').bUsed).toBe(true) + expect(require('./g-p1').cUsed).toBe(false) // c used through neither path + } +}) + +it('H: should resolve mutually circular CJS star-reexports at runtime', () => { + const a = require('./h-a') + expect(a.onlyB).toBe('B') + expect(a.alsoB).toBe('B2') + expect('onlyA' in a).toBe(false) // overwritten by module.exports = require("./h-b") +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +}