diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60e3cd99ac57..184af28243cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,7 @@ jobs: # flaky targets are retried. The larger machine type comes with 2x more SSD space. os: [ubuntu-latest-4core] node: [22] - subset: [yarn, pnpm] + subset: [yarn, pnpm, bun] shard: [0, 1, 2] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index aab59a7bc029..9130c289c1e6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -182,7 +182,7 @@ jobs: # flaky targets are retried. The larger machine type comes with 2x more SSD space. os: [ubuntu-latest-4core] node: [22] - subset: [yarn, pnpm] + subset: [yarn, pnpm, bun] shard: [0, 1, 2] runs-on: ${{ matrix.os }} steps: diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index 1ea2fff699c6..aec0014b9114 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -493,7 +493,9 @@ export default class AddCommandModule // Only show if installation will actually occur task.title = 'Installing package'; - if (context.savePackage === false) { + if (context.savePackage === false && packageManager.name !== PackageManager.Bun) { + // Bun has a `--no-save` option which we are using to + // install the package and not update the package.json and the lock file. task.title += ' in temporary location'; // Temporary packages are located in a different directory diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts index 54b5a21df4de..b913a3bfd72d 100644 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -61,7 +61,7 @@ export class PackageManagerUtils { /** Install a single package. */ async install( packageName: string, - save: 'dependencies' | 'devDependencies' | true = true, + save: 'dependencies' | 'devDependencies' | boolean = true, extraArgs: string[] = [], cwd?: string, ): Promise { @@ -70,6 +70,8 @@ export class PackageManagerUtils { if (save === 'devDependencies') { installArgs.push(packageManagerArgs.saveDev); + } else if (save === false) { + installArgs.push(packageManagerArgs.noLockfile); } return this.run([...installArgs, ...extraArgs], { cwd, silent: true }); @@ -158,11 +160,11 @@ export class PackageManagerUtils { }; case PackageManager.Bun: return { - saveDev: '--development', + saveDev: '--dev', install: 'add', installAll: 'install', prefix: '--cwd', - noLockfile: '', + noLockfile: '--no-save', }; default: return { diff --git a/tests/legacy-cli/e2e/assets/BUILD.bazel b/tests/legacy-cli/e2e/assets/BUILD.bazel index 946db62d0d5a..11bc738d4a29 100644 --- a/tests/legacy-cli/e2e/assets/BUILD.bazel +++ b/tests/legacy-cli/e2e/assets/BUILD.bazel @@ -2,6 +2,9 @@ load("//tools:defaults.bzl", "copy_to_bin") copy_to_bin( name = "assets", - srcs = glob(["**"]), + srcs = glob( + include = ["**"], + exclude = ["BUILD.bazel"], + ), visibility = ["//visibility:public"], ) diff --git a/tests/legacy-cli/e2e/assets/add-collection/collection.json b/tests/legacy-cli/e2e/assets/add-collection-dir/collection.json similarity index 100% rename from tests/legacy-cli/e2e/assets/add-collection/collection.json rename to tests/legacy-cli/e2e/assets/add-collection-dir/collection.json diff --git a/tests/legacy-cli/e2e/assets/add-collection/index.js b/tests/legacy-cli/e2e/assets/add-collection-dir/index.js similarity index 100% rename from tests/legacy-cli/e2e/assets/add-collection/index.js rename to tests/legacy-cli/e2e/assets/add-collection-dir/index.js diff --git a/tests/legacy-cli/e2e/assets/add-collection/package.json b/tests/legacy-cli/e2e/assets/add-collection-dir/package.json similarity index 100% rename from tests/legacy-cli/e2e/assets/add-collection/package.json rename to tests/legacy-cli/e2e/assets/add-collection-dir/package.json diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts index 780c0bbfaf39..9f587fa5c38d 100644 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ b/tests/legacy-cli/e2e/setup/100-global-cli.ts @@ -6,7 +6,7 @@ const PACKAGE_MANAGER_VERSION = { 'npm': '10.8.1', 'yarn': '1.22.22', 'pnpm': '10.17.1', - 'bun': '1.2.21', + 'bun': '1.3.2', }; export default async function () { diff --git a/tests/legacy-cli/e2e/tests/commands/add/base.ts b/tests/legacy-cli/e2e/tests/commands/add/base.ts index f4e7048df6ac..d31210c6c242 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/base.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/base.ts @@ -4,7 +4,7 @@ import { ng } from '../../../utils/process'; import { expectToFail } from '../../../utils/utils'; export default async function () { - await symlinkFile(assetDir('add-collection'), `./node_modules/add-collection`, 'dir'); + await symlinkFile(assetDir('add-collection-dir'), `./node_modules/add-collection`, 'dir'); await ng('add', 'add-collection'); await expectFileToExist('empty-file'); diff --git a/tests/legacy-cli/e2e/tests/commands/add/dir.ts b/tests/legacy-cli/e2e/tests/commands/add/dir.ts index f5fadc486b3d..7cb00704cc8e 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/dir.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/dir.ts @@ -1,8 +1,19 @@ +import { cp } from 'node:fs/promises'; +import { resolve } from 'node:path'; import { assetDir } from '../../../utils/assets'; import { expectFileToExist } from '../../../utils/fs'; import { ng } from '../../../utils/process'; export default async function () { - await ng('add', assetDir('add-collection'), '--name=blah', '--skip-confirmation'); + const collectionName = 'add-collection-dir'; + const dirCollectionPath = resolve(collectionName); + + // Copy locally as bun doesn't install the dependency correctly if it has symlinks. + await cp(assetDir(collectionName), dirCollectionPath, { + recursive: true, + dereference: true, + }); + + await ng('add', dirCollectionPath, '--name=blah', '--skip-confirmation'); await expectFileToExist('blah'); } diff --git a/tests/legacy-cli/e2e/tests/commands/add/peer.ts b/tests/legacy-cli/e2e/tests/commands/add/peer.ts index d085efadf080..143542e4533c 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/peer.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/peer.ts @@ -1,24 +1,44 @@ import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { cp } from 'node:fs/promises'; import { assetDir } from '../../../utils/assets'; import { ng } from '../../../utils/process'; -const warning = 'Adding the package may not succeed.'; +export default async function (): Promise { + const warning = /Adding the package may not succeed/; -export default async function () { - const { stdout: bad } = await ng( - 'add', - assetDir('add-collection-peer-bad'), - '--skip-confirmation', + const stdout1 = await runNgAdd('add-collection-peer-bad'); + assert.match( + stdout1, + warning, + `Peer warning should be shown for add-collection-peer-bad but was not.`, ); - assert.match(bad, new RegExp(warning), 'peer warning not shown on bad package'); - const { stdout: base } = await ng('add', assetDir('add-collection'), '--skip-confirmation'); - assert.doesNotMatch(base, new RegExp(warning), 'peer warning shown on base package'); + const stdout2 = await runNgAdd('add-collection-dir'); + assert.doesNotMatch( + stdout2, + warning, + `Peer warning should NOT be shown for add-collection-dir but was.`, + ); - const { stdout: good } = await ng( - 'add', - assetDir('add-collection-peer-good'), - '--skip-confirmation', + const stdout3 = await runNgAdd('add-collection-peer-good'); + assert.doesNotMatch( + stdout3, + warning, + `Peer warning should NOT be shown for add-collection-peer-good but was.`, ); - assert.doesNotMatch(good, new RegExp(warning), 'peer warning shown on good package'); +} + +async function runNgAdd(collectionName: string): Promise { + const collectionPath = resolve(collectionName); + + // Copy locally as bun doesn't install the dependency correctly if it has symlinks. + await cp(assetDir(collectionName), collectionPath, { + recursive: true, + dereference: true, + }); + + const { stdout } = await ng('add', collectionPath, '--skip-confirmation'); + + return stdout; } diff --git a/tests/legacy-cli/e2e/tests/commands/add/secure-registry.ts b/tests/legacy-cli/e2e/tests/commands/add/secure-registry.ts index a27ba708637d..95218fd653d9 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/secure-registry.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/secure-registry.ts @@ -1,42 +1,49 @@ -import { expectFileNotToExist, expectFileToExist } from '../../../utils/fs'; +import { expectFileNotToExist, expectFileToExist, rimraf } from '../../../utils/fs'; import { getActivePackageManager, installWorkspacePackages } from '../../../utils/packages'; import { git, ng } from '../../../utils/process'; import { createNpmConfigForAuthentication } from '../../../utils/registry'; import { expectToFail } from '../../../utils/utils'; export default async function () { - // The environment variable has priority over the .npmrc - delete process.env['NPM_CONFIG_REGISTRY']; - const isNpm = getActivePackageManager() === 'npm'; + const originalNpmConfigRegistry = process.env['NPM_CONFIG_REGISTRY']; + try { + // The environment variable has priority over the .npmrc + delete process.env['NPM_CONFIG_REGISTRY']; + const packageManager = getActivePackageManager(); + const supportsUnscopedAuth = packageManager !== 'bun' && packageManager !== 'npm'; + const command = ['add', '@angular/pwa', '--skip-confirmation']; - const command = ['add', '@angular/pwa', '--skip-confirmation']; - await expectFileNotToExist('public/manifest.webmanifest'); + await expectFileNotToExist('public/manifest.webmanifest'); - // Works with unscoped registry authentication details - if (!isNpm) { - // NPM no longer support unscoped. - await createNpmConfigForAuthentication(false); + // Works with unscoped registry authentication details + if (supportsUnscopedAuth) { + // Some package managers such as Bun and NPM do not support unscoped auth. + await createNpmConfigForAuthentication(false); + await ng(...command); + await expectFileToExist('public/manifest.webmanifest'); + await git('clean', '-dxf'); + } + + // Works with scoped registry authentication details + await expectFileNotToExist('public/manifest.webmanifest'); + + await createNpmConfigForAuthentication(true); await ng(...command); await expectFileToExist('public/manifest.webmanifest'); await git('clean', '-dxf'); - } - // Works with scoped registry authentication details - await expectFileNotToExist('public/manifest.webmanifest'); - await createNpmConfigForAuthentication(true); - await ng(...command); - await expectFileToExist('public/manifest.webmanifest'); + // Invalid authentication token + if (!supportsUnscopedAuth) { + // Some package managers such as Bun and NPM do not support unscoped auth. + await createNpmConfigForAuthentication(false, true); + await expectToFail(() => ng(...command)); + } - // Invalid authentication token - if (isNpm) { - // NPM no longer support unscoped. - await createNpmConfigForAuthentication(false, true); + await createNpmConfigForAuthentication(true, true); await expectToFail(() => ng(...command)); + } finally { + process.env['NPM_CONFIG_REGISTRY'] = originalNpmConfigRegistry; + await git('clean', '-dxf'); + await installWorkspacePackages(); } - - await createNpmConfigForAuthentication(true, true); - await expectToFail(() => ng(...command)); - - await git('clean', '-dxf'); - await installWorkspacePackages(); }