Skip to content

Commit 557f7c2

Browse files
committed
refactor(@schematics/angular): use a project-based schematic helper to reduce boilerplate
Introduces a new `createProjectSchematic` helper to abstract the common logic of looking up a project within the workspace. This pattern was repeated across many schematics. The following project-scoped schematics have been refactored to use the new helper: - app-shell - component - config - directive - module - pipe - server - service-worker - ssr - web-worker This change simplifies the implementation of these schematics, reduces code duplication, and improves maintainability.
1 parent 290ac55 commit 557f7c2

File tree

11 files changed

+395
-413
lines changed

11 files changed

+395
-413
lines changed

packages/schematics/angular/app-shell/index.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '../utility/ast-utils';
1919
import { applyToUpdateRecorder } from '../utility/change';
2020
import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
21-
import { targetBuildNotFoundError } from '../utility/project-targets';
21+
import { createProjectSchematic } from '../utility/project';
2222
import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util';
2323
import { getWorkspace } from '../utility/workspace';
2424
import { Schema as AppShellOptions } from './schema';
@@ -190,27 +190,19 @@ function addServerRoutingConfig(options: AppShellOptions, isStandalone: boolean)
190190
};
191191
}
192192

193-
export default function (options: AppShellOptions): Rule {
194-
return async (tree) => {
195-
const browserEntryPoint = await getMainFilePath(tree, options.project);
196-
const isStandalone = isStandaloneApp(tree, browserEntryPoint);
197-
198-
const workspace = await getWorkspace(tree);
199-
const project = workspace.projects.get(options.project);
200-
if (!project) {
201-
throw targetBuildNotFoundError();
202-
}
203-
204-
return chain([
205-
validateProject(browserEntryPoint),
206-
schematic('server', options),
207-
addServerRoutingConfig(options, isStandalone),
208-
schematic('component', {
209-
name: 'app-shell',
210-
module: 'app.module.server.ts',
211-
project: options.project,
212-
standalone: isStandalone,
213-
}),
214-
]);
215-
};
216-
}
193+
export default createProjectSchematic<AppShellOptions>(async (options, { tree }) => {
194+
const browserEntryPoint = await getMainFilePath(tree, options.project);
195+
const isStandalone = isStandaloneApp(tree, browserEntryPoint);
196+
197+
return chain([
198+
validateProject(browserEntryPoint),
199+
schematic('server', options),
200+
addServerRoutingConfig(options, isStandalone),
201+
schematic('component', {
202+
name: 'app-shell',
203+
module: 'app.module.server.ts',
204+
project: options.project,
205+
standalone: isStandalone,
206+
}),
207+
]);
208+
});

packages/schematics/angular/component/index.ts

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import {
2525
import { addDeclarationToNgModule } from '../utility/add-declaration-to-ng-module';
2626
import { findModuleFromOptions } from '../utility/find-module';
2727
import { parseName } from '../utility/parse-name';
28+
import { createProjectSchematic } from '../utility/project';
2829
import { validateClassName, validateHtmlSelector } from '../utility/validation';
29-
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
30+
import { buildDefaultPath } from '../utility/workspace';
3031
import { Schema as ComponentOptions, Style } from './schema';
3132

3233
function buildSelector(options: ComponentOptions, projectPrefix: string) {
@@ -40,62 +41,52 @@ function buildSelector(options: ComponentOptions, projectPrefix: string) {
4041
return selector;
4142
}
4243

43-
export default function (options: ComponentOptions): Rule {
44-
return async (host: Tree) => {
45-
const workspace = await getWorkspace(host);
46-
const project = workspace.projects.get(options.project);
47-
48-
if (!project) {
49-
throw new SchematicsException(`Project "${options.project}" does not exist.`);
50-
}
51-
52-
if (options.path === undefined) {
53-
options.path = buildDefaultPath(project);
54-
}
44+
export default createProjectSchematic<ComponentOptions>((options, { project, tree }) => {
45+
if (options.path === undefined) {
46+
options.path = buildDefaultPath(project);
47+
}
5548

56-
options.module = findModuleFromOptions(host, options);
57-
// Schematic templates require a defined type value
58-
options.type ??= '';
49+
options.module = findModuleFromOptions(tree, options);
50+
// Schematic templates require a defined type value
51+
options.type ??= '';
5952

60-
const parsedPath = parseName(options.path, options.name);
61-
options.name = parsedPath.name;
62-
options.path = parsedPath.path;
63-
options.selector =
64-
options.selector || buildSelector(options, (project && project.prefix) || '');
53+
const parsedPath = parseName(options.path, options.name);
54+
options.name = parsedPath.name;
55+
options.path = parsedPath.path;
56+
options.selector = options.selector || buildSelector(options, (project && project.prefix) || '');
6557

66-
validateHtmlSelector(options.selector);
67-
validateClassName(strings.classify(options.name));
58+
validateHtmlSelector(options.selector);
59+
validateClassName(strings.classify(options.name));
6860

69-
const skipStyleFile = options.inlineStyle || options.style === Style.None;
70-
const templateSource = apply(url('./files'), [
71-
options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(),
72-
skipStyleFile ? filter((path) => !path.endsWith('.__style__.template')) : noop(),
73-
options.inlineTemplate ? filter((path) => !path.endsWith('.html.template')) : noop(),
74-
applyTemplates({
75-
...strings,
76-
'if-flat': (s: string) => (options.flat ? '' : s),
77-
'ngext': options.ngHtml ? '.ng' : '',
78-
...options,
79-
}),
80-
!options.type
81-
? forEach(((file) => {
82-
return file.path.includes('..')
83-
? {
84-
content: file.content,
85-
path: file.path.replace('..', '.'),
86-
}
87-
: file;
88-
}) as FileOperator)
89-
: noop(),
90-
move(parsedPath.path),
91-
]);
61+
const skipStyleFile = options.inlineStyle || options.style === Style.None;
62+
const templateSource = apply(url('./files'), [
63+
options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(),
64+
skipStyleFile ? filter((path) => !path.endsWith('.__style__.template')) : noop(),
65+
options.inlineTemplate ? filter((path) => !path.endsWith('.html.template')) : noop(),
66+
applyTemplates({
67+
...strings,
68+
'if-flat': (s: string) => (options.flat ? '' : s),
69+
'ngext': options.ngHtml ? '.ng' : '',
70+
...options,
71+
}),
72+
!options.type
73+
? forEach(((file) => {
74+
return file.path.includes('..')
75+
? {
76+
content: file.content,
77+
path: file.path.replace('..', '.'),
78+
}
79+
: file;
80+
}) as FileOperator)
81+
: noop(),
82+
move(parsedPath.path),
83+
]);
9284

93-
return chain([
94-
addDeclarationToNgModule({
95-
type: 'component',
96-
...options,
97-
}),
98-
mergeWith(templateSource),
99-
]);
100-
};
101-
}
85+
return chain([
86+
addDeclarationToNgModule({
87+
type: 'component',
88+
...options,
89+
}),
90+
mergeWith(templateSource),
91+
]);
92+
});

packages/schematics/angular/config/index.ts

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,33 @@ import {
2020
import { readFile } from 'node:fs/promises';
2121
import { posix as path } from 'node:path';
2222
import { relativePathToWorkspaceRoot } from '../utility/paths';
23-
import { getWorkspace as readWorkspace, updateWorkspace } from '../utility/workspace';
23+
import { createProjectSchematic } from '../utility/project';
24+
import { updateWorkspace } from '../utility/workspace';
2425
import { Builders as AngularBuilder } from '../utility/workspace-models';
2526
import { Schema as ConfigOptions, Type as ConfigType } from './schema';
2627

27-
export default function (options: ConfigOptions): Rule {
28+
export default createProjectSchematic<ConfigOptions>((options, { project }) => {
2829
switch (options.type) {
2930
case ConfigType.Karma:
3031
return addKarmaConfig(options);
3132
case ConfigType.Browserslist:
32-
return addBrowserslistConfig(options);
33+
return addBrowserslistConfig(project.root);
3334
default:
3435
throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`);
3536
}
36-
}
37-
38-
function addBrowserslistConfig(options: ConfigOptions): Rule {
39-
return async (host) => {
40-
const workspace = await readWorkspace(host);
41-
const project = workspace.projects.get(options.project);
42-
if (!project) {
43-
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
44-
}
37+
});
4538

46-
// Read Angular's default vendored `.browserslistrc` file.
47-
const config = await readFile(path.join(__dirname, '.browserslistrc'), 'utf8');
39+
async function addBrowserslistConfig(projectRoot: string): Promise<Rule> {
40+
// Read Angular's default vendored `.browserslistrc` file.
41+
const config = await readFile(path.join(__dirname, '.browserslistrc'), 'utf8');
4842

49-
return mergeWith(
50-
apply(url('./files'), [
51-
filter((p) => p.endsWith('.browserslistrc.template')),
52-
applyTemplates({ config }),
53-
move(project.root),
54-
]),
55-
);
56-
};
43+
return mergeWith(
44+
apply(url('./files'), [
45+
filter((p) => p.endsWith('.browserslistrc.template')),
46+
applyTemplates({ config }),
47+
move(projectRoot),
48+
]),
49+
);
5750
}
5851

5952
function addKarmaConfig(options: ConfigOptions): Rule {

packages/schematics/angular/directive/index.ts

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { Rule, SchematicsException, Tree, chain, strings } from '@angular-devkit/schematics';
9+
import { Rule, chain, strings } from '@angular-devkit/schematics';
1010
import { addDeclarationToNgModule } from '../utility/add-declaration-to-ng-module';
1111
import { findModuleFromOptions } from '../utility/find-module';
1212
import { generateFromFiles } from '../utility/generate-from-files';
1313
import { parseName } from '../utility/parse-name';
14+
import { createProjectSchematic } from '../utility/project';
1415
import { validateClassName, validateHtmlSelector } from '../utility/validation';
15-
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
16+
import { buildDefaultPath } from '../utility/workspace';
1617
import { Schema as DirectiveOptions } from './schema';
1718

1819
function buildSelector(options: DirectiveOptions, projectPrefix: string) {
@@ -26,34 +27,26 @@ function buildSelector(options: DirectiveOptions, projectPrefix: string) {
2627
return strings.camelize(selector);
2728
}
2829

29-
export default function (options: DirectiveOptions): Rule {
30-
return async (host: Tree) => {
31-
const workspace = await getWorkspace(host);
32-
const project = workspace.projects.get(options.project);
33-
if (!project) {
34-
throw new SchematicsException(`Project "${options.project}" does not exist.`);
35-
}
36-
37-
if (options.path === undefined) {
38-
options.path = buildDefaultPath(project);
39-
}
40-
41-
options.module = findModuleFromOptions(host, options);
42-
const parsedPath = parseName(options.path, options.name);
43-
options.name = parsedPath.name;
44-
options.path = parsedPath.path;
45-
options.selector = options.selector || buildSelector(options, project.prefix || '');
46-
47-
validateHtmlSelector(options.selector);
48-
validateClassName(strings.classify(options.name));
49-
50-
return chain([
51-
addDeclarationToNgModule({
52-
type: 'directive',
53-
54-
...options,
55-
}),
56-
generateFromFiles(options),
57-
]);
58-
};
59-
}
30+
export default createProjectSchematic<DirectiveOptions>((options, { project, tree }) => {
31+
if (options.path === undefined) {
32+
options.path = buildDefaultPath(project);
33+
}
34+
35+
options.module = findModuleFromOptions(tree, options);
36+
const parsedPath = parseName(options.path, options.name);
37+
options.name = parsedPath.name;
38+
options.path = parsedPath.path;
39+
options.selector = options.selector || buildSelector(options, project.prefix || '');
40+
41+
validateHtmlSelector(options.selector);
42+
validateClassName(strings.classify(options.name));
43+
44+
return chain([
45+
addDeclarationToNgModule({
46+
type: 'directive',
47+
48+
...options,
49+
}),
50+
generateFromFiles(options),
51+
]);
52+
});

0 commit comments

Comments
 (0)