Skip to content

Commit a331b7c

Browse files
committed
refactor(@schematics/angular): update app-shell to use new workspace rules
1 parent b69618e commit a331b7c

File tree

2 files changed

+107
-92
lines changed

2 files changed

+107
-92
lines changed

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

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import { dirname, experimental, join, normalize } from '@angular-devkit/core';
8+
import { dirname, join, normalize } from '@angular-devkit/core';
99
import {
1010
Rule,
1111
SchematicContext,
1212
SchematicsException,
1313
Tree,
1414
chain,
15+
noop,
1516
schematic,
1617
} from '@angular-devkit/schematics';
1718
import { Schema as ComponentOptions } from '../component/schema';
@@ -26,11 +27,10 @@ import {
2627
isImported,
2728
} from '../utility/ast-utils';
2829
import { Change, InsertChange } from '../utility/change';
29-
import { getWorkspace, updateWorkspace } from '../utility/config';
3030
import { getAppModulePath } from '../utility/ng-ast-utils';
31-
import { getProject } from '../utility/project';
32-
import { getProjectTargets, targetBuildNotFoundError } from '../utility/project-targets';
33-
import { Builders, WorkspaceProject } from '../utility/workspace-models';
31+
import { targetBuildNotFoundError } from '../utility/project-targets';
32+
import { getWorkspace, updateWorkspace } from '../utility/workspace';
33+
import { BrowserBuilderOptions, Builders, ServerBuilderOptions } from '../utility/workspace-models';
3434
import { Schema as AppShellOptions } from './schema';
3535

3636
function getSourceFile(host: Tree, path: string): ts.SourceFile {
@@ -46,18 +46,17 @@ function getSourceFile(host: Tree, path: string): ts.SourceFile {
4646

4747
function getServerModulePath(
4848
host: Tree,
49-
project: experimental.workspace.WorkspaceProject,
50-
architect: experimental.workspace.WorkspaceTool,
49+
projectRoot: string,
50+
mainPath: string,
5151
): string | null {
52-
const mainPath = architect.server.options.main;
5352
const mainSource = getSourceFile(host, mainPath);
5453
const allNodes = getSourceNodes(mainSource);
5554
const expNode = allNodes.filter(node => node.kind === ts.SyntaxKind.ExportDeclaration)[0];
5655
if (!expNode) {
5756
return null;
5857
}
5958
const relativePath = (expNode as ts.ExportDeclaration).moduleSpecifier as ts.StringLiteral;
60-
const modulePath = normalize(`/${project.root}/src/${relativePath.text}.ts`);
59+
const modulePath = normalize(`/${projectRoot}/src/${relativePath.text}.ts`);
6160

6261
return modulePath;
6362
}
@@ -97,14 +96,8 @@ function getComponentTemplate(host: Tree, compPath: string, tmplInfo: TemplateIn
9796

9897
function getBootstrapComponentPath(
9998
host: Tree,
100-
project: WorkspaceProject,
99+
mainPath: string,
101100
): string {
102-
const projectTargets = getProjectTargets(project);
103-
if (!projectTargets.build) {
104-
throw targetBuildNotFoundError();
105-
}
106-
107-
const mainPath = projectTargets.build.options.main;
108101
const modulePath = getAppModulePath(host, mainPath);
109102
const moduleSource = getSourceFile(host, modulePath);
110103

@@ -131,15 +124,11 @@ function getBootstrapComponentPath(
131124
}
132125
// end helper functions.
133126

134-
function validateProject(options: AppShellOptions): Rule {
127+
function validateProject(mainPath: string): Rule {
135128
return (host: Tree, context: SchematicContext) => {
136129
const routerOutletCheckRegex = /<router\-outlet.*?>([\s\S]*?)<\/router\-outlet>/;
137130

138-
const clientProject = getProject(host, options.clientProject);
139-
if (clientProject.projectType !== 'application') {
140-
throw new SchematicsException(`App shell requires a project type of "application".`);
141-
}
142-
const componentPath = getBootstrapComponentPath(host, clientProject);
131+
const componentPath = getBootstrapComponentPath(host, mainPath);
143132
const tmpl = getComponentTemplateInfo(host, componentPath);
144133
const template = getComponentTemplate(host, componentPath, tmpl);
145134
if (!routerOutletCheckRegex.test(template)) {
@@ -152,12 +141,7 @@ function validateProject(options: AppShellOptions): Rule {
152141
}
153142

154143
function addUniversalTarget(options: AppShellOptions): Rule {
155-
return (host: Tree, context: SchematicContext) => {
156-
const architect = getProjectTargets(host, options.clientProject);
157-
if (architect.server) {
158-
return host;
159-
}
160-
144+
return () => {
161145
// Copy options.
162146
const universalOptions = {
163147
...options,
@@ -177,39 +161,38 @@ function addUniversalTarget(options: AppShellOptions): Rule {
177161
}
178162

179163
function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
180-
return (host: Tree) => {
164+
return () => {
181165
if (!options.route) {
182166
throw new SchematicsException(`Route is not defined`);
183167
}
184168

185-
const workspace = getWorkspace(host);
186-
const projectTargets = getProjectTargets(workspace, options.clientProject);
187-
projectTargets['app-shell'] = {
188-
builder: Builders.AppShell,
189-
options: {
190-
browserTarget: `${options.clientProject}:build`,
191-
serverTarget: `${options.clientProject}:server`,
192-
route: options.route,
193-
},
194-
configurations: {
195-
production: {
196-
browserTarget: `${options.clientProject}:build:production`,
197-
serverTarget: `${options.clientProject}:server:production`,
198-
},
199-
},
200-
};
169+
return updateWorkspace(workspace => {
170+
const project = workspace.projects.get(options.clientProject);
171+
if (!project) {
172+
return;
173+
}
201174

202-
return updateWorkspace(workspace);
175+
project.targets.add({
176+
name: 'app-shell',
177+
builder: Builders.AppShell,
178+
options: {
179+
browserTarget: `${options.clientProject}:build`,
180+
serverTarget: `${options.clientProject}:server`,
181+
route: options.route,
182+
},
183+
configurations: {
184+
production: {
185+
browserTarget: `${options.clientProject}:build:production`,
186+
serverTarget: `${options.clientProject}:server:production`,
187+
},
188+
},
189+
});
190+
});
203191
};
204192
}
205193

206-
function addRouterModule(options: AppShellOptions): Rule {
194+
function addRouterModule(mainPath: string): Rule {
207195
return (host: Tree) => {
208-
const projectTargets = getProjectTargets(host, options.clientProject);
209-
if (!projectTargets.build) {
210-
throw targetBuildNotFoundError();
211-
}
212-
const mainPath = projectTargets.build.options.main;
213196
const modulePath = getAppModulePath(host, mainPath);
214197
const moduleSource = getSourceFile(host, modulePath);
215198
const changes = addImportToModule(moduleSource, modulePath, 'RouterModule', '@angular/router');
@@ -245,11 +228,22 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper
245228
}
246229

247230
function addServerRoutes(options: AppShellOptions): Rule {
248-
return (host: Tree) => {
249-
const clientProject = getProject(host, options.clientProject);
250-
const architect = getProjectTargets(clientProject);
251-
// const mainPath = universalArchitect.build.options.main;
252-
const modulePath = getServerModulePath(host, clientProject, architect);
231+
return async (host: Tree) => {
232+
// The workspace gets updated so this needs to be reloaded
233+
const workspace = await getWorkspace(host);
234+
const clientProject = workspace.projects.get(options.clientProject);
235+
if (!clientProject) {
236+
throw new Error('Universal schematic removed client project.');
237+
}
238+
const clientServerTarget = clientProject.targets.get('server');
239+
if (!clientServerTarget) {
240+
throw new Error('Universal schematic did not add server target to client project.');
241+
}
242+
const clientServerOptions = clientServerTarget.options as unknown as ServerBuilderOptions;
243+
if (!clientServerOptions) {
244+
throw new SchematicsException('Server target does not contain options.');
245+
}
246+
const modulePath = getServerModulePath(host, clientProject.root, clientServerOptions.main);
253247
if (modulePath === null) {
254248
throw new SchematicsException('Universal/server module not found.');
255249
}
@@ -296,9 +290,6 @@ function addServerRoutes(options: AppShellOptions): Rule {
296290
}
297291
host.commitUpdate(recorder);
298292
}
299-
300-
301-
return host;
302293
};
303294
}
304295

@@ -313,12 +304,26 @@ function addShellComponent(options: AppShellOptions): Rule {
313304
}
314305

315306
export default function (options: AppShellOptions): Rule {
316-
return chain([
317-
validateProject(options),
318-
addUniversalTarget(options),
319-
addAppShellConfigToWorkspace(options),
320-
addRouterModule(options),
321-
addServerRoutes(options),
322-
addShellComponent(options),
323-
]);
307+
return async tree => {
308+
const workspace = await getWorkspace(tree);
309+
const clientProject = workspace.projects.get(options.clientProject);
310+
if (!clientProject || clientProject.extensions.projectType !== 'application') {
311+
throw new SchematicsException(`A client project type of "application" is required.`);
312+
}
313+
const clientBuildTarget = clientProject.targets.get('build');
314+
if (!clientBuildTarget) {
315+
throw targetBuildNotFoundError();
316+
}
317+
const clientBuildOptions =
318+
(clientBuildTarget.options || {}) as unknown as BrowserBuilderOptions;
319+
320+
return chain([
321+
validateProject(clientBuildOptions.main),
322+
clientProject.targets.has('server') ? noop() : addUniversalTarget(options),
323+
addAppShellConfigToWorkspace(options),
324+
addRouterModule(clientBuildOptions.main),
325+
addServerRoutes(options),
326+
addShellComponent(options),
327+
]);
328+
};
324329
}

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

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,24 @@ describe('App Shell Schematic', () => {
4444
});
4545

4646

47-
it('should ensure the client app has a router-outlet', () => {
47+
it('should ensure the client app has a router-outlet', async () => {
4848
appTree = schematicRunner.runSchematic('workspace', workspaceOptions);
4949
appTree = schematicRunner.runSchematic('application', {...appOptions, routing: false}, appTree);
50-
expect(() => {
51-
schematicRunner.runSchematic('appShell', defaultOptions, appTree);
52-
}).toThrowError();
50+
await expectAsync(
51+
schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree).toPromise(),
52+
).toBeRejected();
5353
});
5454

55-
it('should add a universal app', () => {
56-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
55+
it('should add a universal app', async () => {
56+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
57+
.toPromise();
5758
const filePath = '/projects/bar/src/app/app.server.module.ts';
5859
expect(tree.exists(filePath)).toEqual(true);
5960
});
6061

61-
it('should add app shell configuration', () => {
62-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
62+
it('should add app shell configuration', async () => {
63+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
64+
.toPromise();
6365
const filePath = '/angular.json';
6466
const content = tree.readContent(filePath);
6567
const workspace = JSON.parse(content);
@@ -71,19 +73,21 @@ describe('App Shell Schematic', () => {
7173
expect(target.configurations.production.serverTarget).toEqual('bar:server:production');
7274
});
7375

74-
it('should add router module to client app module', () => {
75-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
76+
it('should add router module to client app module', async () => {
77+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
78+
.toPromise();
7679
const filePath = '/projects/bar/src/app/app.module.ts';
7780
const content = tree.readContent(filePath);
7881
expect(content).toMatch(/import { RouterModule } from \'@angular\/router\';/);
7982
});
8083

81-
it('should not fail when AppModule have imported RouterModule already', () => {
84+
it('should not fail when AppModule have imported RouterModule already', async () => {
8285
const updateRecorder = appTree.beginUpdate('/projects/bar/src/app/app.module.ts');
8386
updateRecorder.insertLeft(0, 'import { RouterModule } from \'@angular/router\';');
8487
appTree.commitUpdate(updateRecorder);
8588

86-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
89+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
90+
.toPromise();
8791
const filePath = '/projects/bar/src/app/app.module.ts';
8892
const content = tree.readContent(filePath);
8993
expect(content).toMatch(/import { RouterModule } from \'@angular\/router\';/);
@@ -119,43 +123,48 @@ describe('App Shell Schematic', () => {
119123
tree.delete('/projects/bar/src/app/app.component.html');
120124
}
121125

122-
it('should not re-add the router outlet (external template)', () => {
126+
it('should not re-add the router outlet (external template)', async () => {
123127
const htmlPath = '/projects/bar/src/app/app.component.html';
124128
appTree.overwrite(htmlPath, '<router-outlet></router-outlet>');
125-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
129+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
130+
.toPromise();
126131

127132
const content = tree.readContent(htmlPath);
128133
const matches = content.match(/<router\-outlet><\/router\-outlet>/g);
129134
const numMatches = matches ? matches.length : 0;
130135
expect(numMatches).toEqual(1);
131136
});
132137

133-
it('should not re-add the router outlet (inline template)', () => {
138+
it('should not re-add the router outlet (inline template)', async () => {
134139
makeInlineTemplate(appTree, '<router-outlet></router-outlet>');
135-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
140+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
141+
.toPromise();
136142
const content = tree.readContent('/projects/bar/src/app/app.component.ts');
137143
const matches = content.match(/<router\-outlet><\/router\-outlet>/g);
138144
const numMatches = matches ? matches.length : 0;
139145
expect(numMatches).toEqual(1);
140146
});
141147
});
142148

143-
it('should add router imports to server module', () => {
144-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
149+
it('should add router imports to server module', async () => {
150+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
151+
.toPromise();
145152
const filePath = '/projects/bar/src/app/app.server.module.ts';
146153
const content = tree.readContent(filePath);
147154
expect(content).toMatch(/import { Routes, RouterModule } from \'@angular\/router\';/);
148155
});
149156

150-
it('should define a server route', () => {
151-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
157+
it('should define a server route', async () => {
158+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
159+
.toPromise();
152160
const filePath = '/projects/bar/src/app/app.server.module.ts';
153161
const content = tree.readContent(filePath);
154162
expect(content).toMatch(/const routes: Routes = \[/);
155163
});
156164

157-
it('should import RouterModule with forRoot', () => {
158-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
165+
it('should import RouterModule with forRoot', async () => {
166+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
167+
.toPromise();
159168
const filePath = '/projects/bar/src/app/app.server.module.ts';
160169
const content = tree.readContent(filePath);
161170
expect(content)
@@ -164,8 +173,9 @@ describe('App Shell Schematic', () => {
164173
.toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/);
165174
});
166175

167-
it('should create the shell component', () => {
168-
const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree);
176+
it('should create the shell component', async () => {
177+
const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree)
178+
.toPromise();
169179
expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true);
170180
const content = tree.readContent('/projects/bar/src/app/app.server.module.ts');
171181
expect(content).toMatch(/app\-shell\.component/);

0 commit comments

Comments
 (0)