diff --git a/e2e/next/src/next-generation.test.ts b/e2e/next/src/next-generation.test.ts index a5327a47ac0..6f6edb3da7d 100644 --- a/e2e/next/src/next-generation.test.ts +++ b/e2e/next/src/next-generation.test.ts @@ -1,6 +1,7 @@ import { checkFilesDoNotExist, checkFilesExist, + getSelectedPackageManager, readFile, runCLI, uniq, @@ -31,6 +32,24 @@ describe('Next.js Applications - Generation', () => { afterAll(() => cleanupNextTest()); + it('should record allowBuilds decisions for sharp and @swc/core on pnpm', () => { + if (getSelectedPackageManager() !== 'pnpm') { + return; + } + + const appName = uniq('app'); + runCLI( + `generate @nx/next:app ${appName} --no-interactive --unitTestRunner=none --e2eTestRunner=none` + ); + + // pnpm 11 refuses to install deps whose build scripts are neither allowed + // nor denied: next pulls in sharp and the swc custom server setup pulls + // in @swc/core, so the generators must have recorded a decision for both + const workspaceYaml = readFile('pnpm-workspace.yaml'); + expect(workspaceYaml).toContain('sharp: false'); + expect(workspaceYaml).toMatch(/['"]@swc\/core['"]: false/); + }, 300_000); + it('should support generating projects with the new name and root format', () => { const { proj } = setup; const appName = uniq('app1'); diff --git a/e2e/web/src/web.test.ts b/e2e/web/src/web.test.ts index a75186d7731..c9a2b1fd80d 100644 --- a/e2e/web/src/web.test.ts +++ b/e2e/web/src/web.test.ts @@ -3,6 +3,7 @@ import { checkFilesExist, cleanupProject, createFile, + getSelectedPackageManager, isNotWindows, killPorts, listFiles, @@ -435,3 +436,25 @@ function setPluginOption( ); }); } + +describe('Web Components Applications - pnpm build scripts', () => { + beforeAll(() => newProject({ packages: ['@nx/web', '@nx/vitest'] })); + afterAll(() => cleanupProject()); + + it('should record an allowBuilds decision for @swc/core on pnpm', () => { + if (getSelectedPackageManager() !== 'pnpm') { + return; + } + + const appName = uniq('app'); + runCLI( + `generate @nx/web:app apps/${appName} --bundler=none --compiler=swc --no-interactive --unitTestRunner=none --e2eTestRunner=none` + ); + + // pnpm 11 refuses to install deps whose build scripts are neither allowed + // nor denied, so the app generator must have recorded a decision + expect(readFile('pnpm-workspace.yaml')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }, 300_000); +}); diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index d0d1d2f2d19..f076da12234 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -10,6 +10,7 @@ import { updateJson, updateNxJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions'; import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; @@ -45,6 +46,21 @@ describe('app', () => { } }); + it('should record the build script decisions the angular build tooling pulls in', async () => { + await withPnpm(appTree, '11.2.2', () => + generateApp(appTree, 'my-app', { + e2eTestRunner: E2eTestRunner.None, + unitTestRunner: UnitTestRunner.None, + }) + ); + + const pnpmWorkspace = appTree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]?esbuild['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?lmdb['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?msgpackr-extract['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]@parcel\/watcher['"]: false/); + }); + it('should add angular dependencies', async () => { // ACT await generateApp(appTree); diff --git a/packages/angular/src/generators/application/application.ts b/packages/angular/src/generators/application/application.ts index 69416907da8..c6ad78222e4 100644 --- a/packages/angular/src/generators/application/application.ts +++ b/packages/angular/src/generators/application/application.ts @@ -16,6 +16,7 @@ import { assertSupportedAngularVersion } from '../../utils/assert-supported-angu import { convertToRspack } from '../convert-to-rspack/convert-to-rspack'; import { angularInitGenerator } from '../init/init'; import { setupSsr } from '../setup-ssr/setup-ssr'; +import { acknowledgeAngularBuildScripts } from '../utils/acknowledge-build-scripts'; import { ensureAngularDependencies } from '../utils/ensure-angular-dependencies'; import { assertNotUsingTsSolutionSetup } from '../utils/validations'; import { @@ -151,6 +152,7 @@ export async function applicationGenerator( devDependencies['less'] = packageVersions.lessVersion; } if (Object.keys(devDependencies).length) { + acknowledgeAngularBuildScripts(tree); addDependenciesToPackageJson(tree, {}, devDependencies, undefined, true); } } diff --git a/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.spec.ts b/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.spec.ts index a5db406c26a..0ed263aa330 100644 --- a/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.spec.ts +++ b/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.spec.ts @@ -1,3 +1,4 @@ +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { convertToRspack } from './convert-to-rspack'; import { @@ -20,6 +21,39 @@ jest.mock('@nx/devkit/internal', () => ({ })); describe('convert-to-rspack', () => { + it('should record the build script decisions @nx/angular-rspack pulls in', async () => { + const tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'app', { + root: 'apps/app', + projectType: 'application', + targets: { + build: { + executor: '@angular-devkit/build-angular:browser', + options: { + outputPath: 'dist/apps/app', + index: 'apps/app/src/index.html', + main: 'apps/app/src/main.ts', + tsConfig: 'apps/app/tsconfig.app.json', + }, + }, + }, + }); + writeJson(tree, 'apps/app/tsconfig.json', {}); + + await withPnpm(tree, '11.2.2', () => + convertToRspack(tree, { project: 'app' }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]?esbuild['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?lmdb['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?msgpackr-extract['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]@parcel\/watcher['"]: false/); + expect(pnpmWorkspace).toMatch(/['"]?core-js['"]?: false/); + // Patches `@angular/build` below 20.2.0, so it has to run. + expect(pnpmWorkspace).toMatch(/['"]@nx\/angular-rspack-compiler['"]: true/); + }); + it('should convert a basic angular webpack application to rspack', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace(); diff --git a/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts b/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts index d6477953742..7cd48f5cb8e 100644 --- a/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts +++ b/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts @@ -1,10 +1,12 @@ import { + acknowledgeBuildScripts, selectPrompt, forEachExecutorOptions, getNamedInputs, } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, + detectPackageManager, ensurePackage, formatFiles, joinPathFragments, @@ -26,6 +28,7 @@ import { relative, resolve } from 'path'; import { join } from 'path/posix'; import { assertSupportedAngularVersion } from '../../utils/assert-supported-angular-version'; import { nxVersion } from '../../utils/versions'; +import { acknowledgeAngularBuildScripts } from '../utils/acknowledge-build-scripts'; import { versions } from '../utils/version-utils'; import { createConfig } from './lib/create-config'; import { getCustomWebpackConfig } from './lib/get-custom-webpack-config'; @@ -714,6 +717,14 @@ export async function convertToRspack( if (!schema.skipInstall) { const { webpackMergeVersion, tsNodeVersion } = versions(tree); + acknowledgeAngularBuildScripts(tree); + // @nx/angular-rspack-compiler's install script patches `@angular/build` + // versions below 20.2.0, so it has to run. core-js only prints a funding + // message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + '@nx/angular-rspack-compiler': true, + 'core-js': false, + }); const installTask = addDependenciesToPackageJson( tree, {}, diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index a84d7eca2e0..de52745e47f 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -12,6 +12,7 @@ import { updateJson, updateNxJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions'; import { createApp } from '../../utils/nx-devkit/testing'; @@ -65,6 +66,37 @@ describe('lib', () => { } }); + describe('pnpm 11 build scripts', () => { + it('should record the decisions ng-packagr pulls in for a buildable library', async () => { + await withPnpm(tree, '11.2.2', () => + runLibraryGeneratorWithOpts({ + buildable: true, + unitTestRunner: UnitTestRunner.None, + linter: 'none', + }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]?esbuild['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]@parcel\/watcher['"]: false/); + }); + + it('should record the decisions @angular/build pulls in for a vitest library', async () => { + await withPnpm(tree, '11.2.2', () => + runLibraryGeneratorWithOpts({ + unitTestRunner: UnitTestRunner.VitestAnalog, + linter: 'none', + }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]?esbuild['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?lmdb['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?msgpackr-extract['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]@parcel\/watcher['"]: false/); + }); + }); + it('should run the library generator without erroring if the directory has a trailing slash', async () => { // ACT & ASSERT await expect( diff --git a/packages/angular/src/generators/library/library.ts b/packages/angular/src/generators/library/library.ts index 5052ff22ac8..6f863cff266 100644 --- a/packages/angular/src/generators/library/library.ts +++ b/packages/angular/src/generators/library/library.ts @@ -15,6 +15,7 @@ import init from '../../generators/init/init'; import { assertSupportedAngularVersion } from '../../utils/assert-supported-angular-version'; import { UnitTestRunner } from '../../utils/test-runners'; import addLintingGenerator from '../add-linting/add-linting'; +import { acknowledgeAngularBuildScripts } from '../utils/acknowledge-build-scripts'; import { addJest } from '../utils/add-jest'; import { addVitestAnalog, addVitestAngular } from '../utils/add-vitest'; import { addBuildableLibrariesPostCssDependencies } from '../utils/dependencies'; @@ -79,6 +80,7 @@ export async function libraryGenerator( (libraryOptions.buildable || libraryOptions.publishable) && !libraryOptions.skipPackageJson ) { + acknowledgeAngularBuildScripts(tree); addDependenciesToPackageJson( tree, {}, diff --git a/packages/angular/src/generators/utils/acknowledge-build-scripts.ts b/packages/angular/src/generators/utils/acknowledge-build-scripts.ts new file mode 100644 index 00000000000..e1ce68e03ae --- /dev/null +++ b/packages/angular/src/generators/utils/acknowledge-build-scripts.ts @@ -0,0 +1,21 @@ +import { detectPackageManager, type Tree } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; + +// The Angular build tooling (`@angular/build`, `@angular-devkit/build-angular`, +// `ng-packagr`) pulls in esbuild, lmdb (-> msgpackr-extract) and sass +// (-> @parcel/watcher). All of them ship prebuilt binaries as optional +// dependencies, so their install scripts have nothing to do. +const angularBuildAllowBuilds = { + esbuild: false, + lmdb: false, + 'msgpackr-extract': false, + '@parcel/watcher': false, +}; + +export function acknowledgeAngularBuildScripts(tree: Tree): void { + acknowledgeBuildScripts( + tree, + detectPackageManager(tree.root), + angularBuildAllowBuilds + ); +} diff --git a/packages/angular/src/generators/utils/add-vitest.ts b/packages/angular/src/generators/utils/add-vitest.ts index f4224e9dbf4..6bce104072b 100644 --- a/packages/angular/src/generators/utils/add-vitest.ts +++ b/packages/angular/src/generators/utils/add-vitest.ts @@ -22,6 +22,7 @@ import { } from '@nx/devkit/internal'; import { intersects, satisfies, valid, validRange } from 'semver'; import { nxVersion, oxcProjectRuntimeVersion } from '../../utils/versions'; +import { acknowledgeAngularBuildScripts } from './acknowledge-build-scripts'; import { getInstalledAngularDevkitVersion, getInstalledAngularVersionInfo, @@ -84,6 +85,7 @@ export async function addVitestAngular( const angularDevkitVersion = getInstalledAngularDevkitVersion(tree) ?? pkgVersions.angularDevkitVersion; + acknowledgeAngularBuildScripts(tree); return addDependenciesToPackageJson( tree, {}, @@ -133,6 +135,7 @@ export async function addVitestAnalog( devDependencies['jsdom'] = pkgVersions.jsdomVersion; } + acknowledgeAngularBuildScripts(tree); tasks.push( addDependenciesToPackageJson(tree, {}, devDependencies, undefined, true) ); diff --git a/packages/angular/src/migrations/update-23-1-0/add-angular-build.md b/packages/angular/src/migrations/update-23-1-0/add-angular-build.md index 241e5acb93f..66a70de31bc 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-angular-build.md +++ b/packages/angular/src/migrations/update-23-1-0/add-angular-build.md @@ -4,6 +4,8 @@ The `@nx/angular:application` and `@nx/angular:unit-test` executors load the Ang Executor usage is detected on any target using `@nx/angular:application`, `@nx/angular:unit-test`, or an `@angular/build:*` executor. Targets that inherit their executor from an `nx.json` `targetDefaults` entry are detected too. +On pnpm 11 and above, the migration also writes `pnpm-workspace.yaml`. `@angular/build` reaches four packages that run a build script on install (`esbuild`, `lmdb`, `msgpackr-extract` and `@parcel/watcher`), and pnpm 11 refuses to install a package whose script is neither allowed nor denied, so the migration records an `allowBuilds` decision for each. A decision already in the file is left untouched. + #### Examples ##### `project.json` @@ -28,7 +30,7 @@ After (`package.json`): // package.json { "devDependencies": { - "@angular/build": "~22.0.4", + "@angular/build": "~22.1.0", }, } ``` diff --git a/packages/angular/src/migrations/update-23-1-0/add-angular-build.spec.ts b/packages/angular/src/migrations/update-23-1-0/add-angular-build.spec.ts index b45e4f628a7..2fe30740d1c 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-angular-build.spec.ts +++ b/packages/angular/src/migrations/update-23-1-0/add-angular-build.spec.ts @@ -6,6 +6,7 @@ import { updateNxJson, type Tree, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { angularDevkitVersion } from '../../utils/versions'; import migration from './add-angular-build'; @@ -158,4 +159,40 @@ describe('add-angular-build migration', () => { expect(getAngularBuildVersion()).toBeUndefined(); }); }); + describe('pnpm 11 build scripts', () => { + it('should deny the build scripts @angular/build pulls in', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + targets: { + build: { executor: '@nx/angular:application', options: {} }, + }, + }); + + await withPnpm(tree, '11.2.2', () => migration(tree)); + + const workspaceYaml = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(workspaceYaml).toMatch(/['"]?esbuild['"]?: false/); + expect(workspaceYaml).toMatch(/['"]?lmdb['"]?: false/); + expect(workspaceYaml).toMatch(/['"]?msgpackr-extract['"]?: false/); + expect(workspaceYaml).toMatch(/['"]@parcel\/watcher['"]: false/); + }); + + it('should not record decisions when @angular/build is not added', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + targets: { + build: { + executor: '@angular-devkit/build-angular:browser', + options: {}, + }, + }, + }); + + await withPnpm(tree, '11.2.2', () => migration(tree)); + + expect(tree.exists('pnpm-workspace.yaml')).toBe(false); + }); + }); }); diff --git a/packages/angular/src/migrations/update-23-1-0/add-angular-build.ts b/packages/angular/src/migrations/update-23-1-0/add-angular-build.ts index 565f5ca337f..6b8cc15fa0a 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-angular-build.ts +++ b/packages/angular/src/migrations/update-23-1-0/add-angular-build.ts @@ -5,6 +5,7 @@ import { readNxJson, type Tree, } from '@nx/devkit'; +import { acknowledgeAngularBuildScripts } from '../../generators/utils/acknowledge-build-scripts'; import { angularDevkitVersion } from '../../utils/versions'; // The `@nx/angular:application` and `@nx/angular:unit-test` executors import @@ -67,6 +68,8 @@ export default async function (tree: Tree) { return; } + acknowledgeAngularBuildScripts(tree); + return addDependenciesToPackageJson( tree, {}, diff --git a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.md b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.md index 0a5ae164c66..297d17566b0 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.md +++ b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.md @@ -12,6 +12,8 @@ A package is added only when a matching target exists: Targets that inherit their executor from an `nx.json` `targetDefaults` entry are detected too. +On pnpm 11 and above, adding `@nx/webpack` or `@nx/rspack` also writes `pnpm-workspace.yaml`. Both reach `@parcel/watcher` through sass, and pnpm 11 refuses to install a package whose build script is neither allowed nor denied, so the migration records an `allowBuilds` decision for it. A decision already in the file is left untouched. + #### Examples For a workspace with an `@nx/angular:webpack-browser` build target, the migration adds the webpack packages to `devDependencies`. diff --git a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.spec.ts b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.spec.ts index f937def0b35..21e4f8f85e4 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.spec.ts +++ b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.spec.ts @@ -4,6 +4,7 @@ import { updateJson, type Tree, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion, webpackMergeVersion } from '../../utils/versions'; import migration from './add-optional-webpack-packages'; @@ -351,4 +352,50 @@ describe('add-optional-webpack-packages migration', () => { ); expect(getDevDependencies()['@nx/webpack']).toBeUndefined(); }); + describe('pnpm 11 build scripts', () => { + it('should deny the @parcel/watcher build script when @nx/webpack is added', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + targets: { + build: { executor: '@nx/angular:webpack-browser', options: {} }, + }, + }); + + await withPnpm(tree, '11.2.2', () => migration(tree)); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should deny the @parcel/watcher build script when @nx/rspack is added', async () => { + updateJson(tree, 'nx.json', (json) => ({ + ...json, + plugins: ['@nx/rspack/plugin'], + })); + + await withPnpm(tree, '11.2.2', () => migration(tree)); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should not record a decision when only @nx/module-federation is added', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + targets: { + serve: { + executor: '@nx/angular:module-federation-dev-server', + options: {}, + }, + }, + }); + + await withPnpm(tree, '11.2.2', () => migration(tree)); + + expect(getDevDependencies()['@nx/module-federation']).toBe(nxVersion); + expect(tree.exists('pnpm-workspace.yaml')).toBe(false); + }); + }); }); diff --git a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.ts b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.ts index 91d63e58212..6b626f5c0ac 100644 --- a/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.ts +++ b/packages/angular/src/migrations/update-23-1-0/add-optional-webpack-packages.ts @@ -1,5 +1,6 @@ import { addDependenciesToPackageJson, + detectPackageManager, getProjects, joinPathFragments, type NxJsonConfiguration, @@ -7,7 +8,10 @@ import { type TargetConfiguration, type Tree, } from '@nx/devkit'; -import { readTargetDefaultsForTarget } from '@nx/devkit/internal'; +import { + acknowledgeBuildScripts, + readTargetDefaultsForTarget, +} from '@nx/devkit/internal'; import { nxVersion, webpackMergeVersion } from '../../utils/versions'; const webpackExecutors = new Set([ @@ -132,6 +136,15 @@ export default async function addOptionalWebpackPackages(tree: Tree) { return; } + if (needsWebpack || needsRspack) { + // @nx/webpack and @nx/rspack depend on sass, which pulls in + // @parcel/watcher. Its install script only builds from source when + // npm_config_build_from_source is set, so skip it. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + '@parcel/watcher': false, + }); + } + return addDependenciesToPackageJson( tree, {}, diff --git a/packages/cypress/src/generators/component-configuration/component-configuration.spec.ts b/packages/cypress/src/generators/component-configuration/component-configuration.spec.ts index f866d6dea64..d8da8d225cf 100644 --- a/packages/cypress/src/generators/component-configuration/component-configuration.spec.ts +++ b/packages/cypress/src/generators/component-configuration/component-configuration.spec.ts @@ -11,6 +11,7 @@ import { updateNxJson, updateProjectConfiguration, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { getInstalledCypressMajorVersion } from '../../utils/versions'; import { componentConfigurationGenerator } from './component-configuration'; @@ -104,6 +105,40 @@ describe('Cypress Component Configuration', () => { jest.clearAllMocks(); }); + it('should deny the esbuild build script pulled in by the webpack dev server', async () => { + mockedInstalledCypressVersion.mockReturnValue(10); + + await withPnpm(tree, '11.2.2', () => + componentConfigurationGenerator(tree, { + project: 'cool-lib', + skipFormat: true, + bundler: 'webpack', + addPlugin: true, + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + + it('should not record an esbuild decision for the vite dev server', async () => { + mockedInstalledCypressVersion.mockReturnValue(10); + + await withPnpm(tree, '11.2.2', () => + componentConfigurationGenerator(tree, { + project: 'cool-lib', + skipFormat: true, + bundler: 'vite', + addPlugin: true, + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'esbuild' + ); + }); + it('should not add the target when @nx/cypress/plugin is registered', async () => { await cypressInitGenerator(tree, { addPlugin: true, diff --git a/packages/cypress/src/generators/component-configuration/component-configuration.ts b/packages/cypress/src/generators/component-configuration/component-configuration.ts index c2dcf8fc1b4..16f9ce73fea 100644 --- a/packages/cypress/src/generators/component-configuration/component-configuration.ts +++ b/packages/cypress/src/generators/component-configuration/component-configuration.ts @@ -1,5 +1,6 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, generateFiles, GeneratorCallback, @@ -15,7 +16,11 @@ import { updateNxJson, updateProjectConfiguration, } from '@nx/devkit'; -import { findTargetDefault, upsertTargetDefault } from '@nx/devkit/internal'; +import { + acknowledgeBuildScripts, + findTargetDefault, + upsertTargetDefault, +} from '@nx/devkit/internal'; import { assertNotUsingTsSolutionSetup } from '@nx/js/internal'; import { assertSupportedCypressVersion } from '../../utils/assert-supported-cypress-version'; import { warnCypressExecutorGenerating } from '../../utils/deprecation'; @@ -116,6 +121,12 @@ function updateDeps(tree: Tree, opts: NormalizeCTOptions) { devDeps['@cypress/vite-dev-server'] = pkgVersions.cypressViteDevServerVersion; } else { + // @cypress/webpack-dev-server depends on tsx, which depends on esbuild, + // whose install script only validates the prebuilt binary that ships as an + // optional dependency. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); devDeps['@cypress/webpack-dev-server'] = pkgVersions.cypressWebpackVersion; devDeps['html-webpack-plugin'] = pkgVersions.htmlWebpackPluginVersion; } diff --git a/packages/detox/src/generators/init/init.spec.ts b/packages/detox/src/generators/init/init.spec.ts index 6f693a0e046..8914e4df9aa 100644 --- a/packages/detox/src/generators/init/init.spec.ts +++ b/packages/detox/src/generators/init/init.spec.ts @@ -1,6 +1,7 @@ import '@nx/devkit/internal-testing-utils/mock-project-graph'; import { Tree, readJson } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { detoxInitGenerator } from './init'; @@ -11,6 +12,17 @@ describe('init', () => { tree = createTreeWithEmptyWorkspace(); }); + it('should record the build script decisions detox pulls in', async () => { + await withPnpm(tree, '11.2.2', () => + detoxInitGenerator(tree, { addPlugin: true }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]?detox['"]?: true/); + expect(pnpmWorkspace).toMatch(/['"]?dtrace-provider['"]?: false/); + expect(pnpmWorkspace).toMatch(/['"]?unrs-resolver['"]?: false/); + }); + it('should add detox dependencies', async () => { await detoxInitGenerator(tree, { addPlugin: true, diff --git a/packages/detox/src/generators/init/init.ts b/packages/detox/src/generators/init/init.ts index 8c612a36a52..3c81fcfccd2 100644 --- a/packages/detox/src/generators/init/init.ts +++ b/packages/detox/src/generators/init/init.ts @@ -74,9 +74,13 @@ export async function detoxInitGeneratorInternal(host: Tree, schema: Schema) { export function updateDependencies(host: Tree, schema: Schema) { // The user explicitly asked for detox, and its postinstall builds the // framework cache it needs to run at all, so enable it — npm and yarn run - // it unconditionally. Transitive deps stay denied. + // it unconditionally. Transitive deps stay denied: dtrace-provider (via + // bunyan) falls back to a no-op when its build fails, and unrs-resolver (via + // @nx/detox's @nx/jest dependency) only fetches a fallback binding. acknowledgeBuildScripts(host, detectPackageManager(host.root), { detox: true, + 'dtrace-provider': false, + 'unrs-resolver': false, }); return addDependenciesToPackageJson( host, diff --git a/packages/devkit/internal-testing-utils.ts b/packages/devkit/internal-testing-utils.ts index 83c6772cd47..69bd3b7dbe5 100644 --- a/packages/devkit/internal-testing-utils.ts +++ b/packages/devkit/internal-testing-utils.ts @@ -3,6 +3,7 @@ export * from 'nx/src/internal-testing-utils/assert-generators-enforce-version-f export * from 'nx/src/internal-testing-utils/assert-valid-migrations'; export * from 'nx/src/internal-testing-utils/run-migration-against-this-workspace'; export * from 'nx/src/internal-testing-utils/with-environment'; +export * from 'nx/src/internal-testing-utils/with-pnpm'; export * from 'nx/src/internal-testing-utils/temp-fs'; export { setCwd } from './src/generators/artifact-name-and-directory-utils'; diff --git a/packages/js/internal.ts b/packages/js/internal.ts index 088a9f48179..9b4a283d65e 100644 --- a/packages/js/internal.ts +++ b/packages/js/internal.ts @@ -85,6 +85,7 @@ export { // SWC helpers export { addSwcConfig, addSwcTestConfig } from './src/utils/swc/add-swc-config'; export { + acknowledgeSwcBuildScripts, addSwcDependencies, addSwcRegisterDependencies, } from './src/utils/swc/add-swc-dependencies'; diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 689650bc6ae..76fcfad4b7a 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -10,6 +10,7 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { libraryGenerator } from './library'; import type { LibraryGeneratorSchema } from './schema'; @@ -44,6 +45,40 @@ describe('lib', () => { } }); + describe('pnpm 11 build scripts', () => { + it('should deny the esbuild build script for the esbuild bundler', async () => { + await withPnpm(tree, '11.2.2', () => + libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + bundler: 'esbuild', + unitTestRunner: 'none', + linter: 'none', + } as LibraryGeneratorSchema) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + + it('should deny the @swc/core build script for the swc bundler', async () => { + await withPnpm(tree, '11.2.2', () => + libraryGenerator(tree, { + ...defaultOptions, + directory: 'my-lib', + bundler: 'swc', + unitTestRunner: 'none', + linter: 'none', + } as LibraryGeneratorSchema) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }); + }); + it.each` bundler ${'esbuild'} diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index ec55ad1c235..4d0159f2519 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -1,4 +1,5 @@ import { + acknowledgeBuildScripts, determineProjectNameAndRootOptions, ensureRootProjectName, isInteractive, @@ -10,6 +11,7 @@ import { import { addDependenciesToPackageJson, addProjectConfiguration, + detectPackageManager, ensurePackage, formatFiles, generateFiles, @@ -38,7 +40,10 @@ import { normalizeLinterOption } from '../../utils/generator-prompts'; import { sortPackageJsonFields } from '../../utils/package-json/sort-fields'; import { getUpdatedPackageJsonContent } from '../../utils/package-json/update-package-json'; import { addSwcConfig } from '../../utils/swc/add-swc-config'; -import { getSwcDependencies } from '../../utils/swc/add-swc-dependencies'; +import { + acknowledgeSwcBuildScripts, + getSwcDependencies, +} from '../../utils/swc/add-swc-dependencies'; import { getNeededCompilerOptionOverrides } from '../../utils/typescript/configuration'; import { getTsConfigBaseOptions } from '../../utils/typescript/create-ts-config'; import { ensureTypescript } from '../../utils/typescript/ensure-typescript'; @@ -936,6 +941,11 @@ function addProjectDependencies( options: NormalizedLibraryGeneratorOptions ): GeneratorCallback { if (options.bundler == 'esbuild') { + // esbuild's install script only validates the prebuilt binary that ships as + // an optional dependency. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); return addDependenciesToPackageJson( tree, {}, @@ -971,6 +981,7 @@ function addProjectDependencies( true ); } else if (options.bundler === 'swc') { + acknowledgeSwcBuildScripts(tree); const { dependencies, devDependencies } = getSwcDependencies(); return addDependenciesToPackageJson( tree, diff --git a/packages/js/src/utils/swc/add-swc-dependencies.ts b/packages/js/src/utils/swc/add-swc-dependencies.ts index d2978988f0f..2af266232e7 100644 --- a/packages/js/src/utils/swc/add-swc-dependencies.ts +++ b/packages/js/src/utils/swc/add-swc-dependencies.ts @@ -15,6 +15,14 @@ import { // covered by its prebuilt optional dependencies, so skip it. const swcAllowBuilds = { '@swc/core': false }; +export function acknowledgeSwcBuildScripts(tree: Tree): void { + acknowledgeBuildScripts( + tree, + detectPackageManager(tree.root), + swcAllowBuilds + ); +} + export function getSwcDependencies(): { dependencies: Record; devDependencies: Record; @@ -33,11 +41,7 @@ export function getSwcDependencies(): { export function addSwcDependencies(tree: Tree) { const { dependencies, devDependencies } = getSwcDependencies(); - acknowledgeBuildScripts( - tree, - detectPackageManager(tree.root), - swcAllowBuilds - ); + acknowledgeSwcBuildScripts(tree); return addDependenciesToPackageJson( tree, dependencies, @@ -48,11 +52,7 @@ export function addSwcDependencies(tree: Tree) { } export function addSwcRegisterDependencies(tree: Tree) { - acknowledgeBuildScripts( - tree, - detectPackageManager(tree.root), - swcAllowBuilds - ); + acknowledgeSwcBuildScripts(tree); return addDependenciesToPackageJson( tree, {}, diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 72c9aa1dac8..db0c869f7f3 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -1,3 +1,4 @@ +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { getProjects, @@ -28,6 +29,53 @@ describe('app', () => { else process.env.ESLINT_USE_FLAT_CONFIG = envBackup; }); + describe('pnpm 11 build scripts', () => { + it('should deny the @swc/core build script for the swc custom server setup', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { directory: uniq(), style: 'css' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }); + + it('should deny the unrs-resolver build script pulled in by eslint-config-next', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: uniq(), + style: 'css', + linter: 'eslint', + unitTestRunner: 'none', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?unrs-resolver['"]?: false/ + ); + }); + + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { directory: uniq(), style: 'scss' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should not record a @parcel/watcher decision without scss', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { directory: uniq(), style: 'css' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + '@parcel/watcher' + ); + }); + }); + it('should add a .gitkeep file to the public directory', async () => { const name = uniq(); await applicationGenerator(tree, { diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index e2fcaed6fcd..d869c700b37 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -1,10 +1,12 @@ import { addDependenciesToPackageJson, + detectPackageManager, GeneratorCallback, joinPathFragments, runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { extraEslintDependencies } from '@nx/react'; import { NormalizedSchema } from './normalize-options'; import { @@ -117,6 +119,13 @@ export async function addLinting( const eslintConfigNextVersion = await getEslintConfigNextDependenciesVersionsToInstall(host); + // eslint-config-next pulls in unrs-resolver via + // eslint-import-resolver-typescript, whose postinstall only fetches a + // fallback binding for platforms its prebuilt optional dependencies miss. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + 'unrs-resolver': false, + }); + tasks.push( addDependenciesToPackageJson( host, diff --git a/packages/next/src/generators/init/init.spec.ts b/packages/next/src/generators/init/init.spec.ts index c9c2d7dbf77..082a19d2a71 100644 --- a/packages/next/src/generators/init/init.spec.ts +++ b/packages/next/src/generators/init/init.spec.ts @@ -5,6 +5,7 @@ import { ProjectGraph, addDependenciesToPackageJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { nextInitGenerator } from './init'; import { @@ -87,4 +88,30 @@ describe('init', () => { const packageJson = readJson(tree, 'package.json'); expect(packageJson.dependencies['next']).toBe('15.0.0'); }); + + describe('pnpm 11 build scripts', () => { + it('should deny the sharp build script pulled in by next 15+', async () => { + await withPnpm(tree, '11.2.2', () => nextInitGenerator(tree, {})); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toContain( + 'sharp: false' + ); + }); + + it('should not record a sharp decision for next 14', async () => { + projectGraph.externalNodes = { + 'npm:next': { + type: 'npm', + name: 'npm:next', + data: { packageName: 'next', version: '14.2.26' }, + }, + }; + + await withPnpm(tree, '11.2.2', () => nextInitGenerator(tree, {})); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'sharp' + ); + }); + }); }); diff --git a/packages/next/src/generators/init/init.ts b/packages/next/src/generators/init/init.ts index f2e67e0daa0..36626f4b1cd 100644 --- a/packages/next/src/generators/init/init.ts +++ b/packages/next/src/generators/init/init.ts @@ -1,6 +1,7 @@ -import { addPlugin } from '@nx/devkit/internal'; +import { acknowledgeBuildScripts, addPlugin } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, + detectPackageManager, removeDependenciesFromPackageJson, runTasksInSerial, type GeneratorCallback, @@ -12,6 +13,7 @@ import { getReactDependenciesVersionsToInstall, isReact18, } from '@nx/react/internal'; +import { coerce, major } from 'semver'; import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry'; import { nxVersion } from '../../utils/versions'; import { getNextDependenciesVersionsToInstall } from '../../utils/version-utils'; @@ -29,6 +31,15 @@ async function updateDependencies(host: Tree, schema: InitSchema) { ); const reactVersions = await getReactDependenciesVersionsToInstall(host); + // next 15+ pulls in sharp, whose install script only compiles from source + // against a system libvips and is otherwise a no-op; the prebuilt binaries + // ship via optional dependencies, so skip it. + if (major(coerce(versions.next)) >= 15) { + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + sharp: false, + }); + } + tasks.push( addDependenciesToPackageJson( host, diff --git a/packages/next/src/utils/add-swc-to-custom-server.ts b/packages/next/src/utils/add-swc-to-custom-server.ts index 3c663b938ea..712826546b7 100644 --- a/packages/next/src/utils/add-swc-to-custom-server.ts +++ b/packages/next/src/utils/add-swc-to-custom-server.ts @@ -6,6 +6,7 @@ import { readJson, } from '@nx/devkit'; import { + acknowledgeSwcBuildScripts, swcCliVersion, swcCoreVersion, swcNodeVersion, @@ -44,6 +45,7 @@ export function configureForSwc( } function addSwcDependencies(tree: Tree) { + acknowledgeSwcBuildScripts(tree); return addDependenciesToPackageJson( tree, { diff --git a/packages/next/src/utils/styles.ts b/packages/next/src/utils/styles.ts index 074dd5c599e..878aa949181 100644 --- a/packages/next/src/utils/styles.ts +++ b/packages/next/src/utils/styles.ts @@ -1,8 +1,10 @@ import { addDependenciesToPackageJson, + detectPackageManager, GeneratorCallback, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { sassVersion } from './versions'; @@ -23,6 +25,14 @@ export function addStyleDependencies( ): GeneratorCallback { const extraDependencies = nextSpecificStyleDependencies[options.style]; + if (options.style === 'scss') { + // sass pulls in @parcel/watcher, whose install script only builds from + // source when npm_config_build_from_source is set. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + '@parcel/watcher': false, + }); + } + return extraDependencies ? addDependenciesToPackageJson( host, diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index 699dbc0f9c2..fc2efb8aa05 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -1,7 +1,8 @@ // Mock `@nx/devkit` so that (1) the project graph is empty during generation and -// (2) the detected package manager is pinned. Pinning keeps inferred lock-file -// outputs (e.g. prune-lockfile) deterministic regardless of which package -// manager runs the tests. Individual tests can override `detectPackageManager`. +// (2) the detected package manager can be pinned. `beforeEach` pins it to npm, +// which keeps inferred lock-file outputs (e.g. prune-lockfile) deterministic +// regardless of which package manager runs the tests. Individual tests can +// override `detectPackageManager`. jest.mock('@nx/devkit', () => { const actual = jest.requireActual('@nx/devkit'); return { @@ -9,7 +10,7 @@ jest.mock('@nx/devkit', () => { createProjectGraphAsync: jest .fn() .mockResolvedValue({ nodes: {}, dependencies: {} }), - detectPackageManager: jest.fn(() => 'npm'), + detectPackageManager: jest.fn(), getPackageManagerCommand: jest.fn((pm = 'npm') => actual.getPackageManagerCommand(pm) ), @@ -27,6 +28,7 @@ import { updateNxJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { PNPM_INSTALL_SETTINGS_INPUTS, TS_SOLUTION_SETUP_TSCONFIG_INPUT, @@ -48,6 +50,8 @@ describe('app', () => { tree = createTreeWithEmptyWorkspace(); jest.clearAllMocks(); + // `clearAllMocks` keeps configured return values, so re-pin the default. + (detectPackageManager as jest.Mock).mockReturnValue('npm'); }); afterEach(() => { @@ -58,6 +62,48 @@ describe('app', () => { } }); + describe('pnpm 11 build scripts', () => { + beforeEach(() => { + (detectPackageManager as jest.Mock).mockReturnValue('pnpm'); + }); + + it('should deny the esbuild build script for the esbuild bundler', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: 'my-node-app', + bundler: 'esbuild', + framework: 'none', + unitTestRunner: 'none', + e2eTestRunner: 'none', + linter: 'none', + addPlugin: true, + } as Schema) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + + it('should not record an esbuild decision for the webpack bundler', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: 'my-node-app', + bundler: 'webpack', + framework: 'none', + unitTestRunner: 'none', + e2eTestRunner: 'none', + linter: 'none', + addPlugin: true, + } as Schema) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'esbuild' + ); + }); + }); + describe('not nested', () => { it('should update project config', async () => { await applicationGenerator(tree, { diff --git a/packages/node/src/generators/application/lib/add-dependencies.ts b/packages/node/src/generators/application/lib/add-dependencies.ts index 8aa9fb4d17b..3a783d9e102 100644 --- a/packages/node/src/generators/application/lib/add-dependencies.ts +++ b/packages/node/src/generators/application/lib/add-dependencies.ts @@ -1,8 +1,10 @@ import { addDependenciesToPackageJson, + detectPackageManager, GeneratorCallback, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { esbuildVersion } from '@nx/js/internal'; import { expressVersions, @@ -59,6 +61,14 @@ export function addProjectDependencies( fastify: {}, }; + if (options.bundler === 'esbuild') { + // esbuild's install script only validates the prebuilt binary that ships as + // an optional dependency. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); + } + const typesNodeVersion = nodeTypesVersions(tree).typesNodeVersion; return { diff --git a/packages/node/src/generators/init/init.spec.ts b/packages/node/src/generators/init/init.spec.ts index 20d8aed72b4..65dba61e160 100644 --- a/packages/node/src/generators/init/init.spec.ts +++ b/packages/node/src/generators/init/init.spec.ts @@ -4,6 +4,7 @@ import { Tree, updateJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion } from '../../utils/versions'; @@ -16,6 +17,14 @@ describe('init', () => { tree = createTreeWithEmptyWorkspace(); }); + it('should deny the unrs-resolver build script pulled in by @nx/jest', async () => { + await withPnpm(tree, '11.2.2', () => initGenerator(tree, {})); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?unrs-resolver['"]?: false/ + ); + }); + it('should add dependencies', async () => { const existing = 'existing'; const existingVersion = '1.0.0'; diff --git a/packages/node/src/generators/init/init.ts b/packages/node/src/generators/init/init.ts index 4e12c6a5af5..17faaf1eb6d 100644 --- a/packages/node/src/generators/init/init.ts +++ b/packages/node/src/generators/init/init.ts @@ -1,16 +1,23 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, GeneratorCallback, removeDependenciesFromPackageJson, runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { nxVersion } from '../../utils/versions'; import { Schema } from './schema'; function updateDependencies(tree: Tree, options: Schema) { const tasks: GeneratorCallback[] = []; + // @nx/node depends on @nx/jest, so jest 30's unrs-resolver is installed even + // without a jest setup. Its postinstall only fetches a fallback binding. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'unrs-resolver': false, + }); tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/node'], [])); tasks.push( addDependenciesToPackageJson( diff --git a/packages/nuxt/src/generators/application/application.spec.ts b/packages/nuxt/src/generators/application/application.spec.ts index 1bbb32b6206..612261e1466 100644 --- a/packages/nuxt/src/generators/application/application.spec.ts +++ b/packages/nuxt/src/generators/application/application.spec.ts @@ -8,6 +8,7 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { applicationGenerator } from './application'; describe('app', () => { @@ -24,6 +25,43 @@ describe('app', () => { else process.env.ESLINT_USE_FLAT_CONFIG = envBackup; }); + describe('pnpm 11 build scripts', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should deny the esbuild build script pulled in by the nuxt toolchain', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: 'my-app', + unitTestRunner: 'none', + e2eTestRunner: 'none', + useAppDir: false, + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: 'my-app', + style: 'scss', + unitTestRunner: 'none', + e2eTestRunner: 'none', + useAppDir: false, + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + }); + describe.each(['my-app', 'myApp'])( 'generated files content - as-provided - %s', (name: string) => { diff --git a/packages/nuxt/src/generators/application/lib/ensure-dependencies.ts b/packages/nuxt/src/generators/application/lib/ensure-dependencies.ts index a97362f67ce..b1799c911a7 100644 --- a/packages/nuxt/src/generators/application/lib/ensure-dependencies.ts +++ b/packages/nuxt/src/generators/application/lib/ensure-dependencies.ts @@ -1,4 +1,9 @@ -import { addDependenciesToPackageJson, type Tree } from '@nx/devkit'; +import { + addDependenciesToPackageJson, + detectPackageManager, + type Tree, +} from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { sassVersion, vueRouterVersion, @@ -29,6 +34,11 @@ export async function ensureDependencies( }; if (options.style === 'scss') { + // sass pulls in @parcel/watcher, whose install script only builds from + // source when npm_config_build_from_source is set. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + '@parcel/watcher': false, + }); devDependencies['sass'] = sassVersion; } diff --git a/packages/nuxt/src/generators/init/lib/utils.ts b/packages/nuxt/src/generators/init/lib/utils.ts index 3e1821273b5..940b600cdf5 100644 --- a/packages/nuxt/src/generators/init/lib/utils.ts +++ b/packages/nuxt/src/generators/init/lib/utils.ts @@ -1,9 +1,11 @@ import { addDependenciesToPackageJson, + detectPackageManager, readNxJson, Tree, updateNxJson, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { nxVersion } from '../../../utils/versions'; import { getNuxtDependenciesVersionsToInstall } from '../../../utils/version-utils'; import { InitSchema } from '../schema'; @@ -11,6 +13,13 @@ import { InitSchema } from '../schema'; export async function updateDependencies(host: Tree, schema: InitSchema) { const nuxtVersions = await getNuxtDependenciesVersionsToInstall(host); + // The nuxt toolchain (nitropack, @unhead/bundler) depends on esbuild, whose + // install script only validates the prebuilt binary that ships as an optional + // dependency. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + esbuild: false, + }); + return addDependenciesToPackageJson( host, {}, diff --git a/packages/nx/src/internal-testing-utils/with-pnpm.ts b/packages/nx/src/internal-testing-utils/with-pnpm.ts new file mode 100644 index 00000000000..e4d71362688 --- /dev/null +++ b/packages/nx/src/internal-testing-utils/with-pnpm.ts @@ -0,0 +1,22 @@ +import type { Tree } from '../generators/tree'; +import { updateJson } from '../generators/utils/json'; +import { withEnvironmentVariables } from './with-environment'; + +/** + * Runs `callback` with the workspace presented as managed by the given pnpm + * version: `packageManager` is written to the tree's package.json and a + * matching `npm_config_user_agent` is set for the duration of the call, which + * is what `detectPackageManager` reads for a tree that has no lock file on + * disk. + */ +export function withPnpm(tree: Tree, version: string, callback: () => T): T { + updateJson(tree, 'package.json', (json) => ({ + ...json, + packageManager: `pnpm@${version}`, + })); + + return withEnvironmentVariables( + { npm_config_user_agent: `pnpm/${version} npm/? node/v22.0.0` }, + callback + ); +} diff --git a/packages/plugin/src/generators/plugin/plugin.spec.ts b/packages/plugin/src/generators/plugin/plugin.spec.ts index 70715555c30..5aa64716bf2 100644 --- a/packages/plugin/src/generators/plugin/plugin.spec.ts +++ b/packages/plugin/src/generators/plugin/plugin.spec.ts @@ -9,6 +9,7 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { pluginGenerator } from './plugin'; import { Schema } from './schema'; @@ -34,6 +35,24 @@ describe('NxPlugin Plugin Generator', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); + it('should deny the unrs-resolver build script pulled in by @nx/jest', async () => { + await withPnpm(tree, '11.2.2', () => + pluginGenerator( + tree, + getSchema({ + skipFormat: true, + linter: 'none', + unitTestRunner: 'none', + e2eTestRunner: 'none', + }) + ) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?unrs-resolver['"]?: false/ + ); + }); + it('should update the project configuration', async () => { await pluginGenerator(tree, getSchema()); const project = readProjectConfiguration(tree, 'my-plugin'); diff --git a/packages/plugin/src/generators/plugin/plugin.ts b/packages/plugin/src/generators/plugin/plugin.ts index da96400e37c..d517f3af40e 100644 --- a/packages/plugin/src/generators/plugin/plugin.ts +++ b/packages/plugin/src/generators/plugin/plugin.ts @@ -1,5 +1,6 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, generateFiles, GeneratorCallback, @@ -11,6 +12,7 @@ import { updateJson, updateProjectConfiguration, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { libraryGenerator as jsLibraryGenerator, addTsLibDependencies, @@ -130,6 +132,11 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) { tasks.push(addTsLibDependencies(host)); } + // @nx/plugin depends on @nx/jest, so jest 30's unrs-resolver is installed even + // without a jest setup. Its postinstall only fetches a fallback binding. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + 'unrs-resolver': false, + }); tasks.push( addDependenciesToPackageJson( host, diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 35b512eb8eb..ef12d7bc087 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -12,6 +12,7 @@ import { updateNxJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { applicationGenerator } from './application'; import { Schema } from './schema'; @@ -73,6 +74,38 @@ describe('app', () => { } }); + describe('pnpm 11 build scripts', () => { + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(appTree, '11.2.2', () => + applicationGenerator(appTree, { + ...schema, + bundler: 'vite', + style: 'scss', + e2eTestRunner: 'none', + }) + ); + + expect(appTree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should not record a @parcel/watcher decision without scss', async () => { + await withPnpm(appTree, '11.2.2', () => + applicationGenerator(appTree, { + ...schema, + bundler: 'vite', + style: 'css', + e2eTestRunner: 'none', + }) + ); + + expect(appTree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + '@parcel/watcher' + ); + }); + }); + describe('not nested', () => { it('should create project configurations', async () => { await applicationGenerator(appTree, schema); diff --git a/packages/react/src/generators/application/lib/install-common-dependencies.ts b/packages/react/src/generators/application/lib/install-common-dependencies.ts index b1ebab88e00..ebe9429660e 100644 --- a/packages/react/src/generators/application/lib/install-common-dependencies.ts +++ b/packages/react/src/generators/application/lib/install-common-dependencies.ts @@ -1,4 +1,9 @@ -import { addDependenciesToPackageJson, Tree } from '@nx/devkit'; +import { + addDependenciesToPackageJson, + detectPackageManager, + Tree, +} from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { babelCoreVersion, babelPresetReactVersion, @@ -50,6 +55,11 @@ export async function installCommonDependencies( // `@nx/webpack` installs them automatically for now. if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') { if (options.style === 'scss') { + // sass pulls in @parcel/watcher, whose install script only builds from + // source when npm_config_build_from_source is set. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + '@parcel/watcher': false, + }); devDependencies['sass'] = sassVersion; } } diff --git a/packages/react/src/generators/consumer/consumer.spec.ts b/packages/react/src/generators/consumer/consumer.spec.ts index c7ec7ee7e24..f1d1292e47a 100644 --- a/packages/react/src/generators/consumer/consumer.spec.ts +++ b/packages/react/src/generators/consumer/consumer.spec.ts @@ -1,4 +1,5 @@ import { Tree, readProjectConfiguration } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import consumerGenerator from './consumer'; import type { SupportedBundler } from '../_utils/normalize'; @@ -20,6 +21,32 @@ describe('@nx/react:consumer', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); + it('should deny the core-js build script pulled in by @rsbuild/core', async () => { + await withPnpm(tree, '11.2.2', () => + consumerGenerator(tree, { + directory: 'apps/my-consumer', + bundler: 'rsbuild', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?core-js['"]?: false/ + ); + }); + + it('should not record a core-js decision for the vite bundler', async () => { + await withPnpm(tree, '11.2.2', () => + consumerGenerator(tree, { + directory: 'apps/my-consumer', + bundler: 'vite', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'core-js' + ); + }); + it.each(['vite', 'rsbuild', 'rspack'])( 'generates a consumer for the %s bundler', async (bundler) => { diff --git a/packages/react/src/generators/consumer/consumer.ts b/packages/react/src/generators/consumer/consumer.ts index 95fbdc3ed3b..b31a989ec01 100644 --- a/packages/react/src/generators/consumer/consumer.ts +++ b/packages/react/src/generators/consumer/consumer.ts @@ -1,6 +1,7 @@ import { addDependenciesToPackageJson, addProjectConfiguration, + detectPackageManager, formatFiles, generateFiles, GeneratorCallback, @@ -9,6 +10,7 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { assertSupportedReactVersion } from '../../utils/assert-supported-react-version'; import { typescriptVersion } from '@nx/js/src/utils/versions'; import { @@ -173,6 +175,13 @@ export async function consumerGenerator( // Write deps into the workspace root package.json so bare bundler bins // resolve when nx invokes the generated run-commands serve target. const deps = getConsumerDeps(opts.bundler); + if (opts.bundler === 'rsbuild') { + // @rsbuild/core v1 depends on core-js, whose install script only prints a + // funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js': false, + }); + } tasks.push( addDependenciesToPackageJson( tree, diff --git a/packages/react/src/generators/library/lib/install-common-dependencies.ts b/packages/react/src/generators/library/lib/install-common-dependencies.ts index 120cdd6dc80..6ca011fcb17 100644 --- a/packages/react/src/generators/library/lib/install-common-dependencies.ts +++ b/packages/react/src/generators/library/lib/install-common-dependencies.ts @@ -1,9 +1,11 @@ import { addDependenciesToPackageJson, + detectPackageManager, GeneratorCallback, runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { addSwcDependencies } from '@nx/js/internal'; import { getReactDependenciesVersionsToInstall } from '../../../utils/version-utils'; import { @@ -41,6 +43,11 @@ export async function installCommonDependencies( // TODO(jack): Once we clean up webpack we can remove this check if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') { if (options.style === 'scss') { + // sass pulls in @parcel/watcher, whose install script only builds from + // source when npm_config_build_from_source is set. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + '@parcel/watcher': false, + }); devDependencies['sass'] = sassVersion; } } diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index ed4e3cd887a..ba00f85b4a4 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -9,6 +9,7 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion } from '../../utils/versions'; import applicationGenerator from '../application/application'; @@ -64,6 +65,38 @@ describe('lib', () => { } }); + describe('pnpm 11 build scripts', () => { + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + libraryGenerator(tree, { + ...defaultSchema, + bundler: 'vite', + style: 'scss', + unitTestRunner: 'none', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should not record a @parcel/watcher decision without scss', async () => { + await withPnpm(tree, '11.2.2', () => + libraryGenerator(tree, { + ...defaultSchema, + bundler: 'vite', + style: 'css', + unitTestRunner: 'none', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + '@parcel/watcher' + ); + }); + }); + it('should update project configuration', async () => { await libraryGenerator(tree, defaultSchema); const project = readProjectConfiguration(tree, 'my-lib'); diff --git a/packages/react/src/generators/provider/provider.spec.ts b/packages/react/src/generators/provider/provider.spec.ts index 12b9eeb3ec4..3c3803cffee 100644 --- a/packages/react/src/generators/provider/provider.spec.ts +++ b/packages/react/src/generators/provider/provider.spec.ts @@ -1,4 +1,5 @@ import { Tree, readProjectConfiguration } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import providerGenerator from './provider'; import type { SupportedBundler } from '../_utils/normalize'; @@ -20,6 +21,32 @@ describe('@nx/react:provider', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); + it('should deny the core-js build script pulled in by @rsbuild/core', async () => { + await withPnpm(tree, '11.2.2', () => + providerGenerator(tree, { + directory: 'apps/my-provider', + bundler: 'rsbuild', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?core-js['"]?: false/ + ); + }); + + it('should not record a core-js decision for the vite bundler', async () => { + await withPnpm(tree, '11.2.2', () => + providerGenerator(tree, { + directory: 'apps/my-provider', + bundler: 'vite', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'core-js' + ); + }); + it.each(['vite', 'rsbuild', 'rspack'])( 'generates a provider for the %s bundler', async (bundler) => { diff --git a/packages/react/src/generators/provider/provider.ts b/packages/react/src/generators/provider/provider.ts index e172db9b5d1..4a2e8b49caa 100644 --- a/packages/react/src/generators/provider/provider.ts +++ b/packages/react/src/generators/provider/provider.ts @@ -1,6 +1,7 @@ import { addDependenciesToPackageJson, addProjectConfiguration, + detectPackageManager, formatFiles, generateFiles, GeneratorCallback, @@ -9,6 +10,7 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { assertSupportedReactVersion } from '../../utils/assert-supported-react-version'; import { typescriptVersion } from '@nx/js/src/utils/versions'; import { @@ -121,6 +123,13 @@ export async function providerGenerator( // resolve when nx invokes the generated run-commands serve target. The // per-project package.json template still ships for pnpm-workspace setups. const deps = getProviderDeps(opts.bundler); + if (opts.bundler === 'rsbuild') { + // @rsbuild/core v1 depends on core-js, whose install script only prints a + // funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js': false, + }); + } const installTask = addDependenciesToPackageJson( tree, deps.dependencies, diff --git a/packages/remix/src/generators/init/init.spec.ts b/packages/remix/src/generators/init/init.spec.ts index 35b68f0c7f6..e156648f238 100644 --- a/packages/remix/src/generators/init/init.spec.ts +++ b/packages/remix/src/generators/init/init.spec.ts @@ -1,10 +1,21 @@ import '@nx/devkit/internal-testing-utils/mock-project-graph'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { addDependenciesToPackageJson, readJson } from '@nx/devkit'; import initGenerator, { remixInitGeneratorInternal } from './init'; describe('Remix Init Generator', () => { + it('should deny the esbuild build script pulled in by @remix-run/dev', async () => { + const tree = createTreeWithEmptyWorkspace(); + + await withPnpm(tree, '11.2.2', () => remixInitGeneratorInternal(tree, {})); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + it('should setup the workspace and add dependencies', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace(); diff --git a/packages/remix/src/generators/init/init.ts b/packages/remix/src/generators/init/init.ts index 5882dcc7006..f4e857af086 100644 --- a/packages/remix/src/generators/init/init.ts +++ b/packages/remix/src/generators/init/init.ts @@ -1,7 +1,8 @@ -import { addPlugin } from '@nx/devkit/internal'; +import { acknowledgeBuildScripts, addPlugin } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, createProjectGraphAsync, + detectPackageManager, formatFiles, GeneratorCallback, readNxJson, @@ -29,6 +30,11 @@ export async function remixInitGeneratorInternal(tree: Tree, options: Schema) { tasks.push(assertAndPinRemixTypescript(tree)); if (!options.skipPackageJson) { + // @remix-run/dev depends on esbuild, whose install script only validates the + // prebuilt binary that ships as an optional dependency. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); const installTask = addDependenciesToPackageJson( tree, { diff --git a/packages/remix/src/generators/library/library.impl.spec.ts b/packages/remix/src/generators/library/library.impl.spec.ts index 681f46dcfa9..3996edf93c0 100644 --- a/packages/remix/src/generators/library/library.impl.spec.ts +++ b/packages/remix/src/generators/library/library.impl.spec.ts @@ -7,11 +7,29 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import applicationGenerator from '../application/application.impl'; import libraryGenerator from './library.impl'; describe('Remix Library Generator', () => { + it('should deny the esbuild build script pulled in by vite', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + await withPnpm(tree, '11.2.2', () => + libraryGenerator(tree, { + directory: 'test', + style: 'css', + addPlugin: true, + unitTestRunner: 'none', + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + it('throws when the workspace declares TypeScript 6', async () => { const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); updateJson(tree, 'package.json', (json) => { diff --git a/packages/remix/src/generators/utils/update-dependencies.ts b/packages/remix/src/generators/utils/update-dependencies.ts index 049e7154f9b..d76d547b5f4 100644 --- a/packages/remix/src/generators/utils/update-dependencies.ts +++ b/packages/remix/src/generators/utils/update-dependencies.ts @@ -1,4 +1,9 @@ -import { type Tree, addDependenciesToPackageJson } from '@nx/devkit'; +import { + type Tree, + addDependenciesToPackageJson, + detectPackageManager, +} from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { eslintVersion, isbotVersion, @@ -13,6 +18,12 @@ import { } from '../../utils/versions'; export function updateDependencies(tree: Tree) { + // Vite depends on esbuild, whose install script only validates the prebuilt + // binary that ships as an optional dependency. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); + return addDependenciesToPackageJson( tree, { diff --git a/packages/rollup/src/utils/ensure-dependencies.spec.ts b/packages/rollup/src/utils/ensure-dependencies.spec.ts index d88b66243be..66b598f1c7c 100644 --- a/packages/rollup/src/utils/ensure-dependencies.spec.ts +++ b/packages/rollup/src/utils/ensure-dependencies.spec.ts @@ -49,6 +49,20 @@ describe('ensureDependencies', () => { ); }); + it('should deny the core-js build script when using babel with pnpm', () => { + (detectPackageManager as jest.Mock).mockReturnValue('pnpm'); + updateJson(tree, 'package.json', (json) => { + json.packageManager = 'pnpm@11.2.2'; + return json; + }); + + ensureDependencies(tree, { compiler: 'babel' }); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toContain( + 'allowBuilds:\n core-js: false' + ); + }); + it('should not write pnpm-workspace.yaml when not using the swc compiler', () => { (detectPackageManager as jest.Mock).mockReturnValue('pnpm'); updateJson(tree, 'package.json', (json) => { diff --git a/packages/rollup/src/utils/ensure-dependencies.ts b/packages/rollup/src/utils/ensure-dependencies.ts index 98038bd17d8..9b1a6da6f85 100644 --- a/packages/rollup/src/utils/ensure-dependencies.ts +++ b/packages/rollup/src/utils/ensure-dependencies.ts @@ -35,6 +35,10 @@ export function ensureDependencies( true ); case 'babel': + // core-js' install script only prints a funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js': false, + }); return addDependenciesToPackageJson( tree, {}, diff --git a/packages/rsbuild/src/generators/init/init.spec.ts b/packages/rsbuild/src/generators/init/init.spec.ts new file mode 100644 index 00000000000..e9d544c3352 --- /dev/null +++ b/packages/rsbuild/src/generators/init/init.spec.ts @@ -0,0 +1,49 @@ +import { updateJson, type Tree } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; + +import { initGenerator } from './init'; + +describe('init', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + describe('pnpm 11 build scripts', () => { + function declareRsbuildCore(version: string) { + updateJson(tree, 'package.json', (json) => { + json.devDependencies = { + ...json.devDependencies, + '@rsbuild/core': version, + }; + return json; + }); + } + + it('should deny the core-js build script when @rsbuild/core v1 is installed', async () => { + declareRsbuildCore('^1.4.0'); + + await withPnpm(tree, '11.2.2', () => initGenerator(tree, {})); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?core-js['"]?: false/ + ); + }); + + it('should not record a core-js decision when @rsbuild/core v2 is installed', async () => { + declareRsbuildCore('^2.0.0'); + + await withPnpm(tree, '11.2.2', () => initGenerator(tree, {})); + + expect(tree.exists('pnpm-workspace.yaml')).toBe(false); + }); + + it('should not record a core-js decision when @rsbuild/core is not installed', async () => { + await withPnpm(tree, '11.2.2', () => initGenerator(tree, {})); + + expect(tree.exists('pnpm-workspace.yaml')).toBe(false); + }); + }); +}); diff --git a/packages/rsbuild/src/generators/init/init.ts b/packages/rsbuild/src/generators/init/init.ts index 7434863583f..5945ac32783 100644 --- a/packages/rsbuild/src/generators/init/init.ts +++ b/packages/rsbuild/src/generators/init/init.ts @@ -1,22 +1,34 @@ -import { addPlugin } from '@nx/devkit/internal'; +import { acknowledgeBuildScripts, addPlugin } from '@nx/devkit/internal'; import { type Tree, type GeneratorCallback, readNxJson, createProjectGraphAsync, addDependenciesToPackageJson, + detectPackageManager, formatFiles, runTasksInSerial, } from '@nx/devkit'; import { InitGeneratorSchema } from './schema'; import { createNodes } from '../../plugins/plugin'; import { nxVersion } from '../../utils/versions'; -import { getRsbuildVersionsForInstalledMajor } from '../../utils/version-utils'; +import { + getInstalledRsbuildMajorVersion, + getRsbuildVersionsForInstalledMajor, +} from '../../utils/version-utils'; import { assertSupportedRsbuildVersion } from '../../utils/assert-supported-rsbuild-version'; export function updateDependencies(tree: Tree, schema: InitGeneratorSchema) { const rsbuildVersions = getRsbuildVersionsForInstalledMajor(tree); + if (getInstalledRsbuildMajorVersion(tree) === 1) { + // @rsbuild/core v1 depends on core-js, whose install script only prints a + // funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js': false, + }); + } + return addDependenciesToPackageJson( tree, {}, diff --git a/packages/rspack/src/generators/init/init.spec.ts b/packages/rspack/src/generators/init/init.spec.ts new file mode 100644 index 00000000000..d09536187d8 --- /dev/null +++ b/packages/rspack/src/generators/init/init.spec.ts @@ -0,0 +1,25 @@ +import '@nx/devkit/internal-testing-utils/mock-project-graph'; + +import { Tree } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; + +import { rspackInitGenerator } from './init'; + +describe('rspackInitGenerator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + rspackInitGenerator(tree, { addPlugin: true }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); +}); diff --git a/packages/rspack/src/generators/init/init.ts b/packages/rspack/src/generators/init/init.ts index 1ce79412c70..da6cd4ce10b 100644 --- a/packages/rspack/src/generators/init/init.ts +++ b/packages/rspack/src/generators/init/init.ts @@ -1,8 +1,9 @@ -import { addPlugin } from '@nx/devkit/internal'; +import { acknowledgeBuildScripts, addPlugin } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, convertNxGenerator, createProjectGraphAsync, + detectPackageManager, GeneratorCallback, readNxJson, runTasksInSerial, @@ -125,6 +126,12 @@ export async function rspackInitGenerator( rspackVersions.rspackDevServerVersion; } + // @nx/rspack depends on sass, which pulls in @parcel/watcher. Its install + // script only builds from source when npm_config_build_from_source is set, + // so skip it. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + '@parcel/watcher': false, + }); const installTask = addDependenciesToPackageJson( tree, {}, diff --git a/packages/storybook/src/generators/configuration/configuration.spec.ts b/packages/storybook/src/generators/configuration/configuration.spec.ts index 10a5f957646..f0b1daaff16 100644 --- a/packages/storybook/src/generators/configuration/configuration.spec.ts +++ b/packages/storybook/src/generators/configuration/configuration.spec.ts @@ -8,6 +8,7 @@ import { updateJson, writeJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { libraryGenerator } from '@nx/js'; @@ -74,6 +75,36 @@ describe('@nx/storybook:configuration', () => { }); }); + it('should deny the build scripts pulled in by @storybook/test-runner', async () => { + await withPnpm(tree, '11.2.2', () => + configurationGenerator(tree, { + project: 'test-ui-lib', + uiFramework: '@storybook/react-vite', + interactionTests: true, + addPlugin: true, + }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]@swc\/core['"]: false/); + expect(pnpmWorkspace).toMatch(/['"]?unrs-resolver['"]?: false/); + }); + + it('should deny the core-js build script added for react-webpack5 libraries', async () => { + await withPnpm(tree, '11.2.2', () => + configurationGenerator(tree, { + project: 'test-ui-lib', + uiFramework: '@storybook/react-webpack5', + interactionTests: false, + addPlugin: true, + }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?core-js['"]?: false/ + ); + }); + it('should add angular related dependencies when using Angular as uiFramework', async () => { const existing = 'existing'; const existingVersion = '1.0.0'; @@ -1469,6 +1500,37 @@ describe('@nx/storybook:configuration', () => { const { devDependencies } = readJson(tree, 'package.json'); expect(devDependencies['@storybook/test-runner']).toBe('^0.23.0'); }); + + it.each(['~8.1.11', '^8.0.0', '9.1.15'])( + 'should not deny unrs-resolver on storybook %s, whose test runner runs on jest 29', + async (storybookRange) => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + await libraryGenerator(tree, { + directory: 'test-ui-lib', + bundler: 'none', + skipFormat: true, + addPlugin: true, + }); + updateJson(tree, 'package.json', (json) => { + json.devDependencies ??= {}; + json.devDependencies['storybook'] = storybookRange; + return json; + }); + + await withPnpm(tree, '11.2.2', () => + configurationGenerator(tree, { + project: 'test-ui-lib', + uiFramework: '@storybook/react-vite', + interactionTests: true, + addPlugin: true, + }) + ); + + const pnpmWorkspace = tree.read('pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatch(/['"]@swc\/core['"]: false/); + expect(pnpmWorkspace).not.toContain('unrs-resolver'); + } + ); }); describe('basic functionalities', () => { diff --git a/packages/storybook/src/generators/configuration/configuration.ts b/packages/storybook/src/generators/configuration/configuration.ts index b396cc43308..fe871da577c 100644 --- a/packages/storybook/src/generators/configuration/configuration.ts +++ b/packages/storybook/src/generators/configuration/configuration.ts @@ -1,5 +1,6 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, GeneratorCallback, logger, @@ -8,6 +9,8 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; +import { gte, minVersion } from 'semver'; import { initGenerator as jsInitGenerator } from '@nx/js'; import { StorybookConfigureSchema } from './schema'; @@ -47,7 +50,7 @@ import { } from '../../utils/versions'; import { ensureDependencies } from './lib/ensure-dependencies'; import { editRootTsConfig } from './lib/edit-root-tsconfig'; -import { getProjectType } from '@nx/js/internal'; +import { acknowledgeSwcBuildScripts, getProjectType } from '@nx/js/internal'; export function configurationGenerator( tree: Tree, @@ -198,7 +201,18 @@ export async function configurationGeneratorInternal( } if (schema.interactionTests) { - devDeps['@storybook/test-runner'] = versions(tree).testRunnerVersion; + const testRunnerVersion = versions(tree).testRunnerVersion; + // @storybook/test-runner depends on @swc/core. + acknowledgeSwcBuildScripts(tree); + if (gte(minVersion(testRunnerVersion), '0.24.0')) { + // Only the 0.24 line runs on jest 30, which reaches unrs-resolver + // through jest-resolve. The lines Storybook 8 and 9 select are on + // jest 29, which resolves without it. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'unrs-resolver': false, + }); + } + devDeps['@storybook/test-runner'] = testRunnerVersion; } if (schema.tsConfiguration) { @@ -217,6 +231,10 @@ export async function configurationGeneratorInternal( projectType !== 'application' && schema.uiFramework === '@storybook/react-webpack5' ) { + // core-js' install script only prints a funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js': false, + }); devDeps['core-js'] = coreJsVersion; } diff --git a/packages/storybook/src/generators/init/init.spec.ts b/packages/storybook/src/generators/init/init.spec.ts index 93b2ab96d96..096e9f88657 100644 --- a/packages/storybook/src/generators/init/init.spec.ts +++ b/packages/storybook/src/generators/init/init.spec.ts @@ -7,6 +7,7 @@ import { readNxJson, } from '@nx/devkit'; import { findTargetDefault } from '@nx/devkit/internal'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { initGenerator } from './init'; @@ -28,6 +29,16 @@ describe('@nx/storybook:init', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); + it('should deny the esbuild build script pulled in by storybook', async () => { + await withPnpm(tree, '11.2.2', () => + initGenerator(tree, { addPlugin: false }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + it('should add build-storybook to cacheable operations if NX_ADD_PLUGINS=false', async () => { await initGenerator(tree, { addPlugin: false, diff --git a/packages/storybook/src/generators/init/init.ts b/packages/storybook/src/generators/init/init.ts index ae2c3961548..e242ae4ae7e 100644 --- a/packages/storybook/src/generators/init/init.ts +++ b/packages/storybook/src/generators/init/init.ts @@ -1,7 +1,12 @@ -import { addPlugin, upsertTargetDefault } from '@nx/devkit/internal'; +import { + acknowledgeBuildScripts, + addPlugin, + upsertTargetDefault, +} from '@nx/devkit/internal'; import { addDependenciesToPackageJson, createProjectGraphAsync, + detectPackageManager, formatFiles, GeneratorCallback, installPackagesTask, @@ -31,6 +36,12 @@ function checkDependenciesInstalled( const storybookVersionToInstall = getStorybookVersionToInstall(host); devDependencies['storybook'] = storybookVersionToInstall; + // storybook depends on esbuild, whose install script only validates the + // prebuilt binary that ships as an optional dependency. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + esbuild: false, + }); + return addDependenciesToPackageJson( host, {}, diff --git a/packages/vite/src/utils/ensure-dependencies.spec.ts b/packages/vite/src/utils/ensure-dependencies.spec.ts index 7910379e97e..bc930863bbe 100644 --- a/packages/vite/src/utils/ensure-dependencies.spec.ts +++ b/packages/vite/src/utils/ensure-dependencies.spec.ts @@ -1,5 +1,5 @@ import { addDependenciesToPackageJson, readJson, type Tree } from '@nx/devkit'; -import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { TempFs, withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { ensureDependencies } from './ensure-dependencies'; import { @@ -43,6 +43,24 @@ describe('@nx/vite:init', () => { expect(packageJson.devDependencies['@vitejs/plugin-react']).toBeUndefined(); }); + it('should deny the @swc/core build script pulled in by the react swc plugin', () => { + withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'react', compiler: 'swc' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }); + + it('should not record a @swc/core decision for the babel react plugin', () => { + withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'react' }) + ); + + expect(tree.exists('pnpm-workspace.yaml')).toBe(false); + }); + it('should add swc plugin for react even with older vite', () => { addDependenciesToPackageJson(tree, {}, { vite: '^7.0.0' }); ensureDependencies(tree, { diff --git a/packages/vite/src/utils/ensure-dependencies.ts b/packages/vite/src/utils/ensure-dependencies.ts index 377f5b530dc..aa7751f01c7 100644 --- a/packages/vite/src/utils/ensure-dependencies.ts +++ b/packages/vite/src/utils/ensure-dependencies.ts @@ -5,6 +5,7 @@ import { type GeneratorCallback, type Tree, } from '@nx/devkit'; +import { acknowledgeSwcBuildScripts } from '@nx/js/internal'; import { coerce, major } from 'semver'; import { ajvVersion, @@ -28,6 +29,8 @@ export function ensureDependencies( if (schema.uiFramework === 'react') { if (schema.compiler === 'swc') { + // @vitejs/plugin-react-swc depends on @swc/core. + acknowledgeSwcBuildScripts(host); devDependencies['@vitejs/plugin-react-swc'] = vitePluginReactSwcVersion; } else { // @vitejs/plugin-react v6 requires Vite 8+, use v4 for older versions. diff --git a/packages/vitest/src/generators/init/init.spec.ts b/packages/vitest/src/generators/init/init.spec.ts index 2317998e22b..aaec9e07349 100644 --- a/packages/vitest/src/generators/init/init.spec.ts +++ b/packages/vitest/src/generators/init/init.spec.ts @@ -6,6 +6,7 @@ import { updateJson, NxJsonConfiguration, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { initGenerator } from './init'; @@ -29,6 +30,28 @@ describe('@nx/vitest:init', () => { tree = createTreeWithEmptyWorkspace(); }); + describe('pnpm 11 build scripts', () => { + it('should deny the esbuild build script when installing vite below 8', async () => { + await withPnpm(tree, '11.2.2', () => + initGenerator(tree, { skipFormat: true, viteVersion: 7 }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?esbuild['"]?: false/ + ); + }); + + it('should not record an esbuild decision for vite 8, which uses rolldown', async () => { + await withPnpm(tree, '11.2.2', () => + initGenerator(tree, { skipFormat: true }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'esbuild' + ); + }); + }); + it('should add the plugin by default when addPlugin is not provided', async () => { await initGenerator(tree, { skipFormat: true, diff --git a/packages/vitest/src/generators/init/init.ts b/packages/vitest/src/generators/init/init.ts index 5ba79aad348..7bca9b55063 100644 --- a/packages/vitest/src/generators/init/init.ts +++ b/packages/vitest/src/generators/init/init.ts @@ -1,4 +1,5 @@ import { + acknowledgeBuildScripts, addPlugin, upsertTargetDefault, findTargetDefault, @@ -6,6 +7,7 @@ import { import { type Tree, type GeneratorCallback, + detectPackageManager, readNxJson, addDependenciesToPackageJson, formatFiles, @@ -13,6 +15,7 @@ import { updateNxJson, createProjectGraphAsync, } from '@nx/devkit'; +import { coerce, major } from 'semver'; import { InitGeneratorSchema } from './schema'; import { nxVersion, @@ -43,6 +46,15 @@ export function updateDependencies(tree: Tree, schema: InitGeneratorSchema) { ? viteV7Version : viteVersion; + // Vite below 8 depends on esbuild (8 bundles rolldown instead), whose install + // script only validates the prebuilt binary that ships as an optional + // dependency. + if (major(coerce(viteVersionToUse)) < 8) { + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + esbuild: false, + }); + } + return addDependenciesToPackageJson( tree, {}, diff --git a/packages/vitest/src/utils/ensure-dependencies.spec.ts b/packages/vitest/src/utils/ensure-dependencies.spec.ts new file mode 100644 index 00000000000..c69f42de866 --- /dev/null +++ b/packages/vitest/src/utils/ensure-dependencies.spec.ts @@ -0,0 +1,32 @@ +import { type Tree } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { ensureDependencies } from './ensure-dependencies'; + +describe('ensureDependencies', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should deny the @swc/core build script pulled in by the react swc plugin', async () => { + await withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'react', compiler: 'swc' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }); + + it('should not record a @swc/core decision for the babel react plugin', async () => { + await withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'react' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + '@swc/core' + ); + }); +}); diff --git a/packages/vitest/src/utils/ensure-dependencies.ts b/packages/vitest/src/utils/ensure-dependencies.ts index 81f78e8f0f1..15f513d14b9 100644 --- a/packages/vitest/src/utils/ensure-dependencies.ts +++ b/packages/vitest/src/utils/ensure-dependencies.ts @@ -6,6 +6,7 @@ import { type GeneratorCallback, type Tree, } from '@nx/devkit'; +import { acknowledgeSwcBuildScripts } from '@nx/js/internal'; import { coerce, major } from 'semver'; import { ajvVersion, @@ -56,6 +57,8 @@ export async function ensureDependencies( if (schema.uiFramework === 'react') { if (schema.compiler === 'swc') { + // @vitejs/plugin-react-swc depends on @swc/core. + acknowledgeSwcBuildScripts(tree); devDependencies['@vitejs/plugin-react-swc'] = vitePluginReactSwcVersion; } else { // @vitejs/plugin-react v6 requires Vite 8+, use v4 for older versions. diff --git a/packages/vue/src/generators/application/application.spec.ts b/packages/vue/src/generators/application/application.spec.ts index ccdc422a115..aa7e1dfe1d0 100644 --- a/packages/vue/src/generators/application/application.spec.ts +++ b/packages/vue/src/generators/application/application.spec.ts @@ -16,6 +16,7 @@ import * as devkitExports from '@nx/devkit'; import { applicationGenerator } from './application'; import { Schema } from './schema'; import { PackageManagerCommands } from '@nx/devkit/internal'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; describe('application generator', () => { let tree: Tree; @@ -39,6 +40,26 @@ describe('application generator', () => { } }); + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { ...options, style: 'scss' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + + it('should not record a @parcel/watcher decision without scss', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { ...options, style: 'css' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + '@parcel/watcher' + ); + }); + it('should run successfully', async () => { await applicationGenerator(tree, options); const config = readProjectConfiguration(tree, 'test'); diff --git a/packages/vue/src/utils/ensure-dependencies.ts b/packages/vue/src/utils/ensure-dependencies.ts index d12acf8684a..8847508cab3 100644 --- a/packages/vue/src/utils/ensure-dependencies.ts +++ b/packages/vue/src/utils/ensure-dependencies.ts @@ -1,8 +1,10 @@ import { addDependenciesToPackageJson, + detectPackageManager, type GeneratorCallback, type Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { sassVersion, vitePluginVueVersion, @@ -32,6 +34,11 @@ export function ensureDependencies( } if (options.style === 'scss') { + // sass pulls in @parcel/watcher, whose install script only builds from + // source when npm_config_build_from_source is set. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + '@parcel/watcher': false, + }); devDependencies['sass'] = sassVersion; } diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index e642616dfa3..267670f35e1 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -11,6 +11,7 @@ import { getProjects, readJson, } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import * as devkitExports from '@nx/devkit'; @@ -636,6 +637,22 @@ describe('app', () => { const tsconfig = readJson(tree, 'my-app/tsconfig.json'); expect(tsconfig.compilerOptions.strict).toBeTruthy(); }); + + it('should deny the @swc/core build script for the swc compiler', async () => { + await withPnpm(tree, '11.2.2', () => + applicationGenerator(tree, { + directory: 'my-app', + compiler: 'swc', + bundler: 'none', + unitTestRunner: 'none', + addPlugin: true, + } as Schema) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@swc\/core['"]: false/ + ); + }); }); describe('setup web app with --bundler=vite', () => { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index d8f06bd3caa..f85dea58c52 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -5,10 +5,12 @@ import { logShowProjectCommand, E2EWebServerDetails, type PackageJson, + acknowledgeBuildScripts, } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, addProjectConfiguration, + detectPackageManager, ensurePackage, formatFiles, generateFiles, @@ -635,6 +637,11 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { target: 'es2016', }, }); + // @swc/core's postinstall only installs a wasm fallback for platforms not + // covered by its prebuilt optional dependencies, so skip it. + acknowledgeBuildScripts(host, detectPackageManager(host.root), { + '@swc/core': false, + }); const installTask = addDependenciesToPackageJson( host, {}, diff --git a/packages/webpack/src/generators/init/init.spec.ts b/packages/webpack/src/generators/init/init.spec.ts index c41438484b8..cbf7f05d6f9 100644 --- a/packages/webpack/src/generators/init/init.spec.ts +++ b/packages/webpack/src/generators/init/init.spec.ts @@ -1,6 +1,7 @@ import '@nx/devkit/internal-testing-utils/mock-project-graph'; import { readJson, Tree, updateJson } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { webpackInitGenerator } from './init'; @@ -12,6 +13,16 @@ describe('webpackInitGenerator', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); + it('should deny the @parcel/watcher build script pulled in by sass', async () => { + await withPnpm(tree, '11.2.2', () => + webpackInitGenerator(tree, { addPlugin: true }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]@parcel\/watcher['"]: false/ + ); + }); + it('should install plugin, webpack, webpack-dev-server, and webpack-cli', async () => { await webpackInitGenerator(tree, { addPlugin: true, diff --git a/packages/webpack/src/generators/init/init.ts b/packages/webpack/src/generators/init/init.ts index 3677c17ae25..b02906a014f 100644 --- a/packages/webpack/src/generators/init/init.ts +++ b/packages/webpack/src/generators/init/init.ts @@ -1,6 +1,7 @@ -import { addPlugin } from '@nx/devkit/internal'; +import { acknowledgeBuildScripts, addPlugin } from '@nx/devkit/internal'; import { addDependenciesToPackageJson, + detectPackageManager, createProjectGraphAsync, formatFiles, GeneratorCallback, @@ -97,6 +98,12 @@ export async function webpackInitGeneratorInternal(tree: Tree, schema: Schema) { devDependencies['webpack-cli'] = webpackCliVersion; } + // @nx/webpack depends on sass, which pulls in @parcel/watcher. Its install + // script only builds from source when npm_config_build_from_source is set, + // so skip it. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + '@parcel/watcher': false, + }); installTask = addDependenciesToPackageJson( tree, {}, diff --git a/packages/webpack/src/utils/ensure-dependencies.spec.ts b/packages/webpack/src/utils/ensure-dependencies.spec.ts new file mode 100644 index 00000000000..c48bb19f7a5 --- /dev/null +++ b/packages/webpack/src/utils/ensure-dependencies.spec.ts @@ -0,0 +1,32 @@ +import { type Tree } from '@nx/devkit'; +import { withPnpm } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { ensureDependencies } from './ensure-dependencies'; + +describe('ensureDependencies', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should deny the core-js-pure build script pulled in by the react refresh plugin', () => { + withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'react' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatch( + /['"]?core-js-pure['"]?: false/ + ); + }); + + it('should not record a core-js-pure decision without react', () => { + withPnpm(tree, '11.2.2', () => + ensureDependencies(tree, { uiFramework: 'none' }) + ); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8') ?? '').not.toContain( + 'core-js-pure' + ); + }); +}); diff --git a/packages/webpack/src/utils/ensure-dependencies.ts b/packages/webpack/src/utils/ensure-dependencies.ts index 43c5cd13d43..d53a996993d 100644 --- a/packages/webpack/src/utils/ensure-dependencies.ts +++ b/packages/webpack/src/utils/ensure-dependencies.ts @@ -1,9 +1,11 @@ import { addDependenciesToPackageJson, + detectPackageManager, runTasksInSerial, type GeneratorCallback, type Tree, } from '@nx/devkit'; +import { acknowledgeBuildScripts } from '@nx/devkit/internal'; import { addSwcDependencies } from '@nx/js/internal'; import { reactRefreshVersion, @@ -36,6 +38,11 @@ export function ensureDependencies( } if (options.uiFramework === 'react') { + // @pmmmwh/react-refresh-webpack-plugin depends on core-js-pure, whose + // install script only prints a funding message. + acknowledgeBuildScripts(tree, detectPackageManager(tree.root), { + 'core-js-pure': false, + }); devDependencies['@pmmmwh/react-refresh-webpack-plugin'] = reactRefreshWebpackPluginVersion; devDependencies['@svgr/webpack'] = svgrWebpackVersion; diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index 4a4f93d9cff..b99baa389f6 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -16,8 +16,14 @@ import yargsParser from 'yargs-parser'; import { fork, ForkOptions } from 'child_process'; import { getNxRequirePaths } from '@nx/devkit/internal'; -export function addPresetDependencies(host: Tree, options: NormalizedSchema) { - const { dependencies, dev } = getPresetDependencies(options); +export type PresetDependencies = ReturnType; + +export function addPresetDependencies( + host: Tree, + options: NormalizedSchema, + presetDependencies: PresetDependencies +) { + const { dependencies, dev } = presetDependencies; return addDependenciesToPackageJson( host, dependencies, @@ -27,6 +33,57 @@ export function addPresetDependencies(host: Tree, options: NormalizedSchema) { ); } +// Install scripts each preset dependency pulls in transitively. `nx new` +// installs the preset dependencies before the preset generator runs, so the +// generators that would otherwise record these decisions come too late. +const presetDependencyBuildScripts: Record> = { + '@nx/angular-rspack': { + // Its @nx/angular-rspack-compiler dependency patches `@angular/build` + // versions below 20.2.0, so that one has to run. The rest come from the + // compiler's `@angular/build` and `@rsbuild/core` peers. + '@nx/angular-rspack-compiler': true, + 'core-js': false, + esbuild: false, + lmdb: false, + 'msgpackr-extract': false, + '@parcel/watcher': false, + }, + // @nx/express and @nx/nest depend on @nx/node, which depends on @nx/jest, + // which pulls in jest 30 -> unrs-resolver. + '@nx/express': { 'unrs-resolver': false }, + '@nx/jest': { 'unrs-resolver': false }, + '@nx/nest': { 'unrs-resolver': false }, + '@nx/node': { 'unrs-resolver': false }, + // Both optionally depend on @nx/detox, which depends on @nx/jest. pnpm + // installs optionalDependencies by default. + '@nx/expo': { 'unrs-resolver': false }, + '@nx/react-native': { 'unrs-resolver': false }, + // Both depend on sass, which pulls in @parcel/watcher. + '@nx/rspack': { '@parcel/watcher': false }, + '@nx/webpack': { '@parcel/watcher': false }, +}; + +export function getPresetBuildScripts({ + dependencies, + dev, +}: PresetDependencies): Record { + // The conditional entries are keyed unconditionally and left `undefined` when + // they don't apply, so the version is what says a package gets installed. + const declared: Record = { + ...dependencies, + ...dev, + }; + + const buildScripts: Record = {}; + for (const pkg of Object.keys(declared)) { + if (declared[pkg]) { + Object.assign(buildScripts, presetDependencyBuildScripts[pkg]); + } + } + + return buildScripts; +} + export function generatePreset(host: Tree, opts: NormalizedSchema) { const parsedArgs = yargsParser(process.argv, { boolean: ['interactive'], @@ -117,7 +174,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { // `typescript` is pinned here rather than left to `@nx/js:init` so it lands in // package.json before the first install. Otherwise npm resolves tsquery's // `typescript: >3.0.0` peer to 7.x, whose entry point dropped the compiler API. -function getPresetDependencies({ +export function getPresetDependencies({ preset, presetVersion, bundler, diff --git a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts index 9f21d3b2880..bd8816b357c 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts @@ -400,6 +400,156 @@ describe('@nx/workspace:generateWorkspaceFiles', () => { expect(tree.exists('proj/.npmrc')).toBeFalsy(); }); + it('should record the build scripts the preset dependencies pull in for pnpm 11+', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.AngularMonorepo, + bundler: 'rspack', + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + allowBuilds: + nx: true + '@parcel/watcher': false + '@nx/angular-rspack-compiler': true + core-js: false + esbuild: false + lmdb: false + msgpackr-extract: false + " + `); + }); + + it('should not record the build scripts of preset dependencies that are not installed', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.AngularMonorepo, + bundler: 'webpack', + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + allowBuilds: + nx: true + " + `); + }); + + it('should only list the build scripts to run for pnpm 10.6.0+', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('10.6.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.AngularMonorepo, + bundler: 'rspack', + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + onlyBuiltDependencies: + - nx + - '@nx/angular-rspack-compiler' + " + `); + }); + + it('should record the build scripts of a preset that installs @nx/webpack and @nx/jest', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.ReactMonorepo, + bundler: 'webpack', + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + workspaces: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + allowBuilds: + nx: true + unrs-resolver: false + '@parcel/watcher': false + " + `); + }); + + it('should record the build scripts the react-native preset pulls in through @nx/detox', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.ReactNative, + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + workspaces: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + allowBuilds: + nx: true + unrs-resolver: false + " + `); + }); + + it('should record the build scripts the expo preset pulls in through @nx/detox', async () => { + tree.write('proj/package.json', JSON.stringify({})); + jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); + + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.Expo, + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + workspaces: false, + }); + + const pnpmWorkspace = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(pnpmWorkspace).toMatchInlineSnapshot(` + "autoInstallPeers: true + allowBuilds: + nx: true + unrs-resolver: false + " + `); + }); + it('should configure the pnpm settings in pnpm-workspace.yaml with allowBuilds for pnpm 11+', async () => { tree.write('proj/package.json', JSON.stringify({})); jest.spyOn(devkit, 'getPackageManagerVersion').mockReturnValue('11.0.0'); diff --git a/packages/workspace/src/generators/new/generate-workspace-files.ts b/packages/workspace/src/generators/new/generate-workspace-files.ts index fd923b3cd8b..e18647216dd 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.ts @@ -13,11 +13,17 @@ import { createNxCloudOnboardingURL, setupAiAgentsGenerator, } from '@nx/devkit/internal'; +import { dump } from '@zkochan/js-yaml'; import { join } from 'path'; import { gte } from 'semver'; import { deduceDefaultBase } from '../../utilities/default-base'; import { nxVersion } from '../../utils/versions'; import { Preset } from '../utils/presets'; +import { + getPresetBuildScripts, + getPresetDependencies, + type PresetDependencies, +} from './generate-preset'; import type { NormalizedSchema } from './new'; type PresetInfo = { @@ -197,10 +203,14 @@ export async function generateWorkspaceFiles( }); } + // Resolving a custom preset's version costs a registry round trip, so this + // is computed once and handed back for `addPresetDependencies` to reuse. + const presetDependencies = getPresetDependencies(options); + const [packageMajor] = packageManagerVersion.split('.'); if (options.packageManager === 'pnpm' && +packageMajor >= 7) { if (gte(packageManagerVersion, '10.6.0')) { - addPnpmSettings(tree, options, packageManagerVersion); + addPnpmSettings(tree, options, packageManagerVersion, presetDependencies); } else { createNpmrc(tree, options); } @@ -215,7 +225,7 @@ export async function generateWorkspaceFiles( addNpmScripts(tree, options); setUpWorkspacesInPackageJson(tree, options); - return { token, aiAgentsCallback }; + return { token, aiAgentsCallback, presetDependencies }; } function setPresetProperty(tree: Tree, options: NormalizedSchema) { @@ -342,15 +352,26 @@ async function createReadme( function addPnpmSettings( tree: Tree, options: NormalizedSchema, - packageManagerVersion: string + packageManagerVersion: string, + presetDependencies: PresetDependencies ) { + const buildScripts = { + nx: true, + ...getPresetBuildScripts(presetDependencies), + }; + // pnpm 10 only understands the allow-list form, so a denied package is simply + // left out. pnpm 11 needs an explicit decision for every scripted package. const buildAllowlist = gte(packageManagerVersion, '11.0.0') - ? `allowBuilds:\n nx: true` - : `onlyBuiltDependencies:\n - nx`; + ? { allowBuilds: buildScripts } + : { + onlyBuiltDependencies: Object.keys(buildScripts).filter( + (pkg) => buildScripts[pkg] + ), + }; tree.write( join(options.directory, 'pnpm-workspace.yaml'), - `autoInstallPeers: true\n${buildAllowlist}\n` + dump({ autoInstallPeers: true, ...buildAllowlist }) ); } diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index d0dcd419f2b..95883e9fd8b 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -66,14 +66,12 @@ export async function newGenerator(tree: Tree, opts: Schema) { const options = normalizeOptions(opts); validateOptions(options, tree); - const { token, aiAgentsCallback } = await generateWorkspaceFiles( - tree, - options - ); + const { token, aiAgentsCallback, presetDependencies } = + await generateWorkspaceFiles(tree, options); options.nxCloudToken = token; - addPresetDependencies(tree, options); + addPresetDependencies(tree, options, presetDependencies); await formatFiles(tree);