Skip to content

Commit 463f1d9

Browse files
arturovtmgechev
authored andcommitted
fix(@schematics/angular): handle aliased or existing environment import (#16377)
Closes: #16226
1 parent 6ffa74a commit 463f1d9

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

packages/schematics/angular/service-worker/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@angular-devkit/schematics';
2121
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
2222
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
23-
import { addSymbolToNgModuleMetadata, insertImport, isImported } from '../utility/ast-utils';
23+
import { addSymbolToNgModuleMetadata, getEnvironmentExportName, insertImport, isImported } from '../utility/ast-utils';
2424
import { InsertChange } from '../utility/change';
2525
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
2626
import { getAppModulePath } from '../utility/ng-ast-utils';
@@ -71,10 +71,16 @@ function updateAppModule(mainPath: string): Rule {
7171
// add import for environments
7272
// import { environment } from '../environments/environment';
7373
moduleSource = getTsSourceFile(host, modulePath);
74-
importModule = 'environment';
74+
const environmentExportName = getEnvironmentExportName(moduleSource);
75+
// if environemnt import already exists then use the found one
76+
// otherwise use the default name
77+
importModule = environmentExportName || 'environment';
7578
// TODO: dynamically find environments relative path
7679
importPath = '../environments/environment';
77-
if (!isImported(moduleSource, importModule, importPath)) {
80+
81+
if (!environmentExportName) {
82+
// if environment import was not found then insert the new one
83+
// with default path and default export name
7884
const change = insertImport(moduleSource, modulePath, importModule, importPath);
7985
if (change) {
8086
const recorder = host.beginUpdate(modulePath);
@@ -85,7 +91,7 @@ function updateAppModule(mainPath: string): Rule {
8591

8692
// register SW in app module
8793
const importText =
88-
`ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })`;
94+
`ServiceWorkerModule.register('ngsw-worker.js', { enabled: ${importModule}.production })`;
8995
moduleSource = getTsSourceFile(host, modulePath);
9096
const metadataChanges = addSymbolToNgModuleMetadata(
9197
moduleSource, modulePath, 'imports', importText);

packages/schematics/angular/service-worker/index_spec.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Schema as ApplicationOptions } from '../application/schema';
1010
import { Schema as WorkspaceOptions } from '../workspace/schema';
1111
import { Schema as ServiceWorkerOptions } from './schema';
1212

13-
13+
// tslint:disable-next-line:no-big-function
1414
describe('Service Worker Schematic', () => {
1515
const schematicRunner = new SchematicTestRunner(
1616
'@schematics/angular',
@@ -97,6 +97,64 @@ describe('Service Worker Schematic', () => {
9797
expect(pkgText).toContain(expectedText);
9898
});
9999

100+
it('should add the SW import to the NgModule imports with aliased environment', async () => {
101+
const moduleContent = `
102+
import { BrowserModule } from '@angular/platform-browser';
103+
import { NgModule } from '@angular/core';
104+
105+
import { AppComponent } from './app.component';
106+
import { environment as env } from '../environments/environment';
107+
108+
@NgModule({
109+
declarations: [
110+
AppComponent
111+
],
112+
imports: [
113+
BrowserModule
114+
],
115+
bootstrap: [AppComponent]
116+
})
117+
export class AppModule {}
118+
`;
119+
120+
appTree.overwrite('/projects/bar/src/app/app.module.ts', moduleContent);
121+
122+
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
123+
.toPromise();
124+
const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts');
125+
const expectedText = 'ServiceWorkerModule.register(\'ngsw-worker.js\', { enabled: env.production })';
126+
expect(pkgText).toContain(expectedText);
127+
});
128+
129+
it('should add the SW import to the NgModule imports with existing environment', async () => {
130+
const moduleContent = `
131+
import { BrowserModule } from '@angular/platform-browser';
132+
import { NgModule } from '@angular/core';
133+
134+
import { AppComponent } from './app.component';
135+
import { environment } from '../environments/environment';
136+
137+
@NgModule({
138+
declarations: [
139+
AppComponent
140+
],
141+
imports: [
142+
BrowserModule
143+
],
144+
bootstrap: [AppComponent]
145+
})
146+
export class AppModule {}
147+
`;
148+
149+
appTree.overwrite('/projects/bar/src/app/app.module.ts', moduleContent);
150+
151+
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
152+
.toPromise();
153+
const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts');
154+
const expectedText = 'ServiceWorkerModule.register(\'ngsw-worker.js\', { enabled: environment.production })';
155+
expect(pkgText).toContain(expectedText);
156+
});
157+
100158
it('should put the ngsw-config.json file in the project root', async () => {
101159
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
102160
.toPromise();
@@ -188,5 +246,4 @@ describe('Service Worker Schematic', () => {
188246
expect(projects.foo.architect.build.configurations.production.ngswConfigPath)
189247
.toBe('ngsw-config.json');
190248
});
191-
192249
});

packages/schematics/angular/utility/ast-utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,50 @@ export function isImported(source: ts.SourceFile,
574574
return matchingNodes.length > 0;
575575
}
576576

577+
/**
578+
* This function returns the name of the environment export
579+
* whether this export is aliased or not. If the environment file
580+
* is not imported, then it will return `null`.
581+
*/
582+
export function getEnvironmentExportName(source: ts.SourceFile): string | null {
583+
// Initial value is `null` as we don't know yet if the user
584+
// has imported `environment` into the root module or not.
585+
let environmentExportName: string | null = null;
586+
587+
const allNodes = getSourceNodes(source);
588+
589+
allNodes
590+
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
591+
.filter(
592+
(declaration: ts.ImportDeclaration) =>
593+
declaration.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral &&
594+
declaration.importClause !== undefined,
595+
)
596+
.map((declaration: ts.ImportDeclaration) =>
597+
// If `importClause` property is defined then the first
598+
// child will be `NamedImports` object (or `namedBindings`).
599+
(declaration.importClause as ts.ImportClause).getChildAt(0),
600+
)
601+
// Find those `NamedImports` object that contains `environment` keyword
602+
// in its text. E.g. `{ environment as env }`.
603+
.filter((namedImports: ts.NamedImports) => namedImports.getText().includes('environment'))
604+
.forEach((namedImports: ts.NamedImports) => {
605+
for (const specifier of namedImports.elements) {
606+
// `propertyName` is defined if the specifier
607+
// has an aliased import.
608+
const name = specifier.propertyName || specifier.name;
609+
610+
// Find specifier that contains `environment` keyword in its text.
611+
// Whether it's `environment` or `environment as env`.
612+
if (name.text.includes('environment')) {
613+
environmentExportName = specifier.name.text;
614+
}
615+
}
616+
});
617+
618+
return environmentExportName;
619+
}
620+
577621
/**
578622
* Returns the RouterModule declaration from NgModule metadata, if any.
579623
*/

0 commit comments

Comments
 (0)