Skip to content
This repository was archived by the owner on Aug 7, 2021. It is now read-only.

Commit 6a9ce33

Browse files
author
Dimitar Tachev
authored
feat: support HMR in Angular apps (#788)
* feat: support HMR in Angular project by inserting the required code snippets using another AngularCompierPlugin transformer * fix: fixed the Lazy transformer condition and add unit tests for all transformer conditions based on the input env options * chore(ng-hmr-transformer): handle invalid main.ts content and add unit tests
1 parent cc82df3 commit 6a9ce33

8 files changed

+771
-128
lines changed

Diff for: templates/webpack.angular.js

+7-2
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");
@@ -59,10 +60,14 @@ module.exports = env => {
5960
ngCompilerTransformers.push(nsReplaceBootstrap);
6061
}
6162

63+
if (hmr) {
64+
ngCompilerTransformers.push(nsSupportHmrNg);
65+
}
66+
6267
// when "@angular/core" is external, it's not included in the bundles. In this way, it will be used
6368
// directly from node_modules and the Angular modules loader won't be able to resolve the lazy routes
6469
// fixes https://github.com/NativeScript/nativescript-cli/issues/4024
65-
if (externals.indexOf("@angular/core") > -1) {
70+
if (env.externals && env.externals.indexOf("@angular/core") > -1) {
6671
const appModuleRelativePath = getMainModulePath(resolve(appFullPath, entryModule));
6772
if (appModuleRelativePath) {
6873
const appModuleFolderPath = dirname(resolve(appFullPath, appModuleRelativePath));
@@ -75,7 +80,7 @@ module.exports = env => {
7580

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,

Diff for: templates/webpack.config.spec.ts

+153-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ proxyquire.noCallThru();
77

88
class EmptyClass { };
99

10+
let angularCompilerOptions: any;
11+
class AngularCompilerStub {
12+
constructor(options) {
13+
angularCompilerOptions = options;
14+
}
15+
};
16+
1017
const nativeScriptDevWebpack = {
1118
GenerateBundleStarterPlugin: EmptyClass,
1219
WatchStateLoggerPlugin: EmptyClass,
@@ -18,15 +25,18 @@ const nativeScriptDevWebpack = {
1825
};
1926

2027
const emptyObject = {};
21-
28+
const FakeAotTransformerFlag = "aot";
29+
const FakeHmrTransformerFlag = "hmr";
30+
const FakeLazyTransformerFlag = "lazy";
2231
const webpackConfigAngular = proxyquire('./webpack.angular', {
2332
'nativescript-dev-webpack': nativeScriptDevWebpack,
2433
'nativescript-dev-webpack/nativescript-target': emptyObject,
25-
'nativescript-dev-webpack/transformers/ns-replace-bootstrap': emptyObject,
26-
'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': emptyObject,
27-
'nativescript-dev-webpack/utils/ast-utils': emptyObject,
34+
'nativescript-dev-webpack/transformers/ns-replace-bootstrap': { nsReplaceBootstrap: () => { return FakeAotTransformerFlag } },
35+
'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': { nsReplaceLazyLoader: () => { return FakeLazyTransformerFlag } },
36+
'nativescript-dev-webpack/transformers/ns-support-hmr-ng': { nsSupportHmrNg: () => { return FakeHmrTransformerFlag } },
37+
'nativescript-dev-webpack/utils/ast-utils': { getMainModulePath: () => { return "fakePath"; } },
2838
'@ngtools/webpack': {
29-
AngularCompilerPlugin: EmptyClass
39+
AngularCompilerPlugin: AngularCompilerStub
3040
}
3141
});
3242

@@ -48,6 +58,12 @@ const webpackConfigVue = proxyquire('./webpack.vue', {
4858
});
4959

5060
describe('webpack.config.js', () => {
61+
const getInput = (options: { platform: string, aot?: boolean, hmr?: boolean, externals?: string[] }) => {
62+
const input: any = { aot: options.aot, hmr: options.hmr, externals: options.externals };
63+
input[options.platform] = true;
64+
return input;
65+
};
66+
5167
[
5268
{ type: 'javascript', webpackConfig: webpackConfigJavaScript },
5369
{ type: 'typescript', webpackConfig: webpackConfigTypeScript },
@@ -57,12 +73,6 @@ describe('webpack.config.js', () => {
5773
const { type, webpackConfig } = element;
5874

5975
describe(`verify externals for webpack.${type}.js`, () => {
60-
const getInput = (platform: string, externals: string[]) => {
61-
const input: any = { externals };
62-
input[platform] = true;
63-
return input;
64-
};
65-
6676
[
6777
'android',
6878
'ios'
@@ -73,7 +83,7 @@ describe('webpack.config.js', () => {
7383
});
7484

7585
it('returns empty array when externals are not passed', () => {
76-
const config = webpackConfig(getInput(platform, null));
86+
const config = webpackConfig(getInput({ platform }));
7787
expect(config.externals).toEqual([]);
7888
});
7989

@@ -84,7 +94,7 @@ describe('webpack.config.js', () => {
8494
return [];
8595
};
8696

87-
const input = getInput(platform, ['nativescript-vue']);
97+
const input = getInput({ platform, externals: ['nativescript-vue'] });
8898
webpackConfig(input);
8999
expect(isCalled).toBe(true, 'Webpack.config.js must use the getConvertedExternals method');
90100
});
@@ -99,7 +109,7 @@ describe('webpack.config.js', () => {
99109
expectedOutput: [/^nativescript-vue((\/.*)|$)/, /^nativescript-angular((\/.*)|$)/]
100110
},
101111
].forEach(testCase => {
102-
const input = getInput(platform, testCase.input);
112+
const input = getInput({ platform, externals: testCase.input });
103113

104114
it(`are correct regular expressions, for input ${testCase.input}`, () => {
105115
const config = webpackConfig(input);
@@ -110,4 +120,133 @@ describe('webpack.config.js', () => {
110120
});
111121
});
112122
});
123+
124+
[
125+
'android',
126+
'ios'
127+
].forEach(platform => {
128+
describe(`angular transformers (${platform})`, () => {
129+
130+
beforeEach(() => {
131+
angularCompilerOptions = null;
132+
});
133+
134+
it("should be empty by default", () => {
135+
const input = getInput({ platform });
136+
137+
webpackConfigAngular(input);
138+
139+
expect(angularCompilerOptions).toBeDefined();
140+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
141+
expect(angularCompilerOptions.platformTransformers.length).toEqual(0);
142+
});
143+
144+
it("should contain the AOT transformer when the AOT flag is passed", () => {
145+
const input = getInput({ platform, aot: true });
146+
147+
webpackConfigAngular(input);
148+
149+
expect(angularCompilerOptions).toBeDefined();
150+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
151+
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
152+
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
153+
});
154+
155+
it("should contain the HMR transformer when the HMR flag is passed", () => {
156+
const input = getInput({ platform, hmr: true });
157+
158+
webpackConfigAngular(input);
159+
160+
expect(angularCompilerOptions).toBeDefined();
161+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
162+
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
163+
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeHmrTransformerFlag);
164+
});
165+
166+
it("should contain the Lazy transformer when the @angular/core is an external module", () => {
167+
const input = getInput({ platform, externals: ["@angular/core"] });
168+
169+
webpackConfigAngular(input);
170+
171+
expect(angularCompilerOptions).toBeDefined();
172+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
173+
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
174+
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeLazyTransformerFlag);
175+
});
176+
177+
it("should contain the AOT + HMR transformers when the AOT and HMR flags are passed", () => {
178+
const input = getInput({ platform, aot: true, hmr: true });
179+
180+
webpackConfigAngular(input);
181+
182+
expect(angularCompilerOptions).toBeDefined();
183+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
184+
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
185+
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
186+
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
187+
});
188+
189+
it("should set the AOT transformer before the HMR one when the AOT and HMR flags are passed", () => {
190+
const input = getInput({ platform, aot: true, hmr: true });
191+
192+
webpackConfigAngular(input);
193+
194+
expect(angularCompilerOptions).toBeDefined();
195+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
196+
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
197+
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
198+
expect(angularCompilerOptions.platformTransformers[1]).toEqual(FakeHmrTransformerFlag);
199+
});
200+
201+
it("should contain the AOT + Lazy transformers when the AOT flag is passed and @angular/core is an external module", () => {
202+
const input = getInput({ platform, aot: true, externals: ["@angular/core"] });
203+
204+
webpackConfigAngular(input);
205+
206+
expect(angularCompilerOptions).toBeDefined();
207+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
208+
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
209+
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
210+
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
211+
});
212+
213+
it("should contain the HMR + Lazy transformers when the HMR flag is passed and @angular/core is an external module", () => {
214+
const input = getInput({ platform, hmr: true, externals: ["@angular/core"] });
215+
216+
webpackConfigAngular(input);
217+
218+
expect(angularCompilerOptions).toBeDefined();
219+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
220+
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
221+
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
222+
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
223+
});
224+
225+
it("should contain the AOT + HMR + Lazy transformers when the AOT and HMR flags are passed and @angular/core is an external module", () => {
226+
const input = getInput({ platform, aot: true, hmr: true, externals: ["@angular/core"] });
227+
228+
webpackConfigAngular(input);
229+
230+
expect(angularCompilerOptions).toBeDefined();
231+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
232+
expect(angularCompilerOptions.platformTransformers.length).toEqual(3);
233+
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
234+
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
235+
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
236+
});
237+
238+
it("should contain the AOT + HMR + Lazy transformers in the proper order when the AOT and HMR flags are passed and @angular/core is an external module", () => {
239+
const input = getInput({ platform, aot: true, hmr: true, externals: ["@angular/core"] });
240+
241+
webpackConfigAngular(input);
242+
243+
expect(angularCompilerOptions).toBeDefined();
244+
expect(angularCompilerOptions.platformTransformers).toBeDefined();
245+
expect(angularCompilerOptions.platformTransformers.length).toEqual(3);
246+
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
247+
expect(angularCompilerOptions.platformTransformers[1]).toEqual(FakeHmrTransformerFlag);
248+
expect(angularCompilerOptions.platformTransformers[2]).toEqual(FakeLazyTransformerFlag);
249+
});
250+
});
251+
});
113252
});

Diff for: transformers/ns-replace-bootstrap.spec.ts

+12-12
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
};

0 commit comments

Comments
 (0)