FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(@angular/cli): discover migrations and schematics from installed packages when omitted by registry metadata by alan-agius4 · Pull Request #33718 · angular/angular-cli · GitHub

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .ts  (3) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
      • cli.ts
      • cli.ts
      • cli_spec.ts
23 changes: 6 additions & 17 deletions packages/angular/cli/src/commands/add/cli.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -224,29 +224,12 @@ export default class AddCommandModule
{
title: 'Confirming installation',
enabled: !skipConfirmation && !options.dryRun,
skip: (context) => {
if (context.hasSchematics) {
return false;
}

return `The ${color.blue(context.packageIdentifier.toString())} package does not provide \`ng add\` actions.`;
},
task: (context, task) => this.confirmInstallationTask(context, task),
rendererOptions: { persistentOutput: true },
},
{
title: 'Installing package',
skip: (context) => {
if (!context.hasSchematics) {
const builtInSchematic =
BUILT_IN_SCHEMATICS[
context.packageIdentifier.name as keyof typeof BUILT_IN_SCHEMATICS
];
if (builtInSchematic) {
return `Skipping package installation.`;
}
}

if (context.dryRun) {
return `Skipping package installation. Would install package ${color.blue(
context.packageIdentifier.toString(),
Expand Down Expand Up @@ -278,6 +261,9 @@ export default class AddCommandModule
if (localManifest['ng-add']?.save === false) {
shouldCleanUp = true;
}
} else {
await this.cleanUpTemporaryDependency(result.collectionName);
shouldCleanUp = false;
}
} catch {}
}
Expand Down Expand Up @@ -305,6 +291,9 @@ export default class AddCommandModule
const builtInSchematic =
BUILT_IN_SCHEMATICS[packageName as keyof typeof BUILT_IN_SCHEMATICS];
if (builtInSchematic) {
logger.info(
`The ${color.blue(packageName)} package does not provide \`ng add\` actions.`,
);
logger.info('The Angular CLI will use built-in actions to add it to your project.');

return this.executeSchematic({
Expand Down
48 changes: 47 additions & 1 deletion packages/angular/cli/src/commands/update/cli.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ export default class UpdateCommandModule extends CommandModule<UpdateCommandArgs
}
}

const migrations = plan.migrationsToRun;
const migrations = await resolveFallbackMigrations(this.context.root, plan);

if (migrations) {
for (const migration of migrations) {
Expand Down Expand Up @@ -706,3 +706,49 @@ async function readPackageManifest(manifestPath: string): Promise<PackageManifes
return undefined;
}
}

/**
* Resolves migrations from installed package manifests on disk when they were omitted
* from the initial update plan.
*
* This fallback is necessary because private package registries (such as GitHub Packages)
* frequently strip custom non-npm metadata properties (like `ng-update`) from their remote
* registry API responses. By inspecting `node_modules/<package>/package.json` after installation,
* we ensure that any migration collections defined by the package are discovered and queued.
*/
export async function resolveFallbackMigrations(
Comment thread
alan-agius4 marked this conversation as resolved.
workspaceRoot: string,
plan: UpdatePlan,
): Promise<{ package: string; collection: string; from: string; to: string }[]> {
const migrations = [...plan.migrationsToRun];
const existingMigrationPackages = new Set(migrations.map((m) => m.package));

for (const [packageName, targetVersion] of plan.packagesToUpdate) {
if (existingMigrationPackages.has(packageName)) {
continue;
}

const packageJsonPath = findPackageJson(workspaceRoot, packageName);
if (packageJsonPath) {
try {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const ngUpdate = packageJson?.['ng-update'];
if (ngUpdate && typeof ngUpdate === 'object' && typeof ngUpdate.migrations === 'string') {
const installedVersion = plan.packageInfoMap.get(packageName)?.installed.version;
if (installedVersion) {
migrations.push({
package: packageName,
collection: ngUpdate.migrations,
from: installedVersion,
to: targetVersion,
});
}
}
} catch {
// Ignore read/parse errors for optional fallback
}
}
}

return migrations;
}
133 changes: 133 additions & 0 deletions packages/angular/cli/src/commands/update/cli_spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import assert from 'node:assert';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import path from 'path';
import { resolveFallbackMigrations } from './cli';
import type { PackageVersionInfo, UpdatePlan } from './update-resolver';

describe('resolveFallbackMigrations', () => {
let tempRoot: string;
let pkgDir: string;
beforeEach(async () => {
const baseTmpDir = process.env['TEST_TMPDIR'];
assert(baseTmpDir, 'TEST_TMPDIR is not set');
tempRoot = await mkdtemp(path.join(baseTmpDir, 'angular-cli-update-cli-test-'));
pkgDir = path.join(tempRoot, 'node_modules/@company/library-name');
await mkdir(pkgDir, { recursive: true });
});

afterEach(async () => {
await rm(tempRoot, { recursive: true, force: true });
});

it('discovers migrations from installed package.json when omitted from plan.migrationsToRun', async () => {
await writeFile(
path.join(pkgDir, 'package.json'),
JSON.stringify({
name: '@company/library-name',
version: '21.2.0-next.1',
'ng-update': {
migrations: './schematics/migration.json',
},
}),
'utf8',
);

const plan: UpdatePlan = {
packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]),
migrationsToRun: [],
packageInfoMap: new Map([
[
'@company/library-name',
{
name: '@company/library-name',
npmPackageJson: {
name: '@company/library-name',
versions: ['21.1.0', '21.2.0-next.1'],
'dist-tags': {},
},
installed: {
version: '21.1.0' as unknown as PackageVersionInfo['version'],
packageJson: { name: '@company/library-name', version: '21.1.0' },
updateMetadata: { packageGroup: {}, requirements: {} },
},
packageJsonRange: '^21.1.0',
},
],
]),
registryClient: undefined as unknown as UpdatePlan['registryClient'],
};

const migrations = await resolveFallbackMigrations(tempRoot, plan);

expect(migrations).toEqual([
{
package: '@company/library-name',
collection: './schematics/migration.json',
from: '21.1.0',
to: '21.2.0-next.1',
},
]);
});

it('does not duplicate migration if package is already in plan.migrationsToRun', async () => {
await writeFile(
path.join(pkgDir, 'package.json'),
JSON.stringify({
name: '@company/library-name',
version: '21.2.0-next.1',
'ng-update': {
migrations: './schematics/migration.json',
},
}),
'utf8',
);

const plan: UpdatePlan = {
packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]),
migrationsToRun: [
{
package: '@company/library-name',
collection: './schematics/migration.json',
from: '21.1.0',
to: '21.2.0-next.1',
},
],
packageInfoMap: new Map(),
registryClient: undefined as unknown as UpdatePlan['registryClient'],
};

const migrations = await resolveFallbackMigrations(tempRoot, plan);

expect(migrations).toHaveSize(1);
});

it('returns unchanged migrations when package has no ng-update field on disk', async () => {
await writeFile(
path.join(pkgDir, 'package.json'),
JSON.stringify({
name: '@company/library-name',
version: '21.2.0-next.1',
}),
'utf8',
);

const plan: UpdatePlan = {
packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]),
migrationsToRun: [],
packageInfoMap: new Map(),
registryClient: undefined as unknown as UpdatePlan['registryClient'],
};

const migrations = await resolveFallbackMigrations(tempRoot, plan);

expect(migrations).toHaveSize(0);
});
});

Back | FazBrowse Home | New Git URL