Skip to content

fix(@schematics/angular): handle aliased or existing environment import #16377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 21, 2019
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
14 changes: 10 additions & 4 deletions packages/schematics/angular/service-worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { addSymbolToNgModuleMetadata, insertImport, isImported } from '../utility/ast-utils';
import { addSymbolToNgModuleMetadata, getEnvironmentExportName, insertImport, isImported } from '../utility/ast-utils';
import { InsertChange } from '../utility/change';
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
import { getAppModulePath } from '../utility/ng-ast-utils';
Expand Down Expand Up @@ -71,10 +71,16 @@ function updateAppModule(mainPath: string): Rule {
// add import for environments
// import { environment } from '../environments/environment';
moduleSource = getTsSourceFile(host, modulePath);
importModule = 'environment';
const environmentExportName = getEnvironmentExportName(moduleSource);
// if environemnt import already exists then use the found one
// otherwise use the default name
importModule = environmentExportName || 'environment';
// TODO: dynamically find environments relative path
importPath = '../environments/environment';
if (!isImported(moduleSource, importModule, importPath)) {

if (!environmentExportName) {
// if environment import was not found then insert the new one
// with default path and default export name
const change = insertImport(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
Expand All @@ -85,7 +91,7 @@ function updateAppModule(mainPath: string): Rule {

// register SW in app module
const importText =
`ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })`;
`ServiceWorkerModule.register('ngsw-worker.js', { enabled: ${importModule}.production })`;
moduleSource = getTsSourceFile(host, modulePath);
const metadataChanges = addSymbolToNgModuleMetadata(
moduleSource, modulePath, 'imports', importText);
Expand Down
61 changes: 59 additions & 2 deletions packages/schematics/angular/service-worker/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Schema as ApplicationOptions } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';
import { Schema as ServiceWorkerOptions } from './schema';


// tslint:disable-next-line:no-big-function
describe('Service Worker Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@schematics/angular',
Expand Down Expand Up @@ -97,6 +97,64 @@ describe('Service Worker Schematic', () => {
expect(pkgText).toContain(expectedText);
});

it('should add the SW import to the NgModule imports with aliased environment', async () => {
const moduleContent = `
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { environment as env } from '../environments/environment';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
bootstrap: [AppComponent]
})
export class AppModule {}
`;

appTree.overwrite('/projects/bar/src/app/app.module.ts', moduleContent);

const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
.toPromise();
const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts');
const expectedText = 'ServiceWorkerModule.register(\'ngsw-worker.js\', { enabled: env.production })';
expect(pkgText).toContain(expectedText);
});

it('should add the SW import to the NgModule imports with existing environment', async () => {
const moduleContent = `
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
bootstrap: [AppComponent]
})
export class AppModule {}
`;

appTree.overwrite('/projects/bar/src/app/app.module.ts', moduleContent);

const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
.toPromise();
const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts');
const expectedText = 'ServiceWorkerModule.register(\'ngsw-worker.js\', { enabled: environment.production })';
expect(pkgText).toContain(expectedText);
});

it('should put the ngsw-config.json file in the project root', async () => {
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
.toPromise();
Expand Down Expand Up @@ -188,5 +246,4 @@ describe('Service Worker Schematic', () => {
expect(projects.foo.architect.build.configurations.production.ngswConfigPath)
.toBe('ngsw-config.json');
});

});
44 changes: 44 additions & 0 deletions packages/schematics/angular/utility/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,50 @@ export function isImported(source: ts.SourceFile,
return matchingNodes.length > 0;
}

/**
* This function returns the name of the environment export
* whether this export is aliased or not. If the environment file
* is not imported, then it will return `null`.
*/
export function getEnvironmentExportName(source: ts.SourceFile): string | null {
// Initial value is `null` as we don't know yet if the user
// has imported `environment` into the root module or not.
let environmentExportName: string | null = null;

const allNodes = getSourceNodes(source);

allNodes
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
.filter(
(declaration: ts.ImportDeclaration) =>
declaration.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral &&
declaration.importClause !== undefined,
)
.map((declaration: ts.ImportDeclaration) =>
// If `importClause` property is defined then the first
// child will be `NamedImports` object (or `namedBindings`).
(declaration.importClause as ts.ImportClause).getChildAt(0),
)
// Find those `NamedImports` object that contains `environment` keyword
// in its text. E.g. `{ environment as env }`.
.filter((namedImports: ts.NamedImports) => namedImports.getText().includes('environment'))
.forEach((namedImports: ts.NamedImports) => {
for (const specifier of namedImports.elements) {
// `propertyName` is defined if the specifier
// has an aliased import.
const name = specifier.propertyName || specifier.name;

// Find specifier that contains `environment` keyword in its text.
// Whether it's `environment` or `environment as env`.
if (name.text.includes('environment')) {
environmentExportName = specifier.name.text;
}
}
});

return environmentExportName;
}

/**
* Returns the RouterModule declaration from NgModule metadata, if any.
*/
Expand Down