Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ff71111

Browse files
clydinalan-agius4
authored andcommitted
refactor(@schematics/angular): add getDependency and removeDependency utilities
Adds two new utility functions to the schematics dependency helper library: - getDependency: Allows a schematic to query a package.json and check for the existence of a dependency, returning its version and type if found. - removeDependency: Provides a schematic rule to safely remove a dependency from any of the dependency sections in a package.json.
1 parent 6a78ef0 commit ff71111

File tree

2 files changed

+307
-2
lines changed

2 files changed

+307
-2
lines changed

‎packages/schematics/angular/utility/dependency.ts‎

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { Rule, SchematicContext } from '@angular-devkit/schematics';
9+
import { Rule, SchematicContext,Tree } from '@angular-devkit/schematics';
1010
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
1111
import * as path from 'node:path';
1212

@@ -73,6 +73,55 @@ export enum ExistingBehavior {
7373
Replace,
7474
}
7575

76+
/**
77+
* Represents a dependency found in a package manifest.
78+
*/
79+
export interface Dependency {
80+
/**
81+
* The type of the dependency.
82+
*/
83+
type: DependencyType;
84+
85+
/**
86+
* The name of the package.
87+
*/
88+
name: string;
89+
90+
/**
91+
* The version specifier of the package.
92+
*/
93+
version: string;
94+
}
95+
96+
/**
97+
* Gets information about a dependency from a `package.json` file.
98+
*
99+
* @param tree The schematic's virtual file system representation.
100+
* @param name The name of the package to check.
101+
* @param packageJsonPath The path to the `package.json` file. Defaults to `/package.json`.
102+
* @returns An object containing the dependency's type and version, or null if not found.
103+
*/
104+
export function getDependency(
105+
tree: Tree,
106+
name: string,
107+
packageJsonPath = '/package.json',
108+
): Dependency | null {
109+
const manifest = tree.readJson(packageJsonPath) as MinimalPackageManifest;
110+
111+
for (const type of [DependencyType.Default, DependencyType.Dev, DependencyType.Peer]) {
112+
const section = manifest[type];
113+
if (section?.[name]) {
114+
return {
115+
type,
116+
name,
117+
version: section[name],
118+
};
119+
}
120+
}
121+
122+
return null;
123+
}
124+
76125
/**
77126
* Adds a package as a dependency to a `package.json`. By default the `package.json` located
78127
* at the schematic's root will be used. The `manifestPath` option can be used to explicitly specify
@@ -177,3 +226,59 @@ export function addDependency(
177226
}
178227
};
179228
}
229+
230+
/**
231+
* Removes a package from the package.json in the project root.
232+
*
233+
* @param name The name of the package to remove.
234+
* @param options An optional object that can contain a path of a manifest file to modify.
235+
* @returns A Schematics {@link Rule}
236+
*/
237+
export function removeDependency(
238+
name: string,
239+
options: {
240+
/**
241+
* The path of the package manifest file (`package.json`) that will be modified.
242+
* Defaults to `/package.json`.
243+
*/
244+
packageJsonPath?: string;
245+
246+
/**
247+
* The dependency installation behavior to use to determine whether a
248+
* {@link NodePackageInstallTask} should be scheduled after removing the dependency.
249+
* Defaults to {@link InstallBehavior.Auto}.
250+
*/
251+
install?: InstallBehavior;
252+
} = {},
253+
): Rule {
254+
const { packageJsonPath = '/package.json', install = InstallBehavior.Auto } = options;
255+
256+
return (tree, context) => {
257+
const manifest = tree.readJson(packageJsonPath) as MinimalPackageManifest;
258+
let wasRemoved = false;
259+
260+
for (const type of [DependencyType.Default, DependencyType.Dev, DependencyType.Peer]) {
261+
const dependencySection = manifest[type];
262+
if (dependencySection?.[name]) {
263+
delete dependencySection[name];
264+
wasRemoved = true;
265+
}
266+
}
267+
268+
if (wasRemoved) {
269+
tree.overwrite(packageJsonPath, JSON.stringify(manifest, null, 2));
270+
271+
const installPaths = installTasks.get(context) ?? new Set<string>();
272+
if (
273+
install === InstallBehavior.Always ||
274+
(install === InstallBehavior.Auto && !installPaths.has(packageJsonPath))
275+
) {
276+
context.addTask(
277+
new NodePackageInstallTask({ workingDirectory: path.dirname(packageJsonPath) }),
278+
);
279+
installPaths.add(packageJsonPath);
280+
installTasks.set(context, installPaths);
281+
}
282+
}
283+
};
284+
}

‎packages/schematics/angular/utility/dependency_spec.ts‎

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import {
1515
callRule,
1616
chain,
1717
} from '@angular-devkit/schematics';
18-
import { DependencyType, ExistingBehavior, InstallBehavior, addDependency } from './dependency';
18+
import {
19+
DependencyType,
20+
ExistingBehavior,
21+
InstallBehavior,
22+
addDependency,
23+
getDependency,
24+
removeDependency,
25+
} from './dependency';
1926

2027
interface LogEntry {
2128
type: 'warn';
@@ -484,3 +491,196 @@ describe('addDependency', () => {
484491
);
485492
});
486493
});
494+
495+
describe('removeDependency', () => {
496+
it('removes a package from "dependencies"', async () => {
497+
const tree = new EmptyTree();
498+
tree.create(
499+
'/package.json',
500+
JSON.stringify({
501+
dependencies: { '@angular/core': '^14.0.0' },
502+
}),
503+
);
504+
505+
const rule = removeDependency('@angular/core');
506+
await testRule(rule, tree);
507+
508+
expect(tree.readJson('/package.json')).toEqual({
509+
dependencies: {},
510+
});
511+
});
512+
513+
it('removes a package from "devDependencies"', async () => {
514+
const tree = new EmptyTree();
515+
tree.create(
516+
'/package.json',
517+
JSON.stringify({
518+
devDependencies: { typescript: '~4.7.2' },
519+
}),
520+
);
521+
522+
const rule = removeDependency('typescript');
523+
await testRule(rule, tree);
524+
525+
expect(tree.readJson('/package.json')).toEqual({
526+
devDependencies: {},
527+
});
528+
});
529+
530+
it('removes a package from "peerDependencies"', async () => {
531+
const tree = new EmptyTree();
532+
tree.create(
533+
'/package.json',
534+
JSON.stringify({
535+
peerDependencies: { rxjs: '^7.0.0' },
536+
}),
537+
);
538+
539+
const rule = removeDependency('rxjs');
540+
await testRule(rule, tree);
541+
542+
expect(tree.readJson('/package.json')).toEqual({
543+
peerDependencies: {},
544+
});
545+
});
546+
547+
it('does not change manifest if package is not found', async () => {
548+
const tree = new EmptyTree();
549+
const manifest = { dependencies: { '@angular/core': '^14.0.0' } };
550+
tree.create('/package.json', JSON.stringify(manifest));
551+
552+
const rule = removeDependency('typescript');
553+
await testRule(rule, tree);
554+
555+
expect(tree.readJson('/package.json')).toEqual(manifest);
556+
});
557+
558+
it('schedules a package install task by default', async () => {
559+
const tree = new EmptyTree();
560+
tree.create('/package.json', JSON.stringify({ dependencies: { '@angular/core': '1.0.0' } }));
561+
562+
const rule = removeDependency('@angular/core');
563+
const { tasks } = await testRule(rule, tree);
564+
565+
expect(tasks.map((task) => task.toConfiguration())).toEqual([
566+
{
567+
name: 'node-package',
568+
options: jasmine.objectContaining({ command: 'install', workingDirectory: '/' }),
569+
},
570+
]);
571+
});
572+
573+
it('does not schedule a package install task if package not found', async () => {
574+
const tree = new EmptyTree();
575+
tree.create('/package.json', JSON.stringify({ dependencies: {} }));
576+
577+
const rule = removeDependency('@angular/core');
578+
const { tasks } = await testRule(rule, tree);
579+
580+
expect(tasks).toEqual([]);
581+
});
582+
583+
it('does not schedule a package install task when install behavior is none', async () => {
584+
const tree = new EmptyTree();
585+
tree.create('/package.json', JSON.stringify({ dependencies: { '@angular/core': '1.0.0' } }));
586+
587+
const rule = removeDependency('@angular/core', { install: InstallBehavior.None });
588+
const { tasks } = await testRule(rule, tree);
589+
590+
expect(tasks).toEqual([]);
591+
});
592+
593+
it('uses specified manifest when provided via "packageJsonPath" option', async () => {
594+
const tree = new EmptyTree();
595+
tree.create('/package.json', JSON.stringify({ dependencies: { '@angular/core': '1.0.0' } }));
596+
tree.create(
597+
'/abc/package.json',
598+
JSON.stringify({ dependencies: { '@angular/core': '1.0.0' } }),
599+
);
600+
601+
const rule = removeDependency('@angular/core', { packageJsonPath: '/abc/package.json' });
602+
await testRule(rule, tree);
603+
604+
expect(tree.readJson('/package.json')).toEqual({ dependencies: { '@angular/core': '1.0.0' } });
605+
expect(tree.readJson('/abc/package.json')).toEqual({ dependencies: {} });
606+
});
607+
});
608+
609+
describe('getDependency', () => {
610+
it('returns a dependency found in "dependencies"', () => {
611+
const tree = new EmptyTree();
612+
tree.create(
613+
'/package.json',
614+
JSON.stringify({
615+
dependencies: { '@angular/core': '^14.0.0' },
616+
}),
617+
);
618+
619+
const dep = getDependency(tree, '@angular/core');
620+
expect(dep).toEqual({
621+
type: DependencyType.Default,
622+
name: '@angular/core',
623+
version: '^14.0.0',
624+
});
625+
});
626+
627+
it('returns a dependency found in "devDependencies"', () => {
628+
const tree = new EmptyTree();
629+
tree.create(
630+
'/package.json',
631+
JSON.stringify({
632+
devDependencies: { typescript: '~4.7.2' },
633+
}),
634+
);
635+
636+
const dep = getDependency(tree, 'typescript');
637+
expect(dep).toEqual({
638+
type: DependencyType.Dev,
639+
name: 'typescript',
640+
version: '~4.7.2',
641+
});
642+
});
643+
644+
it('returns a dependency found in "peerDependencies"', () => {
645+
const tree = new EmptyTree();
646+
tree.create(
647+
'/package.json',
648+
JSON.stringify({
649+
peerDependencies: { rxjs: '^7.0.0' },
650+
}),
651+
);
652+
653+
const dep = getDependency(tree, 'rxjs');
654+
expect(dep).toEqual({
655+
type: DependencyType.Peer,
656+
name: 'rxjs',
657+
version: '^7.0.0',
658+
});
659+
});
660+
661+
it('returns null if a dependency is not found', () => {
662+
const tree = new EmptyTree();
663+
tree.create('/package.json', JSON.stringify({}));
664+
665+
const dep = getDependency(tree, '@angular/core');
666+
expect(dep).toBeNull();
667+
});
668+
669+
it('returns a dependency from a specified manifest path', () => {
670+
const tree = new EmptyTree();
671+
tree.create('/package.json', JSON.stringify({}));
672+
tree.create(
673+
'/abc/package.json',
674+
JSON.stringify({
675+
dependencies: { '@angular/core': '^14.0.0' },
676+
}),
677+
);
678+
679+
const dep = getDependency(tree, '@angular/core', '/abc/package.json');
680+
expect(dep).toEqual({
681+
type: DependencyType.Default,
682+
name: '@angular/core',
683+
version: '^14.0.0',
684+
});
685+
});
686+
});

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /