Skip to content
This repository was archived by the owner on Aug 7, 2021. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5bafd27

Browse files
committedFeb 5, 2019
feat: support HMR in Angular project by inserting the required code snippets using another AngularCompierPlugin transformer
1 parent cc82df3 commit 5bafd27

File tree

8 files changed

+563
-113
lines changed

8 files changed

+563
-113
lines changed
 

‎templates/webpack.angular.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const nsWebpack = require("nativescript-dev-webpack");
55
const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target");
66
const { nsReplaceBootstrap } = require("nativescript-dev-webpack/transformers/ns-replace-bootstrap");
77
const { nsReplaceLazyLoader } = require("nativescript-dev-webpack/transformers/ns-replace-lazy-loader");
8+
const { nsSupportHmrNg } = require("nativescript-dev-webpack/transformers/ns-support-hmr-ng");
89
const { getMainModulePath } = require("nativescript-dev-webpack/utils/ast-utils");
910
const CleanWebpackPlugin = require("clean-webpack-plugin");
1011
const CopyWebpackPlugin = require("copy-webpack-plugin");
@@ -73,9 +74,13 @@ module.exports = env => {
7374
}
7475
}
7576

77+
if (hmr) {
78+
ngCompilerTransformers.push(nsSupportHmrNg);
79+
}
80+
7681
const ngCompilerPlugin = new AngularCompilerPlugin({
7782
hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]),
78-
platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin)),
83+
platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin, resolve(appFullPath, entryModule))),
7984
mainPath: resolve(appPath, entryModule),
8085
tsConfigPath: join(__dirname, "tsconfig.tns.json"),
8186
skipCodeGeneration: !aot,

‎templates/webpack.config.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const webpackConfigAngular = proxyquire('./webpack.angular', {
2424
'nativescript-dev-webpack/nativescript-target': emptyObject,
2525
'nativescript-dev-webpack/transformers/ns-replace-bootstrap': emptyObject,
2626
'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': emptyObject,
27+
'nativescript-dev-webpack/transformers/ns-support-hmr-ng': emptyObject,
2728
'nativescript-dev-webpack/utils/ast-utils': emptyObject,
2829
'@ngtools/webpack': {
2930
AngularCompilerPlugin: EmptyClass

‎transformers/ns-replace-bootstrap.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ describe('@ngtools/webpack transformers', () => {
1414
`;
1515

1616
const output = tags.stripIndent`
17-
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
18-
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
17+
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
18+
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";
1919
20-
__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
20+
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
2121
`;
2222

2323
const { program, compilerHost } = createTypescriptContext(input);
2424
const ngCompiler = <AngularCompilerPlugin>{
2525
typeChecker: program.getTypeChecker(),
2626
entryModule: {
27-
path: '/project/src/app/app.module',
27+
path: '/project/src/app/app.module',
2828
className: 'AppModule',
2929
},
3030
};
@@ -43,17 +43,17 @@ describe('@ngtools/webpack transformers', () => {
4343
`;
4444

4545
const output = tags.stripIndent`
46-
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
47-
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
46+
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
47+
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";
4848
49-
__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
49+
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
5050
`;
5151

5252
const { program, compilerHost } = createTypescriptContext(input);
5353
const ngCompiler = <AngularCompilerPlugin>{
5454
typeChecker: program.getTypeChecker(),
5555
entryModule: {
56-
path: '/project/src/app/app.module',
56+
path: '/project/src/app/app.module',
5757
className: 'AppModule',
5858
},
5959
};
@@ -73,18 +73,18 @@ describe('@ngtools/webpack transformers', () => {
7373
`;
7474

7575
const output = tags.stripIndent`
76-
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
77-
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
76+
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
77+
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";
7878
import "./shared/kinvey.common";
7979
80-
__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
80+
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
8181
`;
8282

8383
const { program, compilerHost } = createTypescriptContext(input);
8484
const ngCompiler = <AngularCompilerPlugin>{
8585
typeChecker: program.getTypeChecker(),
8686
entryModule: {
87-
path: '/project/src/app/app.module',
87+
path: '/project/src/app/app.module',
8888
className: 'AppModule',
8989
},
9090
};

‎transformers/ns-replace-bootstrap.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
makeTransform,
1010
getFirstNode
1111
} from "@ngtools/webpack/src/transformers";
12+
import {
13+
getExpressionName
14+
} from "../utils/ast-utils";
1215
import { AngularCompilerPlugin } from '@ngtools/webpack';
1316
import { getResolvedEntryModule } from "../utils/transformers-utils";
1417

@@ -47,39 +50,46 @@ export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin):
4750
return;
4851
}
4952

50-
const callExpr = entryModuleIdentifier.parent as ts.CallExpression;
53+
const bootstrapCallExpr = entryModuleIdentifier.parent as ts.CallExpression;
5154

52-
if (callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) {
55+
if (bootstrapCallExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) {
5356
return;
5457
}
5558

56-
const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression;
59+
const bootstrapPropAccessExpr = bootstrapCallExpr.expression as ts.PropertyAccessExpression;
5760

58-
if (propAccessExpr.name.text !== 'bootstrapModule'
59-
|| propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) {
61+
if (bootstrapPropAccessExpr.name.text !== 'bootstrapModule'
62+
|| bootstrapPropAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) {
6063
return;
6164
}
6265

63-
const bootstrapModuleIdentifier = propAccessExpr.name;
64-
const innerCallExpr = propAccessExpr.expression as ts.CallExpression;
65-
66-
if (!(
67-
innerCallExpr.expression.kind === ts.SyntaxKind.Identifier
68-
&& (innerCallExpr.expression as ts.Identifier).text === 'platformNativeScriptDynamic'
69-
)) {
66+
const nsPlatformCallExpr = bootstrapPropAccessExpr.expression as ts.CallExpression;
67+
if (!(getExpressionName(nsPlatformCallExpr.expression) === 'platformNativeScriptDynamic')) {
7068
return;
7169
}
7270

73-
const platformNativeScriptDynamicIdentifier = innerCallExpr.expression as ts.Identifier;
74-
75-
const idPlatformNativeScript = ts.createUniqueName('__NgCli_bootstrap_');
76-
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');
71+
const idPlatformNativeScript = ts.createUniqueName('__NgCli_bootstrap_1');
72+
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_2');
7773

7874
const firstNode = getFirstNode(sourceFile);
7975

8076
// Add the transform operations.
8177
const factoryClassName = entryModule.className + 'NgFactory';
8278
const factoryModulePath = normalizedEntryModulePath + '.ngfactory';
79+
80+
81+
const newBootstrapPropAccessExpr = ts.getMutableClone(bootstrapPropAccessExpr);
82+
const newNsPlatformCallExpr = ts.getMutableClone(bootstrapPropAccessExpr.expression) as ts.CallExpression;
83+
newNsPlatformCallExpr.expression = ts.createPropertyAccess(idPlatformNativeScript, 'platformNativeScript');
84+
newBootstrapPropAccessExpr.expression = newNsPlatformCallExpr;
85+
newBootstrapPropAccessExpr.name = ts.createIdentifier("bootstrapModuleFactory");
86+
87+
const newBootstrapCallExpr = ts.getMutableClone(bootstrapCallExpr);
88+
newBootstrapCallExpr.expression = newBootstrapPropAccessExpr;
89+
newBootstrapCallExpr.arguments = ts.createNodeArray([
90+
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))
91+
]);
92+
8393
ops.push(
8494
// Insert an import of the {N} Angular static bootstrap module in the beginning of the file:
8595
// import * as __NgCli_bootstrap_2 from "nativescript-angular/platform-static";
@@ -101,19 +111,10 @@ export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin):
101111
true,
102112
),
103113

104-
// Replace the NgModule nodes with NgModuleFactory nodes
105-
// from 'AppModule' to 'AppModuleNgFactory'
106-
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
107-
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))),
108-
109-
// Replace 'platformNativeScriptDynamic' with 'platformNativeScript'
110-
// and elide all imports of 'platformNativeScriptDynamic'
111-
new ReplaceNodeOperation(sourceFile, platformNativeScriptDynamicIdentifier,
112-
ts.createPropertyAccess(idPlatformNativeScript, 'platformNativeScript')),
113-
114-
// Replace the invocation of 'boostrapModule' with 'bootsrapModuleFactory'
115-
new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier,
116-
ts.createIdentifier('bootstrapModuleFactory')),
114+
// Replace the bootstrap call expression. For example:
115+
// from: platformNativeScriptDynamic().bootstrapModule(AppModule);
116+
// to: platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
117+
new ReplaceNodeOperation(sourceFile, bootstrapCallExpr, newBootstrapCallExpr),
117118
);
118119
});
119120

‎transformers/ns-replace-lazy-loader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import {
1414
makeTransform
1515
} from "@ngtools/webpack/src/transformers";
1616
import { AngularCompilerPlugin } from "@ngtools/webpack";
17-
import { findNode, getObjectPropertyMatches, getDecoratorMetadata } from "../utils/ast-utils";
17+
import { findIdentifierNode, getObjectPropertyMatches, getDecoratorMetadata } from "../utils/ast-utils";
1818
import { getResolvedEntryModule } from "../utils/transformers-utils";
1919

2020
export function nsReplaceLazyLoader(getNgCompiler: () => AngularCompilerPlugin): ts.TransformerFactory<ts.SourceFile> {
2121
const getTypeChecker = () => getNgCompiler().typeChecker;
2222

23-
const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) {
23+
const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) {
2424
let ops: TransformOperation[] = [];
2525
const entryModule = getResolvedEntryModule(getNgCompiler());
2626
const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName)));
@@ -63,7 +63,7 @@ export function addArrayPropertyValueToNgModule(
6363
const ngModuleConfigIndentifierNode = ngModuleConfigNode as ts.Identifier;
6464
// cases like @NgModule(myCoolConfig)
6565
const configObjectDeclarationNodes = collectDeepNodes<ts.Node>(sourceFile, ts.SyntaxKind.VariableStatement).filter(imp => {
66-
return findNode(imp, ts.SyntaxKind.Identifier, ngModuleConfigIndentifierNode.getText());
66+
return findIdentifierNode(imp, ngModuleConfigIndentifierNode.getText());
6767
});
6868
// will be undefined when the object is imported from another file
6969
const configObjectDeclaration = (configObjectDeclarationNodes && configObjectDeclarationNodes[0]);
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { tags } from "@angular-devkit/core";
2+
import { createTypescriptContext, transformTypescript } from "@ngtools/webpack/src/transformers";
3+
import { nsReplaceBootstrap } from './ns-replace-bootstrap';
4+
import { nsSupportHmrNg, getHandleHmrOptionsCode, getAcceptMainModuleCode, GeneratedDynamicAppOptions } from "./ns-support-hmr-ng";
5+
import { AngularCompilerPlugin } from "@ngtools/webpack";
6+
7+
describe("@ngtools/webpack transformers", () => {
8+
describe("ns-support-hmr-ng", () => {
9+
const nsFactoryImportName = `__NgCli_bootstrap_2_1`;
10+
const handleHmrPlatformDynamicImport = `import * as nativescript_angular_platform_Generated from "nativescript-angular/platform";`;
11+
const handleHmrPlatformStaticImport = `import * as nativescript_angular_platform_Generated from "nativescript-angular/platform-static";`;
12+
const handleAotPlatformStaticImport = `import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";`;
13+
const handleAotNgFactoryImport = `import * as ${nsFactoryImportName} from "./test-file.ts.ngfactory";`;
14+
const handleHmrOptionsDeclaration = `var ${GeneratedDynamicAppOptions} = {};`;
15+
const nsStaticPlatformCall = `nativescript_angular_platform_Generated.platformNativeScript`;
16+
const nsDynamicPlatformCall = `nativescript_angular_platform_Generated.platformNativeScriptDynamic`;
17+
const handleHmrOptionsCode = getHandleHmrOptionsCode("AppModule", "./app/app.module");
18+
const acceptMainModuleCode = getAcceptMainModuleCode("./app/app.module");
19+
const handleHmrOptionsAotCode = getHandleHmrOptionsCode("AppModuleNgFactory", "./test-file.ts.ngfactory");
20+
const acceptMainModuleAotCode = getAcceptMainModuleCode("./test-file.ts.ngfactory");
21+
const testCases = [{
22+
name: "should handle HMR when platformNativeScriptDynamic is called without arguments",
23+
rawFile: `
24+
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
25+
import { AppModule } from "./app/app.module";
26+
platformNativeScriptDynamic().bootstrapModule(AppModule);
27+
`,
28+
transformedFile: `
29+
${handleHmrPlatformDynamicImport}
30+
import { AppModule } from "./app/app.module";
31+
32+
${handleHmrOptionsDeclaration}
33+
${handleHmrOptionsCode}
34+
${acceptMainModuleCode}
35+
36+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule);
37+
`,
38+
transformedFileWithAot: `
39+
${handleHmrPlatformStaticImport}
40+
${handleAotPlatformStaticImport}
41+
${handleAotNgFactoryImport}
42+
43+
${handleHmrOptionsDeclaration}
44+
${handleHmrOptionsAotCode}
45+
${acceptMainModuleAotCode}
46+
47+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory);
48+
`
49+
},
50+
{
51+
name: "should handle HMR when AOT is manually configured",
52+
rawFile: `
53+
import { platformNativeScript } from "nativescript-angular/platform-static";
54+
import { AppModuleNgFactory } from "./app/app.module.ngfactory";
55+
platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory);
56+
`,
57+
transformedFile: `
58+
${handleHmrPlatformStaticImport}
59+
import { AppModuleNgFactory } from "./app/app.module.ngfactory";
60+
61+
${handleHmrOptionsDeclaration}
62+
${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")}
63+
${getAcceptMainModuleCode("./app/app.module.ngfactory")}
64+
65+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory);
66+
`,
67+
transformedFileWithAot: `
68+
${handleHmrPlatformStaticImport}
69+
import { AppModuleNgFactory } from "./app/app.module.ngfactory";
70+
71+
${handleHmrOptionsDeclaration}
72+
${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")}
73+
${getAcceptMainModuleCode("./app/app.module.ngfactory")}
74+
75+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory);
76+
`
77+
},
78+
{
79+
name: "should handle HMR when platformNativeScriptDynamic is called without arguments and non default app module",
80+
customAppModuleName: "CustomModule",
81+
rawFile: `
82+
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
83+
import { CustomModule } from "./custom/custom.module";
84+
platformNativeScriptDynamic().bootstrapModule(CustomModule);
85+
`,
86+
transformedFile: `
87+
${handleHmrPlatformDynamicImport}
88+
import { CustomModule } from "./custom/custom.module";
89+
90+
${handleHmrOptionsDeclaration}
91+
${getHandleHmrOptionsCode("CustomModule", "./custom/custom.module")}
92+
${getAcceptMainModuleCode("./custom/custom.module")}
93+
94+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(CustomModule);
95+
`,
96+
transformedFileWithAot: `
97+
${handleHmrPlatformStaticImport}
98+
${handleAotPlatformStaticImport}
99+
${handleAotNgFactoryImport}
100+
101+
${handleHmrOptionsDeclaration}
102+
${getHandleHmrOptionsCode("CustomModuleNgFactory", "./test-file.ts.ngfactory")}
103+
${getAcceptMainModuleCode("./test-file.ts.ngfactory")}
104+
105+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.CustomModuleNgFactory);
106+
`
107+
},
108+
{
109+
name: "should handle HMR when platformNativeScriptDynamic is called from * import",
110+
rawFile: `
111+
import * as nsNgPlatform from "nativescript-angular/platform";
112+
import { AppModule } from "./app/app.module";
113+
nsNgPlatform.platformNativeScriptDynamic().bootstrapModule(AppModule);
114+
`,
115+
transformedFile: `
116+
${handleHmrPlatformDynamicImport}
117+
import { AppModule } from "./app/app.module";
118+
119+
${handleHmrOptionsDeclaration}
120+
${handleHmrOptionsCode}
121+
${acceptMainModuleCode}
122+
123+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule);
124+
`,
125+
transformedFileWithAot: `
126+
${handleHmrPlatformStaticImport}
127+
${handleAotPlatformStaticImport}
128+
${handleAotNgFactoryImport}
129+
130+
${handleHmrOptionsDeclaration}
131+
${handleHmrOptionsAotCode}
132+
${acceptMainModuleAotCode}
133+
134+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory);
135+
`
136+
},
137+
{
138+
name: "should handle HMR when platformNativeScriptDynamic is called with appOptions",
139+
rawFile: `
140+
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
141+
import { AppModule } from "./app/app.module";
142+
platformNativeScriptDynamic({ bootInExistingPage: true }).bootstrapModule(AppModule);
143+
`,
144+
transformedFile: `
145+
${handleHmrPlatformDynamicImport}
146+
import { AppModule } from "./app/app.module";
147+
148+
var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true };
149+
${handleHmrOptionsCode}
150+
${acceptMainModuleCode}
151+
152+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule);
153+
`,
154+
transformedFileWithAot: `
155+
${handleHmrPlatformStaticImport}
156+
${handleAotPlatformStaticImport}
157+
${handleAotNgFactoryImport}
158+
159+
var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true };
160+
${handleHmrOptionsAotCode}
161+
${acceptMainModuleAotCode}
162+
163+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory);
164+
`
165+
},
166+
{
167+
name: "should handle HMR when platformNativeScriptDynamic is called with multiple arguments",
168+
rawFile: `
169+
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
170+
import { AppModule } from "./app/app.module";
171+
platformNativeScriptDynamic({ bootInExistingPage: true }, ["provider1", "provider2"]).bootstrapModule(AppModule);
172+
`,
173+
transformedFile: `
174+
${handleHmrPlatformDynamicImport}
175+
import { AppModule } from "./app/app.module";
176+
177+
var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true };
178+
${handleHmrOptionsCode}
179+
${acceptMainModuleCode}
180+
181+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModule(AppModule);
182+
`,
183+
transformedFileWithAot: `
184+
${handleHmrPlatformStaticImport}
185+
${handleAotPlatformStaticImport}
186+
${handleAotNgFactoryImport}
187+
188+
var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true };
189+
${handleHmrOptionsAotCode}
190+
${acceptMainModuleAotCode}
191+
192+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory);
193+
`
194+
},
195+
{
196+
name: "should accept HMR before the user when custom handling is in place",
197+
rawFile: `
198+
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
199+
import { AppModule } from "./app/app.module";
200+
201+
if (module["hot"]) {
202+
module["hot"].accept(["./app/app.module"], function () {
203+
// customHandling
204+
});
205+
}
206+
207+
platformNativeScriptDynamic().bootstrapModule(AppModule);
208+
`,
209+
transformedFile: `
210+
${handleHmrPlatformDynamicImport}
211+
import { AppModule } from "./app/app.module";
212+
213+
${handleHmrOptionsDeclaration}
214+
${handleHmrOptionsCode}
215+
${acceptMainModuleCode}
216+
217+
if (module["hot"]) {
218+
module["hot"].accept(["./app/app.module"], function () {
219+
// customHandling
220+
});
221+
}
222+
223+
${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule);
224+
`,
225+
transformedFileWithAot: `
226+
${handleHmrPlatformStaticImport}
227+
${handleAotPlatformStaticImport}
228+
${handleAotNgFactoryImport}
229+
230+
${handleHmrOptionsDeclaration}
231+
${handleHmrOptionsAotCode}
232+
${acceptMainModuleAotCode}
233+
234+
if (module["hot"]) {
235+
module["hot"].accept(["./app/app.module"], function () {
236+
// customHandling
237+
});
238+
}
239+
240+
${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory);
241+
`
242+
}
243+
];
244+
testCases.forEach((testCase: any) => {
245+
it(`${testCase.name}`, async () => {
246+
const testFile = "/project/src/test-file.ts";
247+
const input = tags.stripIndent`${testCase.rawFile}`;
248+
const output = tags.stripIndent`${testCase.transformedFile}`;
249+
const { program, compilerHost } = createTypescriptContext(input);
250+
const ngCompiler = <AngularCompilerPlugin>{
251+
typeChecker: program.getTypeChecker(),
252+
entryModule: {
253+
path: testFile,
254+
className: "AppModule",
255+
},
256+
};
257+
const transformer = nsSupportHmrNg(() => ngCompiler, testFile);
258+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
259+
260+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
261+
});
262+
263+
it(`${testCase.name} (in combination with AOT transformer)`, async () => {
264+
const testFile = "/project/src/test-file.ts";
265+
const input = tags.stripIndent`${testCase.rawFile}`;
266+
const output = tags.stripIndent`${testCase.transformedFileWithAot}`;
267+
const { program, compilerHost } = createTypescriptContext(input);
268+
const ngCompiler = <AngularCompilerPlugin>{
269+
typeChecker: program.getTypeChecker(),
270+
entryModule: {
271+
path: testFile,
272+
className: testCase.customAppModuleName || "AppModule",
273+
},
274+
};
275+
276+
const aotTransformer = nsReplaceBootstrap(() => ngCompiler);
277+
const hmrTransformer = nsSupportHmrNg(() => ngCompiler, testFile);
278+
const result = transformTypescript(undefined, [aotTransformer, hmrTransformer], program, compilerHost);
279+
280+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
281+
});
282+
});
283+
});
284+
});

‎transformers/ns-support-hmr-ng.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { normalize } from "path";
2+
import * as ts from "typescript";
3+
import {
4+
AddNodeOperation,
5+
ReplaceNodeOperation,
6+
StandardTransform,
7+
TransformOperation,
8+
collectDeepNodes,
9+
makeTransform,
10+
insertStarImport
11+
} from "@ngtools/webpack/src/transformers";
12+
import { AngularCompilerPlugin } from "@ngtools/webpack";
13+
import {
14+
findBootstrappedModulePathInSource,
15+
findNativeScriptPlatformPathInSource,
16+
findBootstrapModuleCallInSource,
17+
findNativeScriptPlatformCallInSource,
18+
getExpressionName
19+
} from "../utils/ast-utils";
20+
21+
export function nsSupportHmrNg(getNgCompiler: () => AngularCompilerPlugin, entryPath: string): ts.TransformerFactory<ts.SourceFile> {
22+
const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) {
23+
let ops: TransformOperation[] = [];
24+
25+
if (!entryPath || normalize(sourceFile.fileName) !== normalize(entryPath)) {
26+
return ops;
27+
}
28+
29+
try {
30+
ops = handleHmrSupport(sourceFile);
31+
} catch (e) {
32+
ops = [];
33+
}
34+
35+
return ops;
36+
};
37+
38+
return makeTransform(standardTransform, () => getNgCompiler().typeChecker);
39+
}
40+
41+
export function handleHmrSupport(
42+
mainFile: ts.SourceFile
43+
): TransformOperation[] {
44+
const importNodesInFile = collectDeepNodes(mainFile, ts.SyntaxKind.ImportDeclaration);
45+
if (!importNodesInFile || !importNodesInFile.length) {
46+
return [];
47+
}
48+
49+
const bootstrapModuleCallNode = findBootstrapModuleCallInSource(mainFile);
50+
if (!bootstrapModuleCallNode || !bootstrapModuleCallNode.arguments || !bootstrapModuleCallNode.arguments.length) {
51+
return [];
52+
}
53+
54+
const appModuleName = getExpressionName(bootstrapModuleCallNode.arguments[0]);
55+
const nativeScriptPlatformCallNode = findNativeScriptPlatformCallInSource(mainFile);
56+
if (!nativeScriptPlatformCallNode || !nativeScriptPlatformCallNode.arguments) {
57+
return [];
58+
}
59+
60+
return handleHmrSupportCore(mainFile, importNodesInFile, appModuleName, nativeScriptPlatformCallNode);
61+
}
62+
63+
function handleHmrSupportCore(mainFile: ts.SourceFile, importNodesInFile: ts.Node[], appModuleName: string, nativeScriptPlatformCallNode: ts.CallExpression) {
64+
const firstImportNode = importNodesInFile[0];
65+
const lastImportNode = importNodesInFile[importNodesInFile.length - 1];
66+
const appModulePath = findBootstrappedModulePathInSource(mainFile);
67+
let currentAppOptionsInitializationNode: ts.Expression = ts.createObjectLiteral();
68+
if (nativeScriptPlatformCallNode.arguments.length > 0) {
69+
currentAppOptionsInitializationNode = nativeScriptPlatformCallNode.arguments[0];
70+
}
71+
72+
const optionsDeclaration = ts.createVariableDeclaration(GeneratedDynamicAppOptions, undefined, currentAppOptionsInitializationNode);
73+
const optionsDeclarationList = ts.createVariableDeclarationList([optionsDeclaration]);
74+
const optionsStatement = ts.createVariableStatement(undefined, optionsDeclarationList);
75+
76+
const handleHmrOptionsNode = ts.createIdentifier(getHandleHmrOptionsCode(appModuleName, appModulePath));
77+
78+
const acceptHmrNode = ts.createIdentifier(getAcceptMainModuleCode(appModulePath));
79+
80+
const newNsDynamicCallArgs = ts.createNodeArray([ts.createIdentifier(GeneratedDynamicAppOptions), ...nativeScriptPlatformCallNode.arguments.slice(1)]);
81+
const nsPlatformPath = findNativeScriptPlatformPathInSource(mainFile);
82+
const nsPlatformText = getExpressionName(nativeScriptPlatformCallNode.expression);
83+
const newNsDynamicCallNode = ts.createCall(ts.createPropertyAccess(ts.createIdentifier(NsNgPlatformStarImport), ts.createIdentifier(nsPlatformText)), [], newNsDynamicCallArgs);
84+
85+
return [
86+
...insertStarImport(mainFile, ts.createIdentifier(NsNgPlatformStarImport), nsPlatformPath, firstImportNode, true),
87+
new AddNodeOperation(mainFile, lastImportNode, undefined, optionsStatement),
88+
new AddNodeOperation(mainFile, lastImportNode, undefined, handleHmrOptionsNode),
89+
new AddNodeOperation(mainFile, lastImportNode, undefined, acceptHmrNode),
90+
new ReplaceNodeOperation(mainFile, nativeScriptPlatformCallNode, newNsDynamicCallNode)
91+
];
92+
}
93+
94+
export const GeneratedDynamicAppOptions = "options_Generated";
95+
const NsNgPlatformStarImport = "nativescript_angular_platform_Generated";
96+
97+
export function getHandleHmrOptionsCode(appModuleName: string, appModulePath: string) {
98+
return `
99+
if (module["hot"]) {
100+
${GeneratedDynamicAppOptions} = Object.assign(${GeneratedDynamicAppOptions}, {
101+
hmrOptions: {
102+
moduleTypeFactory: function () { return require("${appModulePath}").${appModuleName}; },
103+
livesyncCallback: function (platformReboot) { setTimeout(platformReboot, 0); }
104+
}
105+
});
106+
}
107+
`
108+
}
109+
110+
export function getAcceptMainModuleCode(mainModulePath: string) {
111+
return `
112+
if (module["hot"]) {
113+
module["hot"].accept(["${mainModulePath}"], function () {
114+
global["hmrRefresh"]({});
115+
});
116+
}
117+
`;
118+
}

‎utils/ast-utils.ts

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,130 +3,127 @@
33
// https://github.com/angular/angular-cli/blob/d202480a1707be6575b2c8cf0383cfe6db44413c/packages/schematics/angular/utility/ng-ast-utils.ts
44
// https://github.com/NativeScript/nativescript-schematics/blob/438b9e3ef613389980bfa9d071e28ca1f32ab04f/src/ast-utils.ts
55

6+
// important notes:
7+
// 1) DO NOT USE `null` when building nodes or you will get `Cannot read property 'transformFlags' of null`
8+
// https://github.com/Microsoft/TypeScript/issues/22372#issuecomment-371221056
9+
// 2) DO NOT USE `node.getText()` or `node.getFullText()` while analyzing the AST - it is trying to read
10+
// the text from the source file and if the node is affected by another transformer, it will lead to
11+
// an unexpected behavior. You can use `identifier.text` instead.
12+
// 3) DO NOT USE `node.parent` while analyzing the AST. It will be null when the node is replaced by
13+
// another transformer and will lead to an exception. Take a look at `findMethodCallInSource` for an
14+
// example of a working workaround by searching for content in each parent.
15+
// 4) Always test your transformer both single and in combinations with the other ones.
16+
617
import { dirname, join } from "path";
718
import * as ts from "typescript";
819
import { readFileSync, existsSync } from "fs";
920
import { collectDeepNodes } from "@ngtools/webpack/src/transformers";
1021

1122
export function getMainModulePath(entryFilePath) {
1223
try {
13-
return findBootstrapModulePath(entryFilePath);
24+
return findBootstrappedModulePath(entryFilePath);
1425
} catch (e) {
1526
return null;
1627
}
1728
}
1829

1930
export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | null {
20-
if (!existsSync(mainPath)) {
21-
throw new Error(`Main file (${mainPath}) not found`);
22-
}
23-
const mainText = readFileSync(mainPath, "utf8");
24-
25-
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true);
26-
27-
const allNodes = getSourceNodes(source);
31+
const source = getSourceFile(mainPath);
2832

29-
let bootstrapCall: ts.CallExpression | null = null;
30-
31-
for (const node of allNodes) {
32-
33-
let bootstrapCallNode: ts.Node | null = null;
34-
bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, "bootstrapModule");
33+
return findBootstrapModuleCallInSource(source);
34+
}
3535

36-
// Walk up the parent until CallExpression is found.
37-
while (bootstrapCallNode && bootstrapCallNode.parent
38-
&& bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression) {
36+
export function findBootstrapModuleCallInSource(source: ts.SourceFile): ts.CallExpression | null {
37+
return findMethodCallInSource(source, "bootstrapModule") || findMethodCallInSource(source, "bootstrapModuleFactory");
38+
}
39+
export function findNativeScriptPlatformCallInSource(source: ts.SourceFile): ts.CallExpression | null {
40+
return findMethodCallInSource(source, "platformNativeScriptDynamic") || findMethodCallInSource(source, "platformNativeScript");
41+
}
3942

40-
bootstrapCallNode = bootstrapCallNode.parent;
41-
}
43+
export function findMethodCallInSource(source: ts.SourceFile, methodName: string): ts.CallExpression | null {
44+
const allMethodCalls = collectDeepNodes<ts.CallExpression>(source, ts.SyntaxKind.CallExpression);
45+
let methodCallNode: ts.CallExpression | null = null;
4246

43-
if (bootstrapCallNode !== null &&
44-
bootstrapCallNode.parent !== undefined &&
45-
bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression) {
46-
bootstrapCall = bootstrapCallNode.parent as ts.CallExpression;
47-
break;
47+
for (const callNode of allMethodCalls) {
48+
const currentMethodName = getExpressionName(callNode.expression);
49+
if (methodName === currentMethodName) {
50+
methodCallNode = callNode;
4851
}
4952
}
5053

51-
return bootstrapCall;
54+
return methodCallNode;
5255
}
5356

54-
export function findBootstrapModulePath(mainPath: string): string {
55-
const bootstrapCall = findBootstrapModuleCall(mainPath);
57+
export function findBootstrappedModulePath(mainPath: string): string {
58+
const source = getSourceFile(mainPath);
59+
60+
return findBootstrappedModulePathInSource(source);
61+
}
62+
63+
export function findBootstrappedModulePathInSource(source: ts.SourceFile): string {
64+
const bootstrapCall = findBootstrapModuleCallInSource(source);
5665
if (!bootstrapCall) {
5766
throw new Error("Bootstrap call not found");
5867
}
5968

60-
const bootstrapModule = bootstrapCall.arguments[0];
61-
if (!existsSync(mainPath)) {
62-
throw new Error(`Main file (${mainPath}) not found`);
69+
const appModulePath = getExpressionImportPath(source, bootstrapCall.arguments[0]);
70+
71+
return appModulePath;
72+
}
73+
74+
export function findNativeScriptPlatformPathInSource(source: ts.SourceFile): string {
75+
const nsPlatformCall = findNativeScriptPlatformCallInSource(source);
76+
if (!nsPlatformCall) {
77+
throw new Error("NativeScriptPlatform call not found");
6378
}
64-
const mainText = readFileSync(mainPath, "utf8");
6579

66-
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true);
67-
const allNodes = getSourceNodes(source);
68-
const bootstrapModuleRelativePath = allNodes
69-
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
80+
const nsPlatformImportPath = getExpressionImportPath(source, nsPlatformCall.expression);
81+
82+
return nsPlatformImportPath;
83+
}
84+
85+
function getImportPathInSource(source: ts.SourceFile, importName: string) {
86+
const allImports = collectDeepNodes(source, ts.SyntaxKind.ImportDeclaration);
87+
const importPath = allImports
7088
.filter(imp => {
71-
return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText());
89+
return findIdentifierNode(imp, importName);
7290
})
7391
.map((imp: ts.ImportDeclaration) => {
7492
const modulePathStringLiteral = imp.moduleSpecifier as ts.StringLiteral;
75-
7693
return modulePathStringLiteral.text;
7794
})[0];
78-
79-
return bootstrapModuleRelativePath;
95+
return importPath;
8096
}
8197

8298
export function getAppModulePath(mainPath: string): string {
83-
const moduleRelativePath = findBootstrapModulePath(mainPath);
99+
const moduleRelativePath = findBootstrappedModulePath(mainPath);
84100
const mainDir = dirname(mainPath);
85101
const modulePath = join(mainDir, `${moduleRelativePath}.ts`);
86102

87103
return modulePath;
88104
}
89105

90-
export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null {
91-
if (node.kind === kind && node.getText() === text) {
106+
export function findIdentifierNode(node: ts.Node, text: string): ts.Node | null {
107+
if (node.kind === ts.SyntaxKind.Identifier && (<ts.Identifier>node).text === text) {
92108
return node;
93109
}
94110

95111
let foundNode: ts.Node | null = null;
96112
ts.forEachChild(node, childNode => {
97-
foundNode = foundNode || findNode(childNode, kind, text);
113+
foundNode = foundNode || findIdentifierNode(childNode, text);
98114
});
99115

100116
return foundNode;
101117
}
102118

103-
export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
104-
const nodes: ts.Node[] = [sourceFile];
105-
const result = [];
106-
107-
while (nodes.length > 0) {
108-
const node = nodes.shift();
109-
110-
if (node) {
111-
result.push(node);
112-
if (node.getChildCount(sourceFile) >= 0) {
113-
nodes.unshift(...node.getChildren());
114-
}
115-
}
116-
}
117-
118-
return result;
119-
}
120-
121-
122119
export function getObjectPropertyMatches(objectNode: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, targetPropertyName: string): ts.ObjectLiteralElement[] {
123120
return objectNode.properties
124121
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
125122
.filter((prop: ts.PropertyAssignment) => {
126123
const name = prop.name;
127124
switch (name.kind) {
128125
case ts.SyntaxKind.Identifier:
129-
return (name as ts.Identifier).getText(sourceFile) == targetPropertyName;
126+
return (name as ts.Identifier).text == targetPropertyName;
130127
case ts.SyntaxKind.StringLiteral:
131128
return (name as ts.StringLiteral).text == targetPropertyName;
132129
}
@@ -147,10 +144,9 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
147144
return acc;
148145
}, {});
149146

150-
return getSourceNodes(source)
147+
return collectDeepNodes(source, ts.SyntaxKind.Decorator)
151148
.filter(node => {
152-
return node.kind == ts.SyntaxKind.Decorator
153-
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
149+
return (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
154150
})
155151
.map(node => (node as ts.Decorator).expression as ts.CallExpression)
156152
.filter(expr => {
@@ -168,7 +164,7 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
168164
}
169165

170166
const id = paExpr.name.text;
171-
const moduleId = (paExpr.expression as ts.Identifier).getText(source);
167+
const moduleId = (paExpr.expression as ts.Identifier).text;
172168

173169
return id === identifier && (angularImports[moduleId + '.'] === module);
174170
}
@@ -228,3 +224,48 @@ export function angularImportsFromNode(node: ts.ImportDeclaration,
228224
return {};
229225
}
230226
}
227+
228+
export function getExpressionName(expression: ts.Expression): string {
229+
let text = "";
230+
if (!expression) {
231+
return text;
232+
}
233+
234+
if (expression.kind == ts.SyntaxKind.Identifier) {
235+
text = (<ts.Identifier>expression).text;
236+
} else if (expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
237+
text = (<ts.PropertyAccessExpression>expression).name.text;
238+
}
239+
240+
return text;
241+
}
242+
243+
function getExpressionImportPath(source: ts.SourceFile, expression: ts.Expression): string {
244+
let importString = "";
245+
if (!expression) {
246+
return undefined;
247+
}
248+
249+
if (expression.kind == ts.SyntaxKind.Identifier) {
250+
importString = (<ts.Identifier>expression).text;
251+
} else if (expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
252+
const targetPAArg = (<ts.PropertyAccessExpression>expression);
253+
if (targetPAArg.expression.kind == ts.SyntaxKind.Identifier) {
254+
importString = (<ts.Identifier>targetPAArg.expression).text;
255+
}
256+
}
257+
258+
const importPath = getImportPathInSource(source, importString);
259+
260+
return importPath;
261+
}
262+
263+
function getSourceFile(mainPath: string): ts.SourceFile {
264+
if (!existsSync(mainPath)) {
265+
throw new Error(`Main file (${mainPath}) not found`);
266+
}
267+
const mainText = readFileSync(mainPath, "utf8");
268+
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true);
269+
return source;
270+
}
271+

0 commit comments

Comments
 (0)
This repository has been archived.