Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions e2e/next/src/next-generation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
checkFilesDoNotExist,
checkFilesExist,
getSelectedPackageManager,
readFile,
runCLI,
uniq,
Expand Down Expand Up @@ -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');
Expand Down
23 changes: 23 additions & 0 deletions e2e/web/src/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
checkFilesExist,
cleanupProject,
createFile,
getSelectedPackageManager,
isNotWindows,
killPorts,
listFiles,
Expand Down Expand Up @@ -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);
});
16 changes: 16 additions & 0 deletions packages/angular/src/generators/application/application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -151,6 +152,7 @@ export async function applicationGenerator(
devDependencies['less'] = packageVersions.lessVersion;
}
if (Object.keys(devDependencies).length) {
acknowledgeAngularBuildScripts(tree);
addDependenciesToPackageJson(tree, {}, devDependencies, undefined, true);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
acknowledgeBuildScripts,
selectPrompt,
forEachExecutorOptions,
getNamedInputs,
} from '@nx/devkit/internal';
import {
addDependenciesToPackageJson,
detectPackageManager,
ensurePackage,
formatFiles,
joinPathFragments,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
{},
Expand Down
32 changes: 32 additions & 0 deletions packages/angular/src/generators/library/library.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/src/generators/library/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function libraryGenerator(
(libraryOptions.buildable || libraryOptions.publishable) &&
!libraryOptions.skipPackageJson
) {
acknowledgeAngularBuildScripts(tree);
addDependenciesToPackageJson(
tree,
{},
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
}
3 changes: 3 additions & 0 deletions packages/angular/src/generators/utils/add-vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function addVitestAngular(
const angularDevkitVersion =
getInstalledAngularDevkitVersion(tree) ?? pkgVersions.angularDevkitVersion;

acknowledgeAngularBuildScripts(tree);
return addDependenciesToPackageJson(
tree,
{},
Expand Down Expand Up @@ -133,6 +135,7 @@ export async function addVitestAnalog(
devDependencies['jsdom'] = pkgVersions.jsdomVersion;
}

acknowledgeAngularBuildScripts(tree);
tasks.push(
addDependenciesToPackageJson(tree, {}, devDependencies, undefined, true)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -28,7 +30,7 @@ After (`package.json`):
// package.json
{
"devDependencies": {
"@angular/build": "~22.0.4",
"@angular/build": "~22.1.0",
},
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +68,8 @@ export default async function (tree: Tree) {
return;
}

acknowledgeAngularBuildScripts(tree);

return addDependenciesToPackageJson(
tree,
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading
Loading