Skip to content

Commit fdcd1f7

Browse files
hawkgsvikerman
authored andcommitted
feat(@schematics/angular): lazy routing module generator
Introduces the lazy routing module generator as part of the existing module generator. Feature document: https://docs.google.com/document/d/1NAJEWTG54cu4kW7XBp0gVadmz-gqJouW2XDs4c9PPJM/edit#heading=h.xgjl2srtytjt
1 parent 277d4ab commit fdcd1f7

File tree

8 files changed

+560
-38
lines changed

8 files changed

+560
-38
lines changed

packages/schematics/angular/library/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export default function (options: LibraryOptions): Rule {
227227
commonModule: false,
228228
flat: true,
229229
path: sourceDir,
230-
project: projectName,
230+
project: options.name,
231231
}),
232232
schematic('component', {
233233
name: options.name,
@@ -237,13 +237,13 @@ export default function (options: LibraryOptions): Rule {
237237
flat: true,
238238
path: sourceDir,
239239
export: true,
240-
project: projectName,
240+
project: options.name,
241241
}),
242242
schematic('service', {
243243
name: options.name,
244244
flat: true,
245245
path: sourceDir,
246-
project: projectName,
246+
project: options.name,
247247
}),
248248
options.lintFix ? applyLintFix(sourceDir) : noop(),
249249
(_tree: Tree, context: SchematicContext) => {

packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts.template

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { NgModule } from '@angular/core';
22
import { Routes, RouterModule } from '@angular/router';
3+
<% if (lazyRoute) { %>
4+
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';<% } %>
35

4-
const routes: Routes = [];
6+
const routes: Routes = [<% if (lazyRoute) { %>{ path: '', component: <%= classify(name) %>Component }<% } %>];
57

68
@NgModule({
79
imports: [RouterModule.for<%= routingScope %>(routes)],
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { NgModule } from '@angular/core';<% if (commonModule) { %>
2-
import { CommonModule } from '@angular/common';<% } %><% if (routing) { %>
3-
2+
import { CommonModule } from '@angular/common';<% } %><% if (lazyRouteWithoutRouteModule) { %>
3+
import { Routes, RouterModule } from '@angular/router';<% } %>
4+
<% if (routing || lazyRouteWithRouteModule) { %>
45
import { <%= classify(name) %>RoutingModule } from './<%= dasherize(name) %>-routing.module';<% } %>
6+
<% if (lazyRouteWithoutRouteModule) { %>
7+
const routes: Routes = [
8+
{ path: '', component: <%= classify(name) %>Component }
9+
];<% } %>
510

611
@NgModule({
712
declarations: [],
813
imports: [<% if (commonModule) { %>
9-
CommonModule<%= routing ? ',' : '' %><% } %><% if (routing) { %>
10-
<%= classify(name) %>RoutingModule<% } %>
14+
CommonModule<%= routing || lazyRouteWithRouteModule ? ',' : '' %><% } %><% if (routing || lazyRouteWithRouteModule) { %>
15+
<%= classify(name) %>RoutingModule<% } %><%= lazyRouteWithoutRouteModule ? ',' : '' %><% if (lazyRouteWithoutRouteModule) { %>
16+
RouterModule.forChild(routes)<% } %>
1117
]
1218
})
1319
export class <%= classify(name) %>Module { }

packages/schematics/angular/module/index.ts

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
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 { normalize, strings } from '@angular-devkit/core';
8+
import { Path, normalize, strings } from '@angular-devkit/core';
99
import {
1010
Rule,
1111
SchematicsException,
@@ -17,17 +17,28 @@ import {
1717
mergeWith,
1818
move,
1919
noop,
20+
schematic,
2021
url,
2122
} from '@angular-devkit/schematics';
2223
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
23-
import { addImportToModule } from '../utility/ast-utils';
24+
import { addImportToModule, addRouteDeclarationToModule } from '../utility/ast-utils';
2425
import { InsertChange } from '../utility/change';
2526
import { buildRelativePath, findModuleFromOptions } from '../utility/find-module';
2627
import { applyLintFix } from '../utility/lint-fix';
2728
import { parseName } from '../utility/parse-name';
2829
import { createDefaultPath } from '../utility/workspace';
29-
import { Schema as ModuleOptions } from './schema';
30+
import { RoutingScope, Schema as ModuleOptions } from './schema';
3031

32+
function buildRelativeModulePath(options: ModuleOptions, modulePath: string): string {
33+
const importModulePath = normalize(
34+
`/${options.path}/`
35+
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
36+
+ strings.dasherize(options.name)
37+
+ '.module',
38+
);
39+
40+
return buildRelativePath(modulePath, importModulePath);
41+
}
3142

3243
function addDeclarationToNgModule(options: ModuleOptions): Rule {
3344
return (host: Tree) => {
@@ -41,16 +52,10 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule {
4152
if (text === null) {
4253
throw new SchematicsException(`File ${modulePath} does not exist.`);
4354
}
44-
const sourceText = text.toString('utf-8');
55+
const sourceText = text.toString();
4556
const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true);
4657

47-
const importModulePath = normalize(
48-
`/${options.path}/`
49-
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
50-
+ strings.dasherize(options.name)
51-
+ '.module',
52-
);
53-
const relativePath = buildRelativePath(modulePath, importModulePath);
58+
const relativePath = buildRelativeModulePath(options, modulePath);
5459
const changes = addImportToModule(source,
5560
modulePath,
5661
strings.classify(`${options.name}Module`),
@@ -68,6 +73,66 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule {
6873
};
6974
}
7075

76+
function addRouteDeclarationToNgModule(
77+
options: ModuleOptions,
78+
routingModulePath: Path | undefined,
79+
): Rule {
80+
return (host: Tree) => {
81+
if (!options.route) {
82+
return host;
83+
}
84+
if (!options.module) {
85+
throw new Error('Module option required when creating a lazy loaded routing module.');
86+
}
87+
88+
let path: string;
89+
if (routingModulePath) {
90+
path = routingModulePath;
91+
} else {
92+
path = options.module;
93+
}
94+
95+
const text = host.read(path);
96+
if (!text) {
97+
throw new Error(`Couldn't find the module nor its routing module.`);
98+
}
99+
100+
const sourceText = text.toString();
101+
const addDeclaration = addRouteDeclarationToModule(
102+
ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true),
103+
path,
104+
buildRoute(options, options.module),
105+
) as InsertChange;
106+
107+
const recorder = host.beginUpdate(path);
108+
recorder.insertLeft(addDeclaration.pos, addDeclaration.toAdd);
109+
host.commitUpdate(recorder);
110+
111+
return host;
112+
};
113+
}
114+
115+
function getRoutingModulePath(host: Tree, options: ModuleOptions): Path | undefined {
116+
let path: Path | undefined;
117+
const modulePath = options.module as string;
118+
const routingModuleName = modulePath.split('.')[0] + '-routing';
119+
const { module, ...rest } = options;
120+
121+
try {
122+
path = findModuleFromOptions(host, { module: routingModuleName, ...rest });
123+
} catch {}
124+
125+
return path;
126+
}
127+
128+
function buildRoute(options: ModuleOptions, modulePath: string) {
129+
const relativeModulePath = buildRelativeModulePath(options, modulePath);
130+
const moduleName = `${strings.classify(options.name)}Module`;
131+
const loadChildren = `() => import('${relativeModulePath}').then(m => m.${moduleName})`;
132+
133+
return `{ path: '${options.route}', loadChildren: ${loadChildren} }`;
134+
}
135+
71136
export default function (options: ModuleOptions): Rule {
72137
return async (host: Tree) => {
73138
if (options.path === undefined) {
@@ -82,19 +147,41 @@ export default function (options: ModuleOptions): Rule {
82147
options.name = parsedPath.name;
83148
options.path = parsedPath.path;
84149

150+
let routingModulePath: Path | undefined;
151+
const isLazyLoadedModuleGen = options.route && options.module;
152+
if (isLazyLoadedModuleGen) {
153+
options.routingScope = RoutingScope.Child;
154+
routingModulePath = getRoutingModulePath(host, options);
155+
}
156+
85157
const templateSource = apply(url('./files'), [
86-
options.routing ? noop() : filter(path => !path.endsWith('-routing.module.ts.template')),
158+
options.routing || isLazyLoadedModuleGen && !!routingModulePath
159+
? noop()
160+
: filter(path => !path.endsWith('-routing.module.ts.template')),
87161
applyTemplates({
88162
...strings,
89163
'if-flat': (s: string) => options.flat ? '' : s,
164+
lazyRoute: isLazyLoadedModuleGen,
165+
lazyRouteWithoutRouteModule: isLazyLoadedModuleGen && !routingModulePath,
166+
lazyRouteWithRouteModule: isLazyLoadedModuleGen && routingModulePath,
90167
...options,
91168
}),
92169
move(parsedPath.path),
93170
]);
171+
const moduleDasherized = strings.dasherize(options.name);
172+
const modulePath =
173+
`${!options.flat ? moduleDasherized + '/' : ''}${moduleDasherized}.module.ts`;
94174

95175
return chain([
96-
addDeclarationToNgModule(options),
176+
!isLazyLoadedModuleGen ? addDeclarationToNgModule(options) : noop(),
177+
addRouteDeclarationToNgModule(options, routingModulePath),
97178
mergeWith(templateSource),
179+
isLazyLoadedModuleGen
180+
? schematic('component', {
181+
...options,
182+
module: modulePath,
183+
})
184+
: noop(),
98185
options.lintFix ? applyLintFix(options.path) : noop(),
99186
]);
100187
};

packages/schematics/angular/module/index_spec.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('Module Schematic', () => {
3333
name: 'bar',
3434
inlineStyle: false,
3535
inlineTemplate: false,
36-
routing: false,
36+
routing: true,
3737
skipTests: false,
3838
skipPackageJson: false,
3939
};
@@ -118,4 +118,95 @@ describe('Module Schematic', () => {
118118
.toPromise();
119119
expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.module.ts');
120120
});
121+
122+
describe('lazy route generator', () => {
123+
const options = {
124+
...defaultOptions,
125+
route: '/new-route',
126+
module: 'app',
127+
};
128+
129+
it('should generate a lazy loaded module with a routing module', async () => {
130+
const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise();
131+
const files = tree.files;
132+
133+
expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts');
134+
expect(files).toContain('/projects/bar/src/app/foo/foo-routing.module.ts');
135+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts');
136+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.html');
137+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.css');
138+
139+
const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts');
140+
expect(appRoutingModuleContent).toMatch(
141+
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/,
142+
);
143+
144+
const fooRoutingModuleContent = tree.readContent('/projects/bar/src/app/foo/foo-routing.module.ts');
145+
expect(fooRoutingModuleContent).toMatch(/RouterModule.forChild\(routes\)/);
146+
expect(fooRoutingModuleContent)
147+
.toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/);
148+
});
149+
150+
it('should generate a lazy loaded module with embedded route declarations', async () => {
151+
appTree.overwrite('/projects/bar/src/app/app.module.ts',
152+
`
153+
import { NgModule } from '@angular/core';
154+
import { AppComponent } from './app.component';
155+
156+
@NgModule({
157+
declarations: [
158+
AppComponent
159+
],
160+
imports: [
161+
BrowserModule,
162+
RouterModule.forRoot([])
163+
],
164+
providers: [],
165+
bootstrap: [AppComponent]
166+
})
167+
export class AppModule { }
168+
`,
169+
);
170+
appTree.delete('/projects/bar/src/app/app-routing.module.ts');
171+
172+
const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise();
173+
const files = tree.files;
174+
175+
expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts');
176+
expect(files).not.toContain('/projects/bar/src/app/foo/foo-routing.module.ts');
177+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts');
178+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.html');
179+
expect(files).toContain('/projects/bar/src/app/foo/foo.component.css');
180+
181+
const appModuleContent = tree.readContent('/projects/bar/src/app/app.module.ts');
182+
expect(appModuleContent).toMatch(
183+
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/,
184+
);
185+
186+
const fooModuleContent = tree.readContent('/projects/bar/src/app/foo/foo.module.ts');
187+
expect(fooModuleContent).toMatch(/RouterModule.forChild\(routes\)/);
188+
expect(fooModuleContent)
189+
.toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/);
190+
});
191+
192+
it('should generate a lazy loaded module when "flat" flag is true', async () => {
193+
const tree = await schematicRunner.runSchematicAsync(
194+
'module',
195+
{ ...options, flat: true },
196+
appTree,
197+
).toPromise();
198+
const files = tree.files;
199+
200+
expect(files).toContain('/projects/bar/src/app/foo.module.ts');
201+
expect(files).toContain('/projects/bar/src/app/foo-routing.module.ts');
202+
expect(files).toContain('/projects/bar/src/app/foo.component.ts');
203+
expect(files).toContain('/projects/bar/src/app/foo.component.html');
204+
expect(files).toContain('/projects/bar/src/app/foo.component.css');
205+
206+
const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts');
207+
expect(appRoutingModuleContent).toMatch(
208+
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo.module'\).then\(m => m.FooModule\)/,
209+
);
210+
});
211+
});
121212
});

packages/schematics/angular/module/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
"description": "The scope for the new routing module.",
4040
"default": "Child"
4141
},
42+
"route": {
43+
"type": "string",
44+
"description": "Creates lazy loaded routing module. Requires --module option."
45+
},
4246
"flat": {
4347
"type": "boolean",
4448
"description": "When true, creates the new files at the top level of the current project root. ",

0 commit comments

Comments
 (0)