From 2251d821a9fa4434569edab790e68bd53554f6a8 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Mon, 18 Aug 2025 22:08:46 +0200 Subject: [PATCH 1/4] feat(@angular/cli): allow using Deno as package manager --- .../cli/lib/config/workspace-schema.json | 4 +- .../src/commands/update/schematic/schema.json | 2 +- .../cli/src/utilities/package-manager.ts | 44 ++++++++++++++++--- packages/angular/create/README.md | 6 +++ packages/angular/create/src/index.ts | 2 +- .../tasks/package-manager/executor.ts | 6 +++ .../schematics_cli/blank/schema.json | 2 +- .../schematics_cli/schematic/schema.json | 2 +- .../schematics/angular/ng-new/schema.json | 2 +- .../schematics/angular/workspace/schema.json | 2 +- tests/legacy-cli/e2e.bzl | 5 ++- tests/legacy-cli/e2e/setup/100-global-cli.ts | 3 +- .../e2e/tests/misc/create-angular.ts | 6 ++- tests/legacy-cli/e2e/utils/packages.ts | 12 ++++- tests/legacy-cli/e2e/utils/process.ts | 4 ++ 15 files changed, 81 insertions(+), 21 deletions(-) diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index 3fede1746559..dde97e1ead8f 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -47,7 +47,7 @@ "packageManager": { "description": "Specify which package manager tool to use.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun", "deno"] }, "warnings": { "description": "Control CLI specific console warnings", @@ -101,7 +101,7 @@ "packageManager": { "description": "Specify which package manager tool to use.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun", "deno"] }, "warnings": { "description": "Control CLI specific console warnings", diff --git a/packages/angular/cli/src/commands/update/schematic/schema.json b/packages/angular/cli/src/commands/update/schematic/schema.json index 4768df46f2d5..97a5e67a7654 100644 --- a/packages/angular/cli/src/commands/update/schematic/schema.json +++ b/packages/angular/cli/src/commands/update/schematic/schema.json @@ -57,7 +57,7 @@ "description": "The preferred package manager configuration files to use for registry settings.", "type": "string", "default": "npm", - "enum": ["npm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun", "deno"] } }, "required": [] diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts index b913a3bfd72d..a5a794f53915 100644 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -23,6 +23,7 @@ const LOCKFILE_NAMES: Readonly { const { cwd = process.cwd(), silent = false } = options; return new Promise((resolve) => { - const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; + const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer; }[] = []; const childProcess = spawn(`${this.name} ${args.join(' ')}`, { // Always pipe stderr to allow for failures to be reported @@ -212,7 +221,8 @@ export class PackageManagerUtils { @memoize private getVersion(name: PackageManager): string | undefined { try { - return execSync(`${name} --version`, { + const versionArg = name !== PackageManager.Deno ? '--version' : '-v'; + const version = execSync(`${name} ${versionArg}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: { @@ -222,6 +232,13 @@ export class PackageManagerUtils { NPM_CONFIG_UPDATE_NOTIFIER: 'false', }, }).trim(); + + if (name === PackageManager.Deno) { + // Deno CLI outputs "deno 2.4.4" + return version.replace('deno ', ''); + } + + return version; } catch { return undefined; } @@ -239,6 +256,7 @@ export class PackageManagerUtils { const hasYarnLock = this.hasLockfile(PackageManager.Yarn, filesInRoot); const hasPnpmLock = this.hasLockfile(PackageManager.Pnpm, filesInRoot); const hasBunLock = this.hasLockfile(PackageManager.Bun, filesInRoot); + const hasDenoLock = this.hasLockfile(PackageManager.Deno, filesInRoot); // PERF NOTE: `this.getVersion` spawns the package a the child_process which can take around ~300ms at times. // Therefore, we should only call this method when needed. IE: don't call `this.getVersion(PackageManager.Pnpm)` unless truly needed. @@ -246,7 +264,13 @@ export class PackageManagerUtils { if (hasNpmLock) { // Has NPM lock file. - if (!hasYarnLock && !hasPnpmLock && !hasBunLock && this.getVersion(PackageManager.Npm)) { + if ( + !hasYarnLock && + !hasPnpmLock && + !hasBunLock && + !hasDenoLock && + this.getVersion(PackageManager.Npm) + ) { // Only NPM lock file and NPM binary is available. return PackageManager.Npm; } @@ -261,6 +285,9 @@ export class PackageManagerUtils { } else if (hasBunLock && this.getVersion(PackageManager.Bun)) { // Bun lock file and Bun binary is available. return PackageManager.Bun; + } else if (hasDenoLock && this.getVersion(PackageManager.Deno)) { + // Deno lock file and Deno binary is available. + return PackageManager.Deno; } } @@ -269,13 +296,16 @@ export class PackageManagerUtils { const hasYarn = !!this.getVersion(PackageManager.Yarn); const hasPnpm = !!this.getVersion(PackageManager.Pnpm); const hasBun = !!this.getVersion(PackageManager.Bun); + const hasDeno = !!this.getVersion(PackageManager.Deno); - if (hasYarn && !hasPnpm && !hasBun) { + if (hasYarn && !hasPnpm && !hasBun && !hasDeno) { return PackageManager.Yarn; - } else if (hasPnpm && !hasYarn && !hasBun) { + } else if (hasPnpm && !hasYarn && !hasBun && !hasDeno) { return PackageManager.Pnpm; - } else if (hasBun && !hasYarn && !hasPnpm) { + } else if (hasBun && !hasYarn && !hasPnpm && !hasDeno) { return PackageManager.Bun; + } else if (hasDeno && !hasYarn && !hasPnpm && !hasBun) { + return PackageManager.Deno; } } diff --git a/packages/angular/create/README.md b/packages/angular/create/README.md index 46135476e406..45831e08077b 100644 --- a/packages/angular/create/README.md +++ b/packages/angular/create/README.md @@ -29,3 +29,9 @@ pnpm create @angular [project-name] [...options] ``` bun create @angular [project-name] [...options] ``` + +### deno + +``` +deno init --npm @angular [project-name] [...options] +``` diff --git a/packages/angular/create/src/index.ts b/packages/angular/create/src/index.ts index 5e225fd1b1ca..a18a040c2bae 100644 --- a/packages/angular/create/src/index.ts +++ b/packages/angular/create/src/index.ts @@ -17,7 +17,7 @@ const hasPackageManagerArg = args.some((a) => a.startsWith('--package-manager')) if (!hasPackageManagerArg) { // Ex: yarn/1.22.18 npm/? node/v16.15.1 linux x64 const packageManager = process.env['npm_config_user_agent']?.split('/')[0]; - if (packageManager && ['npm', 'pnpm', 'yarn', 'bun'].includes(packageManager)) { + if (packageManager && ['npm', 'pnpm', 'yarn', 'bun', 'deno'].includes(packageManager)) { args.push('--package-manager', packageManager); } } diff --git a/packages/angular_devkit/schematics/tasks/package-manager/executor.ts b/packages/angular_devkit/schematics/tasks/package-manager/executor.ts index dc97a4e78277..46dd3869bcc9 100644 --- a/packages/angular_devkit/schematics/tasks/package-manager/executor.ts +++ b/packages/angular_devkit/schematics/tasks/package-manager/executor.ts @@ -39,6 +39,12 @@ const packageManagers: { [name: string]: PackageManagerProfile } = { installPackage: 'add', }, }, + 'deno': { + commands: { + installAll: 'install', + installPackage: 'add', + }, + }, 'pnpm': { commands: { installAll: 'install', diff --git a/packages/angular_devkit/schematics_cli/blank/schema.json b/packages/angular_devkit/schematics_cli/blank/schema.json index 9ab174efd482..1abbb50e5163 100644 --- a/packages/angular_devkit/schematics_cli/blank/schema.json +++ b/packages/angular_devkit/schematics_cli/blank/schema.json @@ -15,7 +15,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"], + "enum": ["npm", "yarn", "pnpm", "bun", "deno"], "default": "npm" }, "author": { diff --git a/packages/angular_devkit/schematics_cli/schematic/schema.json b/packages/angular_devkit/schematics_cli/schematic/schema.json index 4780529c9311..d893c4513d1f 100644 --- a/packages/angular_devkit/schematics_cli/schematic/schema.json +++ b/packages/angular_devkit/schematics_cli/schematic/schema.json @@ -15,7 +15,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"], + "enum": ["npm", "yarn", "pnpm", "bun", "deno"], "default": "npm" } }, diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index 3bbb0eb3dee4..e725d6977f4e 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -132,7 +132,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun", "deno"] }, "standalone": { "description": "Creates an application based upon the standalone API, without NgModules.", diff --git a/packages/schematics/angular/workspace/schema.json b/packages/schematics/angular/workspace/schema.json index 51ec1a22e889..c1bdb6639b3a 100644 --- a/packages/schematics/angular/workspace/schema.json +++ b/packages/schematics/angular/workspace/schema.json @@ -40,7 +40,7 @@ "packageManager": { "description": "The package manager to use for installing dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm", "bun"], + "enum": ["npm", "yarn", "pnpm", "bun", "deno"], "$default": { "$source": "packageManager" } diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 62a89ee6a76f..8eed08a93a14 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -85,6 +85,7 @@ def e2e_suites(name, runner, data): _e2e_suite(name, runner, "npm", data, toolchain_name, toolchain) _e2e_suite(name, runner, "bun", data, toolchain_name, toolchain) + _e2e_suite(name, runner, "deno", data, toolchain_name, toolchain) _e2e_suite(name, runner, "pnpm", data, toolchain_name, toolchain) _e2e_suite(name, runner, "yarn", data, toolchain_name, toolchain) _e2e_suite(name, runner, "esbuild", data, toolchain_name, toolchain) @@ -140,7 +141,7 @@ def _e2e_tests(name, runner, toolchain, **kwargs): def _e2e_suite(name, runner, type, data, toolchain_name = "", toolchain = None): """ - Setup a predefined test suite (yarn|pnpm|bun|esbuild|saucelabs|npm). + Setup a predefined test suite (yarn|pnpm|bun|deno|esbuild|saucelabs|npm). """ args = [] tests = None @@ -149,7 +150,7 @@ def _e2e_suite(name, runner, type, data, toolchain_name = "", toolchain = None): if toolchain_name: toolchain_name = "_" + toolchain_name - if type == "yarn" or type == "bun" or type == "pnpm": + if type == "yarn" or type == "bun" or type == "deno" or type == "pnpm": args.append("--package-manager=%s" % type) args.append("--esbuild") tests = PACKAGE_MANAGER_SUBSET_TESTS diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts index 9f587fa5c38d..09452ff994be 100644 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ b/tests/legacy-cli/e2e/setup/100-global-cli.ts @@ -7,9 +7,10 @@ const PACKAGE_MANAGER_VERSION = { 'yarn': '1.22.22', 'pnpm': '10.17.1', 'bun': '1.3.2', + 'deno': '2.4.4', }; -export default async function () { +export default async function() { const argv = getGlobalVariable('argv'); if (argv.noglobal) { return; diff --git a/tests/legacy-cli/e2e/tests/misc/create-angular.ts b/tests/legacy-cli/e2e/tests/misc/create-angular.ts index acbdf135359b..047d532a9a49 100644 --- a/tests/legacy-cli/e2e/tests/misc/create-angular.ts +++ b/tests/legacy-cli/e2e/tests/misc/create-angular.ts @@ -2,7 +2,7 @@ import { equal } from 'node:assert'; import { join, resolve } from 'node:path'; import { expectFileToExist, readFile, rimraf } from '../../utils/fs'; import { getActivePackageManager } from '../../utils/packages'; -import { silentBun, silentNpm, silentPnpm, silentYarn } from '../../utils/process'; +import { silentBun, silentDeno, silentNpm, silentPnpm, silentYarn } from '../../utils/process'; export default async function () { const currentDirectory = process.cwd(); @@ -25,6 +25,10 @@ export default async function () { case 'bun': await silentBun('create', '@angular', projectName, '--style=scss'); + break; + case 'deno': + await silentDeno('init', '--npm', '@angular', projectName, '--style=scss'); + break; case 'pnpm': await silentPnpm('create', '@angular', projectName, '--style=scss'); diff --git a/tests/legacy-cli/e2e/utils/packages.ts b/tests/legacy-cli/e2e/utils/packages.ts index 087d771f14b8..8c977170fd68 100644 --- a/tests/legacy-cli/e2e/utils/packages.ts +++ b/tests/legacy-cli/e2e/utils/packages.ts @@ -1,5 +1,5 @@ import { getGlobalVariable } from './env'; -import { ProcessOutput, silentBun, silentNpm, silentPnpm, silentYarn } from './process'; +import { ProcessOutput, silentBun, silentDeno, silentNpm, silentPnpm, silentYarn } from './process'; export interface PkgInfo { readonly name: string; @@ -7,7 +7,7 @@ export interface PkgInfo { readonly path: string; } -export function getActivePackageManager(): 'npm' | 'yarn' | 'bun' | 'pnpm' { +export function getActivePackageManager(): 'npm' | 'yarn' | 'bun' | 'deno' | 'pnpm' { return getGlobalVariable('package-manager'); } @@ -29,6 +29,9 @@ export async function installWorkspacePackages(options?: { force?: boolean }): P case 'bun': await silentBun('install'); break; + case 'deno': + await silentDeno('install'); + break; } } @@ -41,6 +44,8 @@ export function installPackage(specifier: string, registry?: string): Promise { case 'bun': await silentBun('remove', name); break; + case 'deno': + await silentDeno('remove', name); + break; case 'pnpm': await silentPnpm('remove', name); break; diff --git a/tests/legacy-cli/e2e/utils/process.ts b/tests/legacy-cli/e2e/utils/process.ts index cb542ff1ea0f..44a77138542e 100644 --- a/tests/legacy-cli/e2e/utils/process.ts +++ b/tests/legacy-cli/e2e/utils/process.ts @@ -393,6 +393,10 @@ export function silentBun(...args: string[]) { return _exec({ silent: true }, 'bun', args); } +export function silentDeno(...args: string[]) { + return _exec({ silent: true }, 'deno', args); +} + export function globalNpm(args: string[], env?: NodeJS.ProcessEnv) { if (!process.env.LEGACY_CLI_RUNNER) { throw new Error( From 38470cba9b1ee326ff45b9af32e67bdb3d5c8e12 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:34:16 +0000 Subject: [PATCH 2/4] ci: add CI for deno package manager This commits configures CI to run Deno E2E tests. --- .github/workflows/ci.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faeefa80cbd7..fa5c68042cc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,7 @@ jobs: matrix: os: [ubuntu-latest] node: [22] - subset: [yarn, pnpm, bun] + subset: [yarn, pnpm, bun, deno] shard: [0, 1, 2] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 91d2a7c5a24d..022ddc730518 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -180,7 +180,7 @@ jobs: matrix: os: [ubuntu-latest] node: [22] - subset: [yarn, pnpm, bun] + subset: [yarn, pnpm, bun, deno] shard: [0, 1, 2] runs-on: ${{ matrix.os }} steps: From 8504d27102b0e3c32d1615d3ff87fc0d2f637b88 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 20 Nov 2025 15:55:57 +0100 Subject: [PATCH 3/4] fix(@angular/cli): update deno version --- packages/angular/cli/src/utilities/package-manager.ts | 2 +- tests/legacy-cli/e2e/setup/100-global-cli.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts index a5a794f53915..327612af9ccb 100644 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -234,7 +234,7 @@ export class PackageManagerUtils { }).trim(); if (name === PackageManager.Deno) { - // Deno CLI outputs "deno 2.4.4" + // Deno CLI outputs "deno 2.5.6" return version.replace('deno ', ''); } diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts index 09452ff994be..4badb21766b9 100644 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ b/tests/legacy-cli/e2e/setup/100-global-cli.ts @@ -7,7 +7,7 @@ const PACKAGE_MANAGER_VERSION = { 'yarn': '1.22.22', 'pnpm': '10.17.1', 'bun': '1.3.2', - 'deno': '2.4.4', + 'deno': '2.5.6', }; export default async function() { From f26427e86f83ab304fc0f0ee36ec7134201d83b5 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 20 Nov 2025 16:10:10 +0100 Subject: [PATCH 4/4] fix(@angular/cli): address lint formatting issues --- packages/angular/cli/src/utilities/package-manager.ts | 4 ++-- tests/legacy-cli/e2e/setup/100-global-cli.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts index 327612af9ccb..b92caf4f4453 100644 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -188,12 +188,12 @@ export class PackageManagerUtils { private async run( args: string[], - options: { cwd?: string; silent?: boolean; } = {}, + options: { cwd?: string; silent?: boolean } = {}, ): Promise { const { cwd = process.cwd(), silent = false } = options; return new Promise((resolve) => { - const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer; }[] = []; + const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; const childProcess = spawn(`${this.name} ${args.join(' ')}`, { // Always pipe stderr to allow for failures to be reported diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts index 4badb21766b9..38015aa81f6a 100644 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ b/tests/legacy-cli/e2e/setup/100-global-cli.ts @@ -10,7 +10,7 @@ const PACKAGE_MANAGER_VERSION = { 'deno': '2.5.6', }; -export default async function() { +export default async function () { const argv = getGlobalVariable('argv'); if (argv.noglobal) { return;