Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/schematics/angular/library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default function (options: LibraryOptions): Rule {
commonModule: false,
flat: true,
path: sourceDir,
project: projectName,
project: options.name,
}),
schematic('component', {
name: options.name,
Expand All @@ -237,13 +237,13 @@ export default function (options: LibraryOptions): Rule {
flat: true,
path: sourceDir,
export: true,
project: projectName,
project: options.name,
}),
schematic('service', {
name: options.name,
flat: true,
path: sourceDir,
project: projectName,
project: options.name,
}),
options.lintFix ? applyLintFix(sourceDir) : noop(),
(_tree: Tree, context: SchematicContext) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
<% if (lazyRoute) { %>
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';<% } %>

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

@NgModule({
imports: [RouterModule.for<%= routingScope %>(routes)],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { NgModule } from '@angular/core';<% if (commonModule) { %>
import { CommonModule } from '@angular/common';<% } %><% if (routing) { %>

import { CommonModule } from '@angular/common';<% } %><% if (lazyRouteWithoutRouteModule) { %>
import { Routes, RouterModule } from '@angular/router';<% } %>
<% if (routing || lazyRouteWithRouteModule) { %>
import { <%= classify(name) %>RoutingModule } from './<%= dasherize(name) %>-routing.module';<% } %>
<% if (lazyRouteWithoutRouteModule) { %>
const routes: Routes = [
{ path: '', component: <%= classify(name) %>Component }
];<% } %>

@NgModule({
declarations: [],
imports: [<% if (commonModule) { %>
CommonModule<%= routing ? ',' : '' %><% } %><% if (routing) { %>
<%= classify(name) %>RoutingModule<% } %>
CommonModule<%= routing || lazyRouteWithRouteModule ? ',' : '' %><% } %><% if (routing || lazyRouteWithRouteModule) { %>
<%= classify(name) %>RoutingModule<% } %><%= lazyRouteWithoutRouteModule ? ',' : '' %><% if (lazyRouteWithoutRouteModule) { %>
RouterModule.forChild(routes)<% } %>
]
})
export class <%= classify(name) %>Module { }
113 changes: 100 additions & 13 deletions packages/schematics/angular/module/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { normalize, strings } from '@angular-devkit/core';
import { Path, normalize, strings } from '@angular-devkit/core';
import {
Rule,
SchematicsException,
Expand All @@ -17,17 +17,28 @@ import {
mergeWith,
move,
noop,
schematic,
url,
} from '@angular-devkit/schematics';
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { addImportToModule } from '../utility/ast-utils';
import { addImportToModule, addRouteDeclarationToModule } from '../utility/ast-utils';
import { InsertChange } from '../utility/change';
import { buildRelativePath, findModuleFromOptions } from '../utility/find-module';
import { applyLintFix } from '../utility/lint-fix';
import { parseName } from '../utility/parse-name';
import { createDefaultPath } from '../utility/workspace';
import { Schema as ModuleOptions } from './schema';
import { RoutingScope, Schema as ModuleOptions } from './schema';

function buildRelativeModulePath(options: ModuleOptions, modulePath: string): string {
const importModulePath = normalize(
`/${options.path}/`
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
+ strings.dasherize(options.name)
+ '.module',
);

return buildRelativePath(modulePath, importModulePath);
}

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

const importModulePath = normalize(
`/${options.path}/`
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
+ strings.dasherize(options.name)
+ '.module',
);
const relativePath = buildRelativePath(modulePath, importModulePath);
const relativePath = buildRelativeModulePath(options, modulePath);
const changes = addImportToModule(source,
modulePath,
strings.classify(`${options.name}Module`),
Expand All @@ -68,6 +73,66 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule {
};
}

function addRouteDeclarationToNgModule(
options: ModuleOptions,
routingModulePath: Path | undefined,
): Rule {
return (host: Tree) => {
if (!options.route) {
return host;
}
if (!options.module) {
throw new Error('Module option required when creating a lazy loaded routing module.');
}

let path: string;
if (routingModulePath) {
Comment thread
alan-agius4 marked this conversation as resolved.
path = routingModulePath;
} else {
path = options.module;
}

const text = host.read(path);
if (!text) {
throw new Error(`Couldn't find the module nor its routing module.`);
}

const sourceText = text.toString();
const addDeclaration = addRouteDeclarationToModule(
ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true),
path,
buildRoute(options, options.module),
) as InsertChange;

const recorder = host.beginUpdate(path);
recorder.insertLeft(addDeclaration.pos, addDeclaration.toAdd);
host.commitUpdate(recorder);

return host;
};
}

function getRoutingModulePath(host: Tree, options: ModuleOptions): Path | undefined {
Comment thread
alan-agius4 marked this conversation as resolved.
let path: Path | undefined;
const modulePath = options.module as string;
const routingModuleName = modulePath.split('.')[0] + '-routing';
const { module, ...rest } = options;

try {
path = findModuleFromOptions(host, { module: routingModuleName, ...rest });
} catch {}

return path;
}

function buildRoute(options: ModuleOptions, modulePath: string) {
const relativeModulePath = buildRelativeModulePath(options, modulePath);
const moduleName = `${strings.classify(options.name)}Module`;
const loadChildren = `() => import('${relativeModulePath}').then(m => m.${moduleName})`;

return `{ path: '${options.route}', loadChildren: ${loadChildren} }`;
}

export default function (options: ModuleOptions): Rule {
return async (host: Tree) => {
if (options.path === undefined) {
Expand All @@ -82,19 +147,41 @@ export default function (options: ModuleOptions): Rule {
options.name = parsedPath.name;
options.path = parsedPath.path;

let routingModulePath: Path | undefined;
const isLazyLoadedModuleGen = options.route && options.module;
if (isLazyLoadedModuleGen) {
options.routingScope = RoutingScope.Child;
routingModulePath = getRoutingModulePath(host, options);
}

const templateSource = apply(url('./files'), [
options.routing ? noop() : filter(path => !path.endsWith('-routing.module.ts.template')),
options.routing || isLazyLoadedModuleGen && !!routingModulePath
? noop()
: filter(path => !path.endsWith('-routing.module.ts.template')),
applyTemplates({
...strings,
'if-flat': (s: string) => options.flat ? '' : s,
lazyRoute: isLazyLoadedModuleGen,
lazyRouteWithoutRouteModule: isLazyLoadedModuleGen && !routingModulePath,
lazyRouteWithRouteModule: isLazyLoadedModuleGen && routingModulePath,
...options,
}),
move(parsedPath.path),
]);
const moduleDasherized = strings.dasherize(options.name);
const modulePath =
`${!options.flat ? moduleDasherized + '/' : ''}${moduleDasherized}.module.ts`;

return chain([
addDeclarationToNgModule(options),
!isLazyLoadedModuleGen ? addDeclarationToNgModule(options) : noop(),
addRouteDeclarationToNgModule(options, routingModulePath),
mergeWith(templateSource),
isLazyLoadedModuleGen
? schematic('component', {
...options,
module: modulePath,
})
: noop(),
options.lintFix ? applyLintFix(options.path) : noop(),
]);
};
Expand Down
93 changes: 92 additions & 1 deletion packages/schematics/angular/module/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('Module Schematic', () => {
name: 'bar',
inlineStyle: false,
inlineTemplate: false,
routing: false,
routing: true,
skipTests: false,
skipPackageJson: false,
};
Expand Down Expand Up @@ -118,4 +118,95 @@ describe('Module Schematic', () => {
.toPromise();
expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.module.ts');
});

describe('lazy route generator', () => {
Comment thread
hawkgs marked this conversation as resolved.
const options = {
...defaultOptions,
route: '/new-route',
module: 'app',
};

it('should generate a lazy loaded module with a routing module', async () => {
const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise();
const files = tree.files;

expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts');
expect(files).toContain('/projects/bar/src/app/foo/foo-routing.module.ts');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.html');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.css');

const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts');
expect(appRoutingModuleContent).toMatch(
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/,
);

const fooRoutingModuleContent = tree.readContent('/projects/bar/src/app/foo/foo-routing.module.ts');
expect(fooRoutingModuleContent).toMatch(/RouterModule.forChild\(routes\)/);
expect(fooRoutingModuleContent)
.toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/);
});

it('should generate a lazy loaded module with embedded route declarations', async () => {
appTree.overwrite('/projects/bar/src/app/app.module.ts',
`
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
RouterModule.forRoot([])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
`,
);
appTree.delete('/projects/bar/src/app/app-routing.module.ts');

const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise();
const files = tree.files;

expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts');
expect(files).not.toContain('/projects/bar/src/app/foo/foo-routing.module.ts');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.html');
expect(files).toContain('/projects/bar/src/app/foo/foo.component.css');

const appModuleContent = tree.readContent('/projects/bar/src/app/app.module.ts');
expect(appModuleContent).toMatch(
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/,
);

const fooModuleContent = tree.readContent('/projects/bar/src/app/foo/foo.module.ts');
expect(fooModuleContent).toMatch(/RouterModule.forChild\(routes\)/);
expect(fooModuleContent)
.toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/);
});

it('should generate a lazy loaded module when "flat" flag is true', async () => {
const tree = await schematicRunner.runSchematicAsync(
'module',
{ ...options, flat: true },
appTree,
).toPromise();
const files = tree.files;

expect(files).toContain('/projects/bar/src/app/foo.module.ts');
expect(files).toContain('/projects/bar/src/app/foo-routing.module.ts');
expect(files).toContain('/projects/bar/src/app/foo.component.ts');
expect(files).toContain('/projects/bar/src/app/foo.component.html');
expect(files).toContain('/projects/bar/src/app/foo.component.css');

const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts');
expect(appRoutingModuleContent).toMatch(
/path: '\/new-route', loadChildren: \(\) => import\('.\/foo.module'\).then\(m => m.FooModule\)/,
);
});
});
});
4 changes: 4 additions & 0 deletions packages/schematics/angular/module/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"description": "The scope for the new routing module.",
"default": "Child"
},
"route": {
"type": "string",
"description": "Creates lazy loaded routing module. Requires --module option."
},
"flat": {
"type": "boolean",
"description": "When true, creates the new files at the top level of the current project root. ",
Expand Down
Loading