Skip to content

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)