From 5bafd277d63cf4c54ccf70b337bfb06aaaebe1bc Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Thu, 31 Jan 2019 17:16:55 +0200 Subject: [PATCH 1/3] feat: support HMR in Angular project by inserting the required code snippets using another AngularCompierPlugin transformer --- templates/webpack.angular.js | 7 +- templates/webpack.config.spec.ts | 1 + transformers/ns-replace-bootstrap.spec.ts | 24 +- transformers/ns-replace-bootstrap.ts | 59 ++--- transformers/ns-replace-lazy-loader.ts | 6 +- transformers/ns-support-hmr-ng.spec.ts | 284 ++++++++++++++++++++++ transformers/ns-support-hmr-ng.ts | 118 +++++++++ utils/ast-utils.ts | 177 ++++++++------ 8 files changed, 563 insertions(+), 113 deletions(-) create mode 100644 transformers/ns-support-hmr-ng.spec.ts create mode 100644 transformers/ns-support-hmr-ng.ts diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index 2d57bada..84be9174 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -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"); @@ -73,9 +74,13 @@ module.exports = env => { } } + if (hmr) { + ngCompilerTransformers.push(nsSupportHmrNg); + } + 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, diff --git a/templates/webpack.config.spec.ts b/templates/webpack.config.spec.ts index 4fd15434..b8083e7a 100644 --- a/templates/webpack.config.spec.ts +++ b/templates/webpack.config.spec.ts @@ -24,6 +24,7 @@ const webpackConfigAngular = proxyquire('./webpack.angular', { '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/transformers/ns-support-hmr-ng': emptyObject, 'nativescript-dev-webpack/utils/ast-utils': emptyObject, '@ngtools/webpack': { AngularCompilerPlugin: EmptyClass diff --git a/transformers/ns-replace-bootstrap.spec.ts b/transformers/ns-replace-bootstrap.spec.ts index 98d04795..0e7d3bf6 100644 --- a/transformers/ns-replace-bootstrap.spec.ts +++ b/transformers/ns-replace-bootstrap.spec.ts @@ -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 = { typeChecker: program.getTypeChecker(), entryModule: { - path: '/project/src/app/app.module', + path: '/project/src/app/app.module', className: 'AppModule', }, }; @@ -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 = { typeChecker: program.getTypeChecker(), entryModule: { - path: '/project/src/app/app.module', + path: '/project/src/app/app.module', className: 'AppModule', }, }; @@ -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 = { typeChecker: program.getTypeChecker(), entryModule: { - path: '/project/src/app/app.module', + path: '/project/src/app/app.module', className: 'AppModule', }, }; diff --git a/transformers/ns-replace-bootstrap.ts b/transformers/ns-replace-bootstrap.ts index 97930ee3..3ee06420 100644 --- a/transformers/ns-replace-bootstrap.ts +++ b/transformers/ns-replace-bootstrap.ts @@ -9,6 +9,9 @@ import { makeTransform, getFirstNode } from "@ngtools/webpack/src/transformers"; +import { + getExpressionName +} from "../utils/ast-utils"; import { AngularCompilerPlugin } from '@ngtools/webpack'; import { getResolvedEntryModule } from "../utils/transformers-utils"; @@ -47,39 +50,46 @@ export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin): return; } - const callExpr = entryModuleIdentifier.parent as ts.CallExpression; + const bootstrapCallExpr = entryModuleIdentifier.parent as ts.CallExpression; - if (callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { + if (bootstrapCallExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { return; } - const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression; + const bootstrapPropAccessExpr = bootstrapCallExpr.expression as ts.PropertyAccessExpression; - if (propAccessExpr.name.text !== 'bootstrapModule' - || propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) { + if (bootstrapPropAccessExpr.name.text !== 'bootstrapModule' + || bootstrapPropAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) { return; } - const bootstrapModuleIdentifier = propAccessExpr.name; - const innerCallExpr = propAccessExpr.expression as ts.CallExpression; - - if (!( - innerCallExpr.expression.kind === ts.SyntaxKind.Identifier - && (innerCallExpr.expression as ts.Identifier).text === 'platformNativeScriptDynamic' - )) { + const nsPlatformCallExpr = bootstrapPropAccessExpr.expression as ts.CallExpression; + if (!(getExpressionName(nsPlatformCallExpr.expression) === 'platformNativeScriptDynamic')) { return; } - const platformNativeScriptDynamicIdentifier = innerCallExpr.expression as ts.Identifier; - - const idPlatformNativeScript = ts.createUniqueName('__NgCli_bootstrap_'); - const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_'); + const idPlatformNativeScript = ts.createUniqueName('__NgCli_bootstrap_1'); + const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_2'); const firstNode = getFirstNode(sourceFile); // Add the transform operations. const factoryClassName = entryModule.className + 'NgFactory'; const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; + + + const newBootstrapPropAccessExpr = ts.getMutableClone(bootstrapPropAccessExpr); + const newNsPlatformCallExpr = ts.getMutableClone(bootstrapPropAccessExpr.expression) as ts.CallExpression; + newNsPlatformCallExpr.expression = ts.createPropertyAccess(idPlatformNativeScript, 'platformNativeScript'); + newBootstrapPropAccessExpr.expression = newNsPlatformCallExpr; + newBootstrapPropAccessExpr.name = ts.createIdentifier("bootstrapModuleFactory"); + + const newBootstrapCallExpr = ts.getMutableClone(bootstrapCallExpr); + newBootstrapCallExpr.expression = newBootstrapPropAccessExpr; + newBootstrapCallExpr.arguments = ts.createNodeArray([ + ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName)) + ]); + ops.push( // Insert an import of the {N} Angular static bootstrap module in the beginning of the file: // import * as __NgCli_bootstrap_2 from "nativescript-angular/platform-static"; @@ -101,19 +111,10 @@ export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin): true, ), - // Replace the NgModule nodes with NgModuleFactory nodes - // from 'AppModule' to 'AppModuleNgFactory' - new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, - ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))), - - // Replace 'platformNativeScriptDynamic' with 'platformNativeScript' - // and elide all imports of 'platformNativeScriptDynamic' - new ReplaceNodeOperation(sourceFile, platformNativeScriptDynamicIdentifier, - ts.createPropertyAccess(idPlatformNativeScript, 'platformNativeScript')), - - // Replace the invocation of 'boostrapModule' with 'bootsrapModuleFactory' - new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, - ts.createIdentifier('bootstrapModuleFactory')), + // Replace the bootstrap call expression. For example: + // from: platformNativeScriptDynamic().bootstrapModule(AppModule); + // to: platformNativeScript().bootstrapModuleFactory(__NgCli_bootstrap_2.AppModuleNgFactory); + new ReplaceNodeOperation(sourceFile, bootstrapCallExpr, newBootstrapCallExpr), ); }); diff --git a/transformers/ns-replace-lazy-loader.ts b/transformers/ns-replace-lazy-loader.ts index 324c9e9e..6b8e82bc 100644 --- a/transformers/ns-replace-lazy-loader.ts +++ b/transformers/ns-replace-lazy-loader.ts @@ -14,13 +14,13 @@ import { makeTransform } from "@ngtools/webpack/src/transformers"; import { AngularCompilerPlugin } from "@ngtools/webpack"; -import { findNode, getObjectPropertyMatches, getDecoratorMetadata } from "../utils/ast-utils"; +import { findIdentifierNode, getObjectPropertyMatches, getDecoratorMetadata } from "../utils/ast-utils"; import { getResolvedEntryModule } from "../utils/transformers-utils"; export function nsReplaceLazyLoader(getNgCompiler: () => AngularCompilerPlugin): ts.TransformerFactory { const getTypeChecker = () => getNgCompiler().typeChecker; - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) { let ops: TransformOperation[] = []; const entryModule = getResolvedEntryModule(getNgCompiler()); const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName))); @@ -63,7 +63,7 @@ export function addArrayPropertyValueToNgModule( const ngModuleConfigIndentifierNode = ngModuleConfigNode as ts.Identifier; // cases like @NgModule(myCoolConfig) const configObjectDeclarationNodes = collectDeepNodes(sourceFile, ts.SyntaxKind.VariableStatement).filter(imp => { - return findNode(imp, ts.SyntaxKind.Identifier, ngModuleConfigIndentifierNode.getText()); + return findIdentifierNode(imp, ngModuleConfigIndentifierNode.getText()); }); // will be undefined when the object is imported from another file const configObjectDeclaration = (configObjectDeclarationNodes && configObjectDeclarationNodes[0]); diff --git a/transformers/ns-support-hmr-ng.spec.ts b/transformers/ns-support-hmr-ng.spec.ts new file mode 100644 index 00000000..d64853f9 --- /dev/null +++ b/transformers/ns-support-hmr-ng.spec.ts @@ -0,0 +1,284 @@ +import { tags } from "@angular-devkit/core"; +import { createTypescriptContext, transformTypescript } from "@ngtools/webpack/src/transformers"; +import { nsReplaceBootstrap } from './ns-replace-bootstrap'; +import { nsSupportHmrNg, getHandleHmrOptionsCode, getAcceptMainModuleCode, GeneratedDynamicAppOptions } from "./ns-support-hmr-ng"; +import { AngularCompilerPlugin } from "@ngtools/webpack"; + +describe("@ngtools/webpack transformers", () => { + describe("ns-support-hmr-ng", () => { + const nsFactoryImportName = `__NgCli_bootstrap_2_1`; + const handleHmrPlatformDynamicImport = `import * as nativescript_angular_platform_Generated from "nativescript-angular/platform";`; + const handleHmrPlatformStaticImport = `import * as nativescript_angular_platform_Generated from "nativescript-angular/platform-static";`; + const handleAotPlatformStaticImport = `import * as __NgCli_bootstrap_1_1 from "nativescript-angular/platform-static";`; + const handleAotNgFactoryImport = `import * as ${nsFactoryImportName} from "./test-file.ts.ngfactory";`; + const handleHmrOptionsDeclaration = `var ${GeneratedDynamicAppOptions} = {};`; + const nsStaticPlatformCall = `nativescript_angular_platform_Generated.platformNativeScript`; + const nsDynamicPlatformCall = `nativescript_angular_platform_Generated.platformNativeScriptDynamic`; + const handleHmrOptionsCode = getHandleHmrOptionsCode("AppModule", "./app/app.module"); + const acceptMainModuleCode = getAcceptMainModuleCode("./app/app.module"); + const handleHmrOptionsAotCode = getHandleHmrOptionsCode("AppModuleNgFactory", "./test-file.ts.ngfactory"); + const acceptMainModuleAotCode = getAcceptMainModuleCode("./test-file.ts.ngfactory"); + const testCases = [{ + name: "should handle HMR when platformNativeScriptDynamic is called without arguments", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic().bootstrapModule(AppModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + }, + { + name: "should handle HMR when AOT is manually configured", + rawFile: ` + import { platformNativeScript } from "nativescript-angular/platform-static"; + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); + `, + transformedFile: ` + ${handleHmrPlatformStaticImport} + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} + ${getAcceptMainModuleCode("./app/app.module.ngfactory")} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} + ${getAcceptMainModuleCode("./app/app.module.ngfactory")} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); + ` + }, + { + name: "should handle HMR when platformNativeScriptDynamic is called without arguments and non default app module", + customAppModuleName: "CustomModule", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { CustomModule } from "./custom/custom.module"; + platformNativeScriptDynamic().bootstrapModule(CustomModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { CustomModule } from "./custom/custom.module"; + + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("CustomModule", "./custom/custom.module")} + ${getAcceptMainModuleCode("./custom/custom.module")} + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(CustomModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("CustomModuleNgFactory", "./test-file.ts.ngfactory")} + ${getAcceptMainModuleCode("./test-file.ts.ngfactory")} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.CustomModuleNgFactory); + ` + }, + { + name: "should handle HMR when platformNativeScriptDynamic is called from * import", + rawFile: ` + import * as nsNgPlatform from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + nsNgPlatform.platformNativeScriptDynamic().bootstrapModule(AppModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + }, + { + name: "should handle HMR when platformNativeScriptDynamic is called with appOptions", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic({ bootInExistingPage: true }).bootstrapModule(AppModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + }, + { + name: "should handle HMR when platformNativeScriptDynamic is called with multiple arguments", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic({ bootInExistingPage: true }, ["provider1", "provider2"]).bootstrapModule(AppModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModule(AppModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + }, + { + name: "should accept HMR before the user when custom handling is in place", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } + + platformNativeScriptDynamic().bootstrapModule(AppModule); + `, + transformedFile: ` + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, + transformedFileWithAot: ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + } + ]; + testCases.forEach((testCase: any) => { + it(`${testCase.name}`, async () => { + const testFile = "/project/src/test-file.ts"; + const input = tags.stripIndent`${testCase.rawFile}`; + const output = tags.stripIndent`${testCase.transformedFile}`; + const { program, compilerHost } = createTypescriptContext(input); + const ngCompiler = { + typeChecker: program.getTypeChecker(), + entryModule: { + path: testFile, + className: "AppModule", + }, + }; + const transformer = nsSupportHmrNg(() => ngCompiler, testFile); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it(`${testCase.name} (in combination with AOT transformer)`, async () => { + const testFile = "/project/src/test-file.ts"; + const input = tags.stripIndent`${testCase.rawFile}`; + const output = tags.stripIndent`${testCase.transformedFileWithAot}`; + const { program, compilerHost } = createTypescriptContext(input); + const ngCompiler = { + typeChecker: program.getTypeChecker(), + entryModule: { + path: testFile, + className: testCase.customAppModuleName || "AppModule", + }, + }; + + const aotTransformer = nsReplaceBootstrap(() => ngCompiler); + const hmrTransformer = nsSupportHmrNg(() => ngCompiler, testFile); + const result = transformTypescript(undefined, [aotTransformer, hmrTransformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); + }); +}); diff --git a/transformers/ns-support-hmr-ng.ts b/transformers/ns-support-hmr-ng.ts new file mode 100644 index 00000000..0a41eac2 --- /dev/null +++ b/transformers/ns-support-hmr-ng.ts @@ -0,0 +1,118 @@ +import { normalize } from "path"; +import * as ts from "typescript"; +import { + AddNodeOperation, + ReplaceNodeOperation, + StandardTransform, + TransformOperation, + collectDeepNodes, + makeTransform, + insertStarImport +} from "@ngtools/webpack/src/transformers"; +import { AngularCompilerPlugin } from "@ngtools/webpack"; +import { + findBootstrappedModulePathInSource, + findNativeScriptPlatformPathInSource, + findBootstrapModuleCallInSource, + findNativeScriptPlatformCallInSource, + getExpressionName +} from "../utils/ast-utils"; + +export function nsSupportHmrNg(getNgCompiler: () => AngularCompilerPlugin, entryPath: string): ts.TransformerFactory { + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + let ops: TransformOperation[] = []; + + if (!entryPath || normalize(sourceFile.fileName) !== normalize(entryPath)) { + return ops; + } + + try { + ops = handleHmrSupport(sourceFile); + } catch (e) { + ops = []; + } + + return ops; + }; + + return makeTransform(standardTransform, () => getNgCompiler().typeChecker); +} + +export function handleHmrSupport( + mainFile: ts.SourceFile +): TransformOperation[] { + const importNodesInFile = collectDeepNodes(mainFile, ts.SyntaxKind.ImportDeclaration); + if (!importNodesInFile || !importNodesInFile.length) { + return []; + } + + const bootstrapModuleCallNode = findBootstrapModuleCallInSource(mainFile); + if (!bootstrapModuleCallNode || !bootstrapModuleCallNode.arguments || !bootstrapModuleCallNode.arguments.length) { + return []; + } + + const appModuleName = getExpressionName(bootstrapModuleCallNode.arguments[0]); + const nativeScriptPlatformCallNode = findNativeScriptPlatformCallInSource(mainFile); + if (!nativeScriptPlatformCallNode || !nativeScriptPlatformCallNode.arguments) { + return []; + } + + return handleHmrSupportCore(mainFile, importNodesInFile, appModuleName, nativeScriptPlatformCallNode); +} + +function handleHmrSupportCore(mainFile: ts.SourceFile, importNodesInFile: ts.Node[], appModuleName: string, nativeScriptPlatformCallNode: ts.CallExpression) { + const firstImportNode = importNodesInFile[0]; + const lastImportNode = importNodesInFile[importNodesInFile.length - 1]; + const appModulePath = findBootstrappedModulePathInSource(mainFile); + let currentAppOptionsInitializationNode: ts.Expression = ts.createObjectLiteral(); + if (nativeScriptPlatformCallNode.arguments.length > 0) { + currentAppOptionsInitializationNode = nativeScriptPlatformCallNode.arguments[0]; + } + + const optionsDeclaration = ts.createVariableDeclaration(GeneratedDynamicAppOptions, undefined, currentAppOptionsInitializationNode); + const optionsDeclarationList = ts.createVariableDeclarationList([optionsDeclaration]); + const optionsStatement = ts.createVariableStatement(undefined, optionsDeclarationList); + + const handleHmrOptionsNode = ts.createIdentifier(getHandleHmrOptionsCode(appModuleName, appModulePath)); + + const acceptHmrNode = ts.createIdentifier(getAcceptMainModuleCode(appModulePath)); + + const newNsDynamicCallArgs = ts.createNodeArray([ts.createIdentifier(GeneratedDynamicAppOptions), ...nativeScriptPlatformCallNode.arguments.slice(1)]); + const nsPlatformPath = findNativeScriptPlatformPathInSource(mainFile); + const nsPlatformText = getExpressionName(nativeScriptPlatformCallNode.expression); + const newNsDynamicCallNode = ts.createCall(ts.createPropertyAccess(ts.createIdentifier(NsNgPlatformStarImport), ts.createIdentifier(nsPlatformText)), [], newNsDynamicCallArgs); + + return [ + ...insertStarImport(mainFile, ts.createIdentifier(NsNgPlatformStarImport), nsPlatformPath, firstImportNode, true), + new AddNodeOperation(mainFile, lastImportNode, undefined, optionsStatement), + new AddNodeOperation(mainFile, lastImportNode, undefined, handleHmrOptionsNode), + new AddNodeOperation(mainFile, lastImportNode, undefined, acceptHmrNode), + new ReplaceNodeOperation(mainFile, nativeScriptPlatformCallNode, newNsDynamicCallNode) + ]; +} + +export const GeneratedDynamicAppOptions = "options_Generated"; +const NsNgPlatformStarImport = "nativescript_angular_platform_Generated"; + +export function getHandleHmrOptionsCode(appModuleName: string, appModulePath: string) { + return ` +if (module["hot"]) { + ${GeneratedDynamicAppOptions} = Object.assign(${GeneratedDynamicAppOptions}, { + hmrOptions: { + moduleTypeFactory: function () { return require("${appModulePath}").${appModuleName}; }, + livesyncCallback: function (platformReboot) { setTimeout(platformReboot, 0); } + } + }); +} +` +} + +export function getAcceptMainModuleCode(mainModulePath: string) { + return ` +if (module["hot"]) { + module["hot"].accept(["${mainModulePath}"], function () { + global["hmrRefresh"]({}); + }); +} +`; +} \ No newline at end of file diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index bb4ae6a1..a98c53de 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -3,6 +3,17 @@ // https://github.com/angular/angular-cli/blob/d202480a1707be6575b2c8cf0383cfe6db44413c/packages/schematics/angular/utility/ng-ast-utils.ts // https://github.com/NativeScript/nativescript-schematics/blob/438b9e3ef613389980bfa9d071e28ca1f32ab04f/src/ast-utils.ts +// important notes: +// 1) DO NOT USE `null` when building nodes or you will get `Cannot read property 'transformFlags' of null` +// https://github.com/Microsoft/TypeScript/issues/22372#issuecomment-371221056 +// 2) DO NOT USE `node.getText()` or `node.getFullText()` while analyzing the AST - it is trying to read +// the text from the source file and if the node is affected by another transformer, it will lead to +// an unexpected behavior. You can use `identifier.text` instead. +// 3) DO NOT USE `node.parent` while analyzing the AST. It will be null when the node is replaced by +// another transformer and will lead to an exception. Take a look at `findMethodCallInSource` for an +// example of a working workaround by searching for content in each parent. +// 4) Always test your transformer both single and in combinations with the other ones. + import { dirname, join } from "path"; import * as ts from "typescript"; import { readFileSync, existsSync } from "fs"; @@ -10,115 +21,101 @@ import { collectDeepNodes } from "@ngtools/webpack/src/transformers"; export function getMainModulePath(entryFilePath) { try { - return findBootstrapModulePath(entryFilePath); + return findBootstrappedModulePath(entryFilePath); } catch (e) { return null; } } export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | null { - if (!existsSync(mainPath)) { - throw new Error(`Main file (${mainPath}) not found`); - } - const mainText = readFileSync(mainPath, "utf8"); - - const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); - - const allNodes = getSourceNodes(source); + const source = getSourceFile(mainPath); - let bootstrapCall: ts.CallExpression | null = null; - - for (const node of allNodes) { - - let bootstrapCallNode: ts.Node | null = null; - bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, "bootstrapModule"); + return findBootstrapModuleCallInSource(source); +} - // Walk up the parent until CallExpression is found. - while (bootstrapCallNode && bootstrapCallNode.parent - && bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression) { +export function findBootstrapModuleCallInSource(source: ts.SourceFile): ts.CallExpression | null { + return findMethodCallInSource(source, "bootstrapModule") || findMethodCallInSource(source, "bootstrapModuleFactory"); +} +export function findNativeScriptPlatformCallInSource(source: ts.SourceFile): ts.CallExpression | null { + return findMethodCallInSource(source, "platformNativeScriptDynamic") || findMethodCallInSource(source, "platformNativeScript"); +} - bootstrapCallNode = bootstrapCallNode.parent; - } +export function findMethodCallInSource(source: ts.SourceFile, methodName: string): ts.CallExpression | null { + const allMethodCalls = collectDeepNodes(source, ts.SyntaxKind.CallExpression); + let methodCallNode: ts.CallExpression | null = null; - if (bootstrapCallNode !== null && - bootstrapCallNode.parent !== undefined && - bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression) { - bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; - break; + for (const callNode of allMethodCalls) { + const currentMethodName = getExpressionName(callNode.expression); + if (methodName === currentMethodName) { + methodCallNode = callNode; } } - return bootstrapCall; + return methodCallNode; } -export function findBootstrapModulePath(mainPath: string): string { - const bootstrapCall = findBootstrapModuleCall(mainPath); +export function findBootstrappedModulePath(mainPath: string): string { + const source = getSourceFile(mainPath); + + return findBootstrappedModulePathInSource(source); +} + +export function findBootstrappedModulePathInSource(source: ts.SourceFile): string { + const bootstrapCall = findBootstrapModuleCallInSource(source); if (!bootstrapCall) { throw new Error("Bootstrap call not found"); } - const bootstrapModule = bootstrapCall.arguments[0]; - if (!existsSync(mainPath)) { - throw new Error(`Main file (${mainPath}) not found`); + const appModulePath = getExpressionImportPath(source, bootstrapCall.arguments[0]); + + return appModulePath; +} + +export function findNativeScriptPlatformPathInSource(source: ts.SourceFile): string { + const nsPlatformCall = findNativeScriptPlatformCallInSource(source); + if (!nsPlatformCall) { + throw new Error("NativeScriptPlatform call not found"); } - const mainText = readFileSync(mainPath, "utf8"); - const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); - const allNodes = getSourceNodes(source); - const bootstrapModuleRelativePath = allNodes - .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) + const nsPlatformImportPath = getExpressionImportPath(source, nsPlatformCall.expression); + + return nsPlatformImportPath; +} + +function getImportPathInSource(source: ts.SourceFile, importName: string) { + const allImports = collectDeepNodes(source, ts.SyntaxKind.ImportDeclaration); + const importPath = allImports .filter(imp => { - return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); + return findIdentifierNode(imp, importName); }) .map((imp: ts.ImportDeclaration) => { const modulePathStringLiteral = imp.moduleSpecifier as ts.StringLiteral; - return modulePathStringLiteral.text; })[0]; - - return bootstrapModuleRelativePath; + return importPath; } export function getAppModulePath(mainPath: string): string { - const moduleRelativePath = findBootstrapModulePath(mainPath); + const moduleRelativePath = findBootstrappedModulePath(mainPath); const mainDir = dirname(mainPath); const modulePath = join(mainDir, `${moduleRelativePath}.ts`); return modulePath; } -export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null { - if (node.kind === kind && node.getText() === text) { +export function findIdentifierNode(node: ts.Node, text: string): ts.Node | null { + if (node.kind === ts.SyntaxKind.Identifier && (node).text === text) { return node; } let foundNode: ts.Node | null = null; ts.forEachChild(node, childNode => { - foundNode = foundNode || findNode(childNode, kind, text); + foundNode = foundNode || findIdentifierNode(childNode, text); }); return foundNode; } -export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { - const nodes: ts.Node[] = [sourceFile]; - const result = []; - - while (nodes.length > 0) { - const node = nodes.shift(); - - if (node) { - result.push(node); - if (node.getChildCount(sourceFile) >= 0) { - nodes.unshift(...node.getChildren()); - } - } - } - - return result; -} - - export function getObjectPropertyMatches(objectNode: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, targetPropertyName: string): ts.ObjectLiteralElement[] { return objectNode.properties .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) @@ -126,7 +123,7 @@ export function getObjectPropertyMatches(objectNode: ts.ObjectLiteralExpression, const name = prop.name; switch (name.kind) { case ts.SyntaxKind.Identifier: - return (name as ts.Identifier).getText(sourceFile) == targetPropertyName; + return (name as ts.Identifier).text == targetPropertyName; case ts.SyntaxKind.StringLiteral: return (name as ts.StringLiteral).text == targetPropertyName; } @@ -147,10 +144,9 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, return acc; }, {}); - return getSourceNodes(source) + return collectDeepNodes(source, ts.SyntaxKind.Decorator) .filter(node => { - return node.kind == ts.SyntaxKind.Decorator - && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; + return (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; }) .map(node => (node as ts.Decorator).expression as ts.CallExpression) .filter(expr => { @@ -168,7 +164,7 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, } const id = paExpr.name.text; - const moduleId = (paExpr.expression as ts.Identifier).getText(source); + const moduleId = (paExpr.expression as ts.Identifier).text; return id === identifier && (angularImports[moduleId + '.'] === module); } @@ -228,3 +224,48 @@ export function angularImportsFromNode(node: ts.ImportDeclaration, return {}; } } + +export function getExpressionName(expression: ts.Expression): string { + let text = ""; + if (!expression) { + return text; + } + + if (expression.kind == ts.SyntaxKind.Identifier) { + text = (expression).text; + } else if (expression.kind == ts.SyntaxKind.PropertyAccessExpression) { + text = (expression).name.text; + } + + return text; +} + +function getExpressionImportPath(source: ts.SourceFile, expression: ts.Expression): string { + let importString = ""; + if (!expression) { + return undefined; + } + + if (expression.kind == ts.SyntaxKind.Identifier) { + importString = (expression).text; + } else if (expression.kind == ts.SyntaxKind.PropertyAccessExpression) { + const targetPAArg = (expression); + if (targetPAArg.expression.kind == ts.SyntaxKind.Identifier) { + importString = (targetPAArg.expression).text; + } + } + + const importPath = getImportPathInSource(source, importString); + + return importPath; +} + +function getSourceFile(mainPath: string): ts.SourceFile { + if (!existsSync(mainPath)) { + throw new Error(`Main file (${mainPath}) not found`); + } + const mainText = readFileSync(mainPath, "utf8"); + const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); + return source; +} + From 4ebf8ab99d47cf5a988eec171b0c4a94c725f6c6 Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Tue, 5 Feb 2019 17:21:11 +0200 Subject: [PATCH 2/3] fix: fixed the Lazy transformer condition and add unit tests for all transformer conditions based on the input env options --- templates/webpack.angular.js | 10 +- templates/webpack.config.spec.ts | 168 ++++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 20 deletions(-) diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index 84be9174..e8de5173 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -60,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)); @@ -74,10 +78,6 @@ module.exports = env => { } } - if (hmr) { - ngCompilerTransformers.push(nsSupportHmrNg); - } - const ngCompilerPlugin = new AngularCompilerPlugin({ hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]), platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin, resolve(appFullPath, entryModule))), diff --git a/templates/webpack.config.spec.ts b/templates/webpack.config.spec.ts index b8083e7a..74c6f41e 100644 --- a/templates/webpack.config.spec.ts +++ b/templates/webpack.config.spec.ts @@ -7,6 +7,13 @@ proxyquire.noCallThru(); class EmptyClass { }; +let angularCompilerOptions: any; +class AngularCompilerStub { + constructor(options) { + angularCompilerOptions = options; + } +}; + const nativeScriptDevWebpack = { GenerateBundleStarterPlugin: EmptyClass, WatchStateLoggerPlugin: EmptyClass, @@ -18,16 +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/transformers/ns-support-hmr-ng': 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 } }); @@ -49,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 }, @@ -58,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' @@ -74,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([]); }); @@ -85,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'); }); @@ -100,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); @@ -111,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); + }); + }); + }); }); From fc2f86bae45009abae31ccc9c9ecdcfffb04d81b Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Tue, 5 Feb 2019 19:02:51 +0200 Subject: [PATCH 3/3] chore(ng-hmr-transformer): handle invalid main.ts content and add unit tests --- transformers/ns-support-hmr-ng.spec.ts | 359 ++++++++++++++----------- transformers/ns-support-hmr-ng.ts | 4 + 2 files changed, 209 insertions(+), 154 deletions(-) diff --git a/transformers/ns-support-hmr-ng.spec.ts b/transformers/ns-support-hmr-ng.spec.ts index d64853f9..545a1c0e 100644 --- a/transformers/ns-support-hmr-ng.spec.ts +++ b/transformers/ns-support-hmr-ng.spec.ts @@ -21,224 +21,275 @@ describe("@ngtools/webpack transformers", () => { const testCases = [{ name: "should handle HMR when platformNativeScriptDynamic is called without arguments", rawFile: ` - import { platformNativeScriptDynamic } from "nativescript-angular/platform"; - import { AppModule } from "./app/app.module"; - platformNativeScriptDynamic().bootstrapModule(AppModule); - `, + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic().bootstrapModule(AppModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { AppModule } from "./app/app.module"; + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsCode} - ${acceptMainModuleCode} + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); - `, + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsAotCode} - ${acceptMainModuleAotCode} + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` + }, + { + name: "should not handle HMR when the AppModule import cannot be found", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + platformNativeScriptDynamic().bootstrapModule(SyntaxErrorModule); + `, + transformedFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + platformNativeScriptDynamic().bootstrapModule(SyntaxErrorModule); + `, + transformedFileWithAot: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + platformNativeScriptDynamic().bootstrapModule(SyntaxErrorModule); + ` + }, + { + name: "(known limitation) should not handle HMR when the platformNativeScriptDynamic method is renamed", + rawFile: ` + import { platformNativeScriptDynamic as x } from "nativescript-angular/platform"; + x().bootstrapModule(SyntaxErrorModule); + `, + transformedFile: ` + import { platformNativeScriptDynamic as x } from "nativescript-angular/platform"; + x().bootstrapModule(SyntaxErrorModule); + `, + transformedFileWithAot: ` + import { platformNativeScriptDynamic as x } from "nativescript-angular/platform"; + x().bootstrapModule(SyntaxErrorModule); + ` + }, + { + name: "(known limitation) should not handle HMR when the bootstrapModule method is renamed", + rawFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + + const x = platformNativeScriptDynamic().bootstrapModule; + x(SyntaxErrorModule); + `, + transformedFile: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + + const x = platformNativeScriptDynamic().bootstrapModule; + x(SyntaxErrorModule); + `, + transformedFileWithAot: ` + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + + const x = platformNativeScriptDynamic().bootstrapModule; + x(SyntaxErrorModule); + ` }, { name: "should handle HMR when AOT is manually configured", rawFile: ` - import { platformNativeScript } from "nativescript-angular/platform-static"; - import { AppModuleNgFactory } from "./app/app.module.ngfactory"; - platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); - `, + import { platformNativeScript } from "nativescript-angular/platform-static"; + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); + `, transformedFile: ` - ${handleHmrPlatformStaticImport} - import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + ${handleHmrPlatformStaticImport} + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; - ${handleHmrOptionsDeclaration} - ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} - ${getAcceptMainModuleCode("./app/app.module.ngfactory")} + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} + ${getAcceptMainModuleCode("./app/app.module.ngfactory")} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); - `, + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - import { AppModuleNgFactory } from "./app/app.module.ngfactory"; + ${handleHmrPlatformStaticImport} + import { AppModuleNgFactory } from "./app/app.module.ngfactory"; - ${handleHmrOptionsDeclaration} - ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} - ${getAcceptMainModuleCode("./app/app.module.ngfactory")} + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("AppModuleNgFactory", "./app/app.module.ngfactory")} + ${getAcceptMainModuleCode("./app/app.module.ngfactory")} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(AppModuleNgFactory); + ` }, { name: "should handle HMR when platformNativeScriptDynamic is called without arguments and non default app module", customAppModuleName: "CustomModule", rawFile: ` - import { platformNativeScriptDynamic } from "nativescript-angular/platform"; - import { CustomModule } from "./custom/custom.module"; - platformNativeScriptDynamic().bootstrapModule(CustomModule); - `, + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { CustomModule } from "./custom/custom.module"; + platformNativeScriptDynamic().bootstrapModule(CustomModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { CustomModule } from "./custom/custom.module"; + ${handleHmrPlatformDynamicImport} + import { CustomModule } from "./custom/custom.module"; - ${handleHmrOptionsDeclaration} - ${getHandleHmrOptionsCode("CustomModule", "./custom/custom.module")} - ${getAcceptMainModuleCode("./custom/custom.module")} + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("CustomModule", "./custom/custom.module")} + ${getAcceptMainModuleCode("./custom/custom.module")} - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(CustomModule); - `, + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(CustomModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} - ${handleHmrOptionsDeclaration} - ${getHandleHmrOptionsCode("CustomModuleNgFactory", "./test-file.ts.ngfactory")} - ${getAcceptMainModuleCode("./test-file.ts.ngfactory")} + ${handleHmrOptionsDeclaration} + ${getHandleHmrOptionsCode("CustomModuleNgFactory", "./test-file.ts.ngfactory")} + ${getAcceptMainModuleCode("./test-file.ts.ngfactory")} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.CustomModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.CustomModuleNgFactory); + ` }, { name: "should handle HMR when platformNativeScriptDynamic is called from * import", rawFile: ` - import * as nsNgPlatform from "nativescript-angular/platform"; - import { AppModule } from "./app/app.module"; - nsNgPlatform.platformNativeScriptDynamic().bootstrapModule(AppModule); - `, + import * as nsNgPlatform from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + nsNgPlatform.platformNativeScriptDynamic().bootstrapModule(AppModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { AppModule } from "./app/app.module"; + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsCode} - ${acceptMainModuleCode} + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); - `, + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsAotCode} - ${acceptMainModuleAotCode} + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` }, { - name: "should handle HMR when platformNativeScriptDynamic is called with appOptions", + name: "should handle HMR when platformNativeScriptDynamic is called with inline appOptions", rawFile: ` - import { platformNativeScriptDynamic } from "nativescript-angular/platform"; - import { AppModule } from "./app/app.module"; - platformNativeScriptDynamic({ bootInExistingPage: true }).bootstrapModule(AppModule); - `, + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic({ bootInExistingPage: true }).bootstrapModule(AppModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { AppModule } from "./app/app.module"; + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; - var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; - ${handleHmrOptionsCode} - ${acceptMainModuleCode} + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsCode} + ${acceptMainModuleCode} - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); - `, + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} - var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; - ${handleHmrOptionsAotCode} - ${acceptMainModuleAotCode} + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` }, { name: "should handle HMR when platformNativeScriptDynamic is called with multiple arguments", rawFile: ` - import { platformNativeScriptDynamic } from "nativescript-angular/platform"; - import { AppModule } from "./app/app.module"; - platformNativeScriptDynamic({ bootInExistingPage: true }, ["provider1", "provider2"]).bootstrapModule(AppModule); - `, + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; + platformNativeScriptDynamic({ bootInExistingPage: true }, ["provider1", "provider2"]).bootstrapModule(AppModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { AppModule } from "./app/app.module"; + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; - var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; - ${handleHmrOptionsCode} - ${acceptMainModuleCode} + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsCode} + ${acceptMainModuleCode} - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModule(AppModule); - `, + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModule(AppModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} - var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; - ${handleHmrOptionsAotCode} - ${acceptMainModuleAotCode} + var ${GeneratedDynamicAppOptions} = { bootInExistingPage: true }; + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); - ` + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}, ["provider1", "provider2"]).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` }, { name: "should accept HMR before the user when custom handling is in place", rawFile: ` - import { platformNativeScriptDynamic } from "nativescript-angular/platform"; - import { AppModule } from "./app/app.module"; + import { platformNativeScriptDynamic } from "nativescript-angular/platform"; + import { AppModule } from "./app/app.module"; - if (module["hot"]) { - module["hot"].accept(["./app/app.module"], function () { - // customHandling - }); - } + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } - platformNativeScriptDynamic().bootstrapModule(AppModule); - `, + platformNativeScriptDynamic().bootstrapModule(AppModule); + `, transformedFile: ` - ${handleHmrPlatformDynamicImport} - import { AppModule } from "./app/app.module"; - - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsCode} - ${acceptMainModuleCode} - - if (module["hot"]) { - module["hot"].accept(["./app/app.module"], function () { - // customHandling - }); - } - - ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); - `, + ${handleHmrPlatformDynamicImport} + import { AppModule } from "./app/app.module"; + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsCode} + ${acceptMainModuleCode} + + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } + + ${nsDynamicPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModule(AppModule); + `, transformedFileWithAot: ` - ${handleHmrPlatformStaticImport} - ${handleAotPlatformStaticImport} - ${handleAotNgFactoryImport} - - ${handleHmrOptionsDeclaration} - ${handleHmrOptionsAotCode} - ${acceptMainModuleAotCode} - - if (module["hot"]) { - module["hot"].accept(["./app/app.module"], function () { - // customHandling - }); - } - - ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); - ` + ${handleHmrPlatformStaticImport} + ${handleAotPlatformStaticImport} + ${handleAotNgFactoryImport} + + ${handleHmrOptionsDeclaration} + ${handleHmrOptionsAotCode} + ${acceptMainModuleAotCode} + + if (module["hot"]) { + module["hot"].accept(["./app/app.module"], function () { + // customHandling + }); + } + + ${nsStaticPlatformCall}(${GeneratedDynamicAppOptions}).bootstrapModuleFactory(${nsFactoryImportName}.AppModuleNgFactory); + ` } ]; testCases.forEach((testCase: any) => { diff --git a/transformers/ns-support-hmr-ng.ts b/transformers/ns-support-hmr-ng.ts index 0a41eac2..06cade50 100644 --- a/transformers/ns-support-hmr-ng.ts +++ b/transformers/ns-support-hmr-ng.ts @@ -64,6 +64,10 @@ function handleHmrSupportCore(mainFile: ts.SourceFile, importNodesInFile: ts.Nod const firstImportNode = importNodesInFile[0]; const lastImportNode = importNodesInFile[importNodesInFile.length - 1]; const appModulePath = findBootstrappedModulePathInSource(mainFile); + if (!appModuleName || !appModulePath) { + return []; + } + let currentAppOptionsInitializationNode: ts.Expression = ts.createObjectLiteral(); if (nativeScriptPlatformCallNode.arguments.length > 0) { currentAppOptionsInitializationNode = nativeScriptPlatformCallNode.arguments[0];