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

feat: support HMR in Angular apps #788

Merged
merged 3 commits into from
Feb 6, 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
9 changes: 7 additions & 2 deletions templates/webpack.angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const nsWebpack = require("nativescript-dev-webpack");
const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target");
const { nsReplaceBootstrap } = require("nativescript-dev-webpack/transformers/ns-replace-bootstrap");
const { nsReplaceLazyLoader } = require("nativescript-dev-webpack/transformers/ns-replace-lazy-loader");
const { nsSupportHmrNg } = require("nativescript-dev-webpack/transformers/ns-support-hmr-ng");
const { getMainModulePath } = require("nativescript-dev-webpack/utils/ast-utils");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
Expand Down Expand Up @@ -59,10 +60,14 @@ module.exports = env => {
ngCompilerTransformers.push(nsReplaceBootstrap);
}

if (hmr) {
ngCompilerTransformers.push(nsSupportHmrNg);
}

// when "@angular/core" is external, it's not included in the bundles. In this way, it will be used
// directly from node_modules and the Angular modules loader won't be able to resolve the lazy routes
// fixes https://github.com/NativeScript/nativescript-cli/issues/4024
if (externals.indexOf("@angular/core") > -1) {
if (env.externals && env.externals.indexOf("@angular/core") > -1) {
const appModuleRelativePath = getMainModulePath(resolve(appFullPath, entryModule));
if (appModuleRelativePath) {
const appModuleFolderPath = dirname(resolve(appFullPath, appModuleRelativePath));
Expand All @@ -75,7 +80,7 @@ module.exports = env => {

const ngCompilerPlugin = new AngularCompilerPlugin({
hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]),
platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin)),
platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin, resolve(appFullPath, entryModule))),
mainPath: resolve(appPath, entryModule),
tsConfigPath: join(__dirname, "tsconfig.tns.json"),
skipCodeGeneration: !aot,
Expand Down
167 changes: 153 additions & 14 deletions templates/webpack.config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ proxyquire.noCallThru();

class EmptyClass { };

let angularCompilerOptions: any;
class AngularCompilerStub {
constructor(options) {
angularCompilerOptions = options;
}
};

const nativeScriptDevWebpack = {
GenerateBundleStarterPlugin: EmptyClass,
WatchStateLoggerPlugin: EmptyClass,
Expand All @@ -18,15 +25,18 @@ const nativeScriptDevWebpack = {
};

const emptyObject = {};

const FakeAotTransformerFlag = "aot";
const FakeHmrTransformerFlag = "hmr";
const FakeLazyTransformerFlag = "lazy";
const webpackConfigAngular = proxyquire('./webpack.angular', {
'nativescript-dev-webpack': nativeScriptDevWebpack,
'nativescript-dev-webpack/nativescript-target': emptyObject,
'nativescript-dev-webpack/transformers/ns-replace-bootstrap': emptyObject,
'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': emptyObject,
'nativescript-dev-webpack/utils/ast-utils': emptyObject,
'nativescript-dev-webpack/transformers/ns-replace-bootstrap': { nsReplaceBootstrap: () => { return FakeAotTransformerFlag } },
'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': { nsReplaceLazyLoader: () => { return FakeLazyTransformerFlag } },
'nativescript-dev-webpack/transformers/ns-support-hmr-ng': { nsSupportHmrNg: () => { return FakeHmrTransformerFlag } },
'nativescript-dev-webpack/utils/ast-utils': { getMainModulePath: () => { return "fakePath"; } },
'@ngtools/webpack': {
AngularCompilerPlugin: EmptyClass
AngularCompilerPlugin: AngularCompilerStub
}
});

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

describe('webpack.config.js', () => {
const getInput = (options: { platform: string, aot?: boolean, hmr?: boolean, externals?: string[] }) => {
const input: any = { aot: options.aot, hmr: options.hmr, externals: options.externals };
input[options.platform] = true;
return input;
};

[
{ type: 'javascript', webpackConfig: webpackConfigJavaScript },
{ type: 'typescript', webpackConfig: webpackConfigTypeScript },
Expand All @@ -57,12 +73,6 @@ describe('webpack.config.js', () => {
const { type, webpackConfig } = element;

describe(`verify externals for webpack.${type}.js`, () => {
const getInput = (platform: string, externals: string[]) => {
const input: any = { externals };
input[platform] = true;
return input;
};

[
'android',
'ios'
Expand All @@ -73,7 +83,7 @@ describe('webpack.config.js', () => {
});

it('returns empty array when externals are not passed', () => {
const config = webpackConfig(getInput(platform, null));
const config = webpackConfig(getInput({ platform }));
expect(config.externals).toEqual([]);
});

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

const input = getInput(platform, ['nativescript-vue']);
const input = getInput({ platform, externals: ['nativescript-vue'] });
webpackConfig(input);
expect(isCalled).toBe(true, 'Webpack.config.js must use the getConvertedExternals method');
});
Expand All @@ -99,7 +109,7 @@ describe('webpack.config.js', () => {
expectedOutput: [/^nativescript-vue((\/.*)|$)/, /^nativescript-angular((\/.*)|$)/]
},
].forEach(testCase => {
const input = getInput(platform, testCase.input);
const input = getInput({ platform, externals: testCase.input });

it(`are correct regular expressions, for input ${testCase.input}`, () => {
const config = webpackConfig(input);
Expand All @@ -110,4 +120,133 @@ describe('webpack.config.js', () => {
});
});
});

[
'android',
'ios'
].forEach(platform => {
describe(`angular transformers (${platform})`, () => {

beforeEach(() => {
angularCompilerOptions = null;
});

it("should be empty by default", () => {
const input = getInput({ platform });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(0);
});

it("should contain the AOT transformer when the AOT flag is passed", () => {
const input = getInput({ platform, aot: true });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
});

it("should contain the HMR transformer when the HMR flag is passed", () => {
const input = getInput({ platform, hmr: true });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeHmrTransformerFlag);
});

it("should contain the Lazy transformer when the @angular/core is an external module", () => {
const input = getInput({ platform, externals: ["@angular/core"] });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(1);
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeLazyTransformerFlag);
});

it("should contain the AOT + HMR transformers when the AOT and HMR flags are passed", () => {
const input = getInput({ platform, aot: true, hmr: true });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
});

it("should set the AOT transformer before the HMR one when the AOT and HMR flags are passed", () => {
const input = getInput({ platform, aot: true, hmr: true });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
expect(angularCompilerOptions.platformTransformers[1]).toEqual(FakeHmrTransformerFlag);
});

it("should contain the AOT + Lazy transformers when the AOT flag is passed and @angular/core is an external module", () => {
const input = getInput({ platform, aot: true, externals: ["@angular/core"] });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
});

it("should contain the HMR + Lazy transformers when the HMR flag is passed and @angular/core is an external module", () => {
const input = getInput({ platform, hmr: true, externals: ["@angular/core"] });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(2);
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
});

it("should contain the AOT + HMR + Lazy transformers when the AOT and HMR flags are passed and @angular/core is an external module", () => {
const input = getInput({ platform, aot: true, hmr: true, externals: ["@angular/core"] });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(3);
expect(angularCompilerOptions.platformTransformers).toContain(FakeAotTransformerFlag);
expect(angularCompilerOptions.platformTransformers).toContain(FakeHmrTransformerFlag);
expect(angularCompilerOptions.platformTransformers).toContain(FakeLazyTransformerFlag);
});

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", () => {
const input = getInput({ platform, aot: true, hmr: true, externals: ["@angular/core"] });

webpackConfigAngular(input);

expect(angularCompilerOptions).toBeDefined();
expect(angularCompilerOptions.platformTransformers).toBeDefined();
expect(angularCompilerOptions.platformTransformers.length).toEqual(3);
expect(angularCompilerOptions.platformTransformers[0]).toEqual(FakeAotTransformerFlag);
expect(angularCompilerOptions.platformTransformers[1]).toEqual(FakeHmrTransformerFlag);
expect(angularCompilerOptions.platformTransformers[2]).toEqual(FakeLazyTransformerFlag);
});
});
});
});
24 changes: 12 additions & 12 deletions transformers/ns-replace-bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ describe('@ngtools/webpack transformers', () => {
`;

const output = tags.stripIndent`
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";

__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
`;

const { program, compilerHost } = createTypescriptContext(input);
const ngCompiler = <AngularCompilerPlugin>{
typeChecker: program.getTypeChecker(),
entryModule: {
path: '/project/src/app/app.module',
path: '/project/src/app/app.module',
className: 'AppModule',
},
};
Expand All @@ -43,17 +43,17 @@ describe('@ngtools/webpack transformers', () => {
`;

const output = tags.stripIndent`
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";

__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
`;

const { program, compilerHost } = createTypescriptContext(input);
const ngCompiler = <AngularCompilerPlugin>{
typeChecker: program.getTypeChecker(),
entryModule: {
path: '/project/src/app/app.module',
path: '/project/src/app/app.module',
className: 'AppModule',
},
};
Expand All @@ -73,18 +73,18 @@ describe('@ngtools/webpack transformers', () => {
`;

const output = tags.stripIndent`
import * as __NgCli_bootstrap_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";
import * as __NgCli_bootstrap_2_1 from "./app/app.module.ngfactory";
import "./shared/kinvey.common";

__NgCli_bootstrap_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory);
__NgCli_bootstrap_1_1.platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2_1.AppModuleNgFactory);
`;

const { program, compilerHost } = createTypescriptContext(input);
const ngCompiler = <AngularCompilerPlugin>{
typeChecker: program.getTypeChecker(),
entryModule: {
path: '/project/src/app/app.module',
path: '/project/src/app/app.module',
className: 'AppModule',
},
};
Expand Down
Loading