From 80fee54f923456aaffd067594c701eb04ed507bf Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Fri, 23 Nov 2018 16:41:09 +0200 Subject: [PATCH 1/5] feat: transform the main angular module in order to include the lazy loader in the bundle when angular/core is external --- .gitignore | 10 +- index.js | 30 +- templates/webpack.angular.js | 25 +- transformers/ns-replace-bootstrap.d.ts | 3 - transformers/ns-replace-lazy-loader.spec.ts | 228 +++++++++++++++ transformers/ns-replace-lazy-loader.ts | 301 ++++++++++++++++++++ utils/ast-utils.ts | 127 +++++++++ 7 files changed, 704 insertions(+), 20 deletions(-) delete mode 100644 transformers/ns-replace-bootstrap.d.ts create mode 100644 transformers/ns-replace-lazy-loader.spec.ts create mode 100644 transformers/ns-replace-lazy-loader.ts create mode 100644 utils/ast-utils.ts diff --git a/.gitignore b/.gitignore index 4ab4a5ea..cee7d177 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,13 @@ plugins/NativeScriptAngularCompilerPlugin.d.ts plugins/NativeScriptAngularCompilerPlugin.js plugins/NativeScriptAngularCompilerPlugin.js.map -transformers/ns-replace-bootstrap.d.ts -transformers/ns-replace-bootstrap.js -transformers/ns-replace-bootstrap.js.map +transformers/*.d.ts +transformers/*.js +transformers/*.js.map + +utils/*.d.ts +utils/*.js +utils/*.js.map plugins/PlatformFSPlugin.d.ts plugins/PlatformFSPlugin.js diff --git a/index.js b/index.js index 307236ae..966eee41 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const path = require("path"); const { existsSync } = require("fs"); - +const { findBootstrapModulePath } = require("./utils/ast-utils") const { ANDROID_APP_PATH } = require("./androidProjectHelpers"); const { getPackageJson, @@ -12,22 +12,22 @@ Object.assign(exports, require("./plugins")); Object.assign(exports, require("./host/resolver")); exports.getAotEntryModule = function (appDirectory) { - verifyEntryModuleDirectory(appDirectory); - + verifyEntryModuleDirectory(appDirectory); + const entry = getPackageJsonEntry(appDirectory); const aotEntry = `${entry}.aot.ts`; const aotEntryPath = path.resolve(appDirectory, aotEntry); if (!existsSync(aotEntryPath)) { throw new Error(`For ahead-of-time compilation you need to have an entry module ` + - `at ${aotEntryPath} that bootstraps the app with a static platform instead of dynamic one!`) + `at ${aotEntryPath} that bootstraps the app with a static platform instead of dynamic one!`) } return aotEntry; } exports.getEntryModule = function (appDirectory) { - verifyEntryModuleDirectory(appDirectory); + verifyEntryModuleDirectory(appDirectory); const entry = getPackageJsonEntry(appDirectory); @@ -35,12 +35,20 @@ exports.getEntryModule = function (appDirectory) { const jsEntryPath = path.resolve(appDirectory, `${entry}.js`); if (!existsSync(tsEntryPath) && !existsSync(jsEntryPath)) { throw new Error(`The entry module ${entry} specified in ` + - `${appDirectory}/package.json doesn't exist!`) + `${appDirectory}/package.json doesn't exist!`) } return entry; }; +exports.getMainModulePath = function (entryFilePath) { + try { + return findBootstrapModulePath(entryFilePath); + } catch (e) { + return null; + } +} + exports.getAppPath = (platform, projectDir) => { if (isIos(platform)) { const appName = path.basename(projectDir); @@ -72,10 +80,10 @@ function getPackageJsonEntry(appDirectory) { function verifyEntryModuleDirectory(appDirectory) { if (!appDirectory) { - throw new Error("Path to app directory is not specified. Unable to find entry module."); - } + throw new Error("Path to app directory is not specified. Unable to find entry module."); + } - if (!existsSync(appDirectory)) { - throw new Error(`The specified path to app directory ${appDirectory} does not exist. Unable to find entry module.`); - } + if (!existsSync(appDirectory)) { + throw new Error(`The specified path to app directory ${appDirectory} does not exist. Unable to find entry module.`); + } } diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index a4486c5b..dd0a43d5 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -1,9 +1,10 @@ -const { join, relative, resolve, sep } = require("path"); +const { join, relative, resolve, sep, dirname } = require("path"); const webpack = require("webpack"); 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 CleanWebpackPlugin = require("clean-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); @@ -44,7 +45,8 @@ module.exports = env => { sourceMap, // --env.sourceMap hmr, // --env.hmr, } = env; - const externals = (env.externals || []).map((e) => { // --env.externals + env.externals = env.externals || []; + const externals = (env.externals).map((e) => { // --env.externals return new RegExp(e + ".*"); }); @@ -53,14 +55,31 @@ module.exports = env => { const entryModule = `${nsWebpack.getEntryModule(appFullPath)}.ts`; const entryPath = `.${sep}${entryModule}`; + const ngCompilerTransformers = []; + const additionalLazyModuleResources = []; + if (aot) { + ngCompilerTransformers.push(nsReplaceBootstrap); + } + + if (env.externals.indexOf("@angular/core") > -1) { + const appModuleRelativePath = nsWebpack.getMainModulePath(resolve(appFullPath, entryModule)); + if (appModuleRelativePath) { + const appModuleFolderPath = dirname(resolve(appFullPath, appModuleRelativePath)); + // include the lazy loader inside app module + ngCompilerTransformers.push(nsReplaceLazyLoader); + // include the new lazy loader path in the allowed ones + additionalLazyModuleResources.push(appModuleFolderPath); + } + } const ngCompilerPlugin = new AngularCompilerPlugin({ hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]), - platformTransformers: aot ? [nsReplaceBootstrap(() => ngCompilerPlugin)] : null, + platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin)), mainPath: resolve(appPath, entryModule), tsConfigPath: join(__dirname, "tsconfig.tns.json"), skipCodeGeneration: !aot, sourceMap: !!sourceMap, + additionalLazyModuleResources: additionalLazyModuleResources }); const config = { diff --git a/transformers/ns-replace-bootstrap.d.ts b/transformers/ns-replace-bootstrap.d.ts deleted file mode 100644 index 59d35d45..00000000 --- a/transformers/ns-replace-bootstrap.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as ts from 'typescript'; -import { AngularCompilerPlugin } from '@ngtools/webpack'; -export declare function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin): ts.TransformerFactory; diff --git a/transformers/ns-replace-lazy-loader.spec.ts b/transformers/ns-replace-lazy-loader.spec.ts new file mode 100644 index 00000000..5724baaf --- /dev/null +++ b/transformers/ns-replace-lazy-loader.spec.ts @@ -0,0 +1,228 @@ +import { tags } from '@angular-devkit/core'; +import { createTypescriptContext, transformTypescript } from "@ngtools/webpack/src/transformers"; +import { nsReplaceLazyLoader, NgLazyLoaderCode, getConfigObjectSetupCode } from './ns-replace-lazy-loader'; +import { AngularCompilerPlugin } from '@ngtools/webpack'; + +describe('@ngtools/webpack transformers', () => { + describe('ns-replace-lazy-loader', () => { + const configObjectName = "testIdentifier"; + const configObjectSetupCode = getConfigObjectSetupCode(configObjectName, "providers", "NgModuleFactoryLoader", "{ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }"); + const testCases = [ + { + name: "should add providers and NgModuleFactoryLoader when providers is missing", + rawAppModule: ` + import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + + @NgModule({ + bootstrap: [ + AppComponent + ], + imports: [ + NativeScriptModule + ], + declarations: [ + AppComponent, + ] + }) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + ${NgLazyLoaderCode} + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule({ + bootstrap: [ AppComponent ], + imports: [ NativeScriptModule ], + declarations: [ AppComponent, ], + providers: [{ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }] }) + ], + AppModule); + export { AppModule };` + }, + { + name: "should add NgModuleFactoryLoader when the providers array is empty", + rawAppModule: ` + import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + + @NgModule({ + bootstrap: [ + AppComponent + ], + imports: [ + NativeScriptModule + ], + declarations: [ + AppComponent, + ], + providers: [] + }) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + ${NgLazyLoaderCode} + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule({ + bootstrap: [ AppComponent ], + imports: [ NativeScriptModule ], + declarations: [ AppComponent, ], + providers: [{ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }] }) + ], + AppModule); + export { AppModule };` + }, + { + name: "should add NgModuleFactoryLoader at the end when the providers array is containing other providers", + rawAppModule: ` + import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + @NgModule({ + bootstrap: [ + AppComponent + ], + imports: [ + NativeScriptModule + ], + declarations: [ + AppComponent, + ], + providers: [MyCoolProvider] + }) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + ${NgLazyLoaderCode} + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule({ + bootstrap: [ AppComponent ], + imports: [ NativeScriptModule ], + declarations: [ AppComponent, ], + providers: [MyCoolProvider, { provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }] }) + ], + AppModule); + export { AppModule };` + }, + { + name: "should NOT add NgModuleFactoryLoader when its already defined", + rawAppModule: ` + import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + + @NgModule({ + bootstrap: [ + AppComponent + ], + imports: [ + NativeScriptModule + ], + declarations: [ + AppComponent, + ], + providers: [{ provide: NgModuleFactoryLoader, useClass: CustomLoader }] + }) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; import { NgModule } from "@angular/core"; + import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + import { AppComponent } from "./app.component"; + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule({ + bootstrap: [ AppComponent ], + imports: [ NativeScriptModule ], + declarations: [ AppComponent, ], + providers: [{ provide: NgModuleFactoryLoader, useClass: CustomLoader }] }) + ], + AppModule); + export { AppModule };` + }, + { + name: "should setup the object when an object is passed to the NgModule", + rawAppModule: ` + import { NgModule } from "@angular/core"; + import { ${configObjectName} } from "somewhere"; + + @NgModule(${configObjectName}) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; + import { NgModule } from "@angular/core"; + import { ${configObjectName} } from "somewhere"; + + ${NgLazyLoaderCode} + ${configObjectSetupCode} + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule(${configObjectName}) ], AppModule); + + export { AppModule }; + ` + }, + { + name: "should setup the object after its initialization when a local object is passed to the NgModule", + rawAppModule: ` + import { NgModule } from "@angular/core"; + const ${configObjectName} = { + bootstrap: [ + AppComponent + ], + declarations: [ + AppComponent + ] + }; + + @NgModule(${configObjectName}) + export class AppModule { } + `, + transformedAppModule: ` + import * as tslib_1 from "tslib"; + import { NgModule } from "@angular/core"; + ${NgLazyLoaderCode} + const ${configObjectName} = { + bootstrap: [ + AppComponent + ], + declarations: [ + AppComponent + ] + }; + ${configObjectSetupCode} + let AppModule = class AppModule { }; + AppModule = tslib_1.__decorate([ NgModule(${configObjectName}) ], AppModule); + export { AppModule }; + ` + } + ]; + testCases.forEach((testCase: any) => { + it(`${testCase.name}`, async () => { + const input = tags.stripIndent`${testCase.rawAppModule}`; + const output = tags.stripIndent`${testCase.transformedAppModule}`; + const { program, compilerHost } = createTypescriptContext(input); + const ngCompiler = { + typeChecker: program.getTypeChecker(), + entryModule: { + path: '/project/src/test-file', + className: 'AppModule', + }, + }; + const transformer = nsReplaceLazyLoader(() => ngCompiler); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); + }); +}); diff --git a/transformers/ns-replace-lazy-loader.ts b/transformers/ns-replace-lazy-loader.ts new file mode 100644 index 00000000..2ac09f67 --- /dev/null +++ b/transformers/ns-replace-lazy-loader.ts @@ -0,0 +1,301 @@ +// inspired by: +// https://github.com/angular/angular-cli/blob/d202480a1707be6575b2c8cf0383cfe6db44413c/packages/schematics/angular/utility/ast-utils.ts +// 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 + +import { dirname, basename, extname, join } from 'path'; +import * as ts from 'typescript'; +import { + StandardTransform, + TransformOperation, + collectDeepNodes, + AddNodeOperation, + ReplaceNodeOperation, + makeTransform +} from "@ngtools/webpack/src/transformers"; +import { workaroundResolve } from '@ngtools/webpack/src/compiler_host'; +import { AngularCompilerPlugin } from '@ngtools/webpack'; +import { findNode, getSourceNodes, getObjectPropertyMatches } from "../utils/ast-utils"; + +export function nsReplaceLazyLoader(getNgCompiler: () => AngularCompilerPlugin): ts.TransformerFactory { + const getTypeChecker = () => getNgCompiler().typeChecker; + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + let ops: TransformOperation[] = []; + const ngCompiler = getNgCompiler(); + + const entryModule = ngCompiler.entryModule + ? { path: workaroundResolve(ngCompiler.entryModule.path), className: getNgCompiler().entryModule.className } + : ngCompiler.entryModule; + const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName))); + if (!entryModule || sourceFilePath !== entryModule.path) { + return ops; + } + + try { + ops = addArrayPropertyValueToNgModule(sourceFile, "providers", "NgModuleFactoryLoader", "{ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }") || []; + } catch (e) { + ops = []; + } + + return ops; + }; + + return makeTransform(standardTransform, getTypeChecker); +} + +export function addArrayPropertyValueToNgModule( + sourceFile: ts.SourceFile, + targetPropertyName: string, + newPropertyValueMatch: string, + newPropertyValue: string +): TransformOperation[] { + const ngModuleConfigNodesInFile = getDecoratorMetadata(sourceFile, 'NgModule', '@angular/core'); + let ngModuleConfigNode: any = ngModuleConfigNodesInFile && ngModuleConfigNodesInFile[0]; + if (!ngModuleConfigNode) { + return null; + } + + const importsInFile = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration); + const lastImport = importsInFile && importsInFile[importsInFile.length - 1]; + if (!lastImport) { + return null; + } + + const ngLazyLoaderNode = ts.createIdentifier(NgLazyLoaderCode); + if (ngModuleConfigNode.kind == ts.SyntaxKind.Identifier) { + // cases like @NgModule(myCoolConfig) + const configObjectDeclarationNodes = collectDeepNodes(sourceFile, ts.SyntaxKind.VariableStatement).filter(imp => { + return findNode(imp, ts.SyntaxKind.Identifier, ngModuleConfigNode.getText()); + }); + // will be undefined when the object is imported from another file + const configObjectDeclaration = (configObjectDeclarationNodes && configObjectDeclarationNodes[0]); + + const configObjectName = ngModuleConfigNode.escapedText.trim(); + const configObjectSetupCode = getConfigObjectSetupCode(configObjectName, targetPropertyName, newPropertyValueMatch, newPropertyValue); + const configObjectSetupNode = ts.createIdentifier(configObjectSetupCode); + + return [ + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode), + new AddNodeOperation(sourceFile, configObjectDeclaration || lastImport, undefined, configObjectSetupNode) + ]; + } else if (ngModuleConfigNode.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // cases like @NgModule({ bootstrap: ... }) + const ngModuleConfigObjectNode = ngModuleConfigNode as ts.ObjectLiteralExpression; + const matchingProperties: ts.ObjectLiteralElement[] = getObjectPropertyMatches(ngModuleConfigObjectNode, sourceFile, targetPropertyName); + if (!matchingProperties) { + // invalid object + return null; + } + + if (matchingProperties.length == 0) { + if (ngModuleConfigObjectNode.properties.length == 0) { + // empty object @NgModule({ }) + return null; + } + + // the target field is missing, we will insert it @NgModule({ otherProps }) + const lastConfigObjPropertyNode = ngModuleConfigObjectNode.properties[ngModuleConfigObjectNode.properties.length - 1]; + const newTargetPropertyNode = ts.createIdentifier(`${targetPropertyName}: [${newPropertyValue}]`); + + return [ + new AddNodeOperation(sourceFile, lastConfigObjPropertyNode, undefined, newTargetPropertyNode), + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; + + } + + // the target property is found + const targetPropertyNode = matchingProperties[0] as ts.PropertyAssignment; + if (targetPropertyNode.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + // not an array + return null; + } + + const targetPropertyValuesNode = targetPropertyNode.initializer as ts.ArrayLiteralExpression; + const targetPropertyValues = targetPropertyValuesNode.elements; + if (targetPropertyValues.length > 0) { + // @NgModule({ targetProperty: [ someValues ] }) + const targetPropertyValuesStrings = targetPropertyValues.map(node => node.getText()); + const wholeWordPropValueRegex = new RegExp("\\b" + newPropertyValueMatch + "\\b"); + if (targetPropertyValuesStrings.some(((value) => wholeWordPropValueRegex.test(value)))) { + // already registered + return null; + } + + const lastPropertyValueNode = targetPropertyValues[targetPropertyValues.length - 1]; + const newPropertyValueNode = ts.createIdentifier(`${newPropertyValue}`); + + return [new AddNodeOperation(sourceFile, lastPropertyValueNode, undefined, newPropertyValueNode), + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; + } else { + // empty array @NgModule({ targetProperty: [ ] }) + const newTargetPropertyValuesNode = ts.createIdentifier(`[${newPropertyValue}]`); + + return [new ReplaceNodeOperation(sourceFile, targetPropertyValuesNode, newTargetPropertyValuesNode), + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; + } + } +} + +function getDecoratorMetadata(source: ts.SourceFile, identifier: string, + module: string): ts.Node[] { + const angularImports: { [name: string]: string } + = collectDeepNodes(source, ts.SyntaxKind.ImportDeclaration) + .map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source)) + .reduce((acc: { [name: string]: string }, current: { [name: string]: string }) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, {}); + + return getSourceNodes(source) + .filter(node => { + return node.kind == ts.SyntaxKind.Decorator + && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return id.getFullText(source) == identifier + && angularImports[id.getFullText(source)] === module; + } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && (angularImports[moduleId + '.'] === module); + } + + return false; + }) + .filter(expr => expr.arguments[0] + && (expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression || + expr.arguments[0].kind == ts.SyntaxKind.Identifier)) + .map(expr => expr.arguments[0] as ts.Node); +} + +function _angularImportsFromNode(node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getConfigObjectSetupCode(configObjectName: string, targetPropertyName: string, newPropertyValueMatch: string, newPropertyValue: string) { + return ` +if (!${configObjectName}.${targetPropertyName}) { + ${configObjectName}.${targetPropertyName} = []; +} +if (Array.isArray(${configObjectName}.${targetPropertyName})) { + var wholeWordPropertyRegex = new RegExp("\\b${newPropertyValueMatch}\\b"); + if (!${configObjectName}.${targetPropertyName}.some(function (property) { return wholeWordPropertyRegex.test(property); })) { + ${configObjectName}.${targetPropertyName}.push(${newPropertyValue}); + } +} +`; +} + +// based on: https://github.com/angular/angular/blob/4c2ce4e8ba4c5ac5ce8754d67bc6603eaad4564a/packages/core/src/linker/system_js_ng_module_factory_loader.ts +// when @angular/core is an external module, this fixes https://github.com/NativeScript/nativescript-cli/issues/4024 by including the lazy loader INSIDE the bundle allowing it to access the lazy modules +export const NgLazyLoaderCode = ` +var nsNgCoreImport_Generated = require("@angular/core"); +var NSLazyModulesLoader_Generated = /** @class */ (function () { + function NSLazyModulesLoader_Generated(_compiler, config) { + this._compiler = _compiler; + this._config = config || { + factoryPathPrefix: '', + factoryPathSuffix: '.ngfactory', + }; + } + NSLazyModulesLoader_Generated.prototype.load = function (path) { + var offlineMode = this._compiler instanceof nsNgCoreImport_Generated.Compiler; + return offlineMode ? this.loadFactory(path) : this.loadAndCompile(path); + }; + NSLazyModulesLoader_Generated.prototype.loadAndCompile = function (path) { + var _this = this; + var _a = path.split('#'), module = _a[0], exportName = _a[1]; + if (exportName === undefined) { + exportName = 'default'; + } + return import(module) + .then(function (module) { return module[exportName]; }) + .then(function (type) { return _this.checkNotEmpty(type, module, exportName); }) + .then(function (type) { return _this._compiler.compileModuleAsync(type); }); + }; + NSLazyModulesLoader_Generated.prototype.loadFactory = function (path) { + var _this = this; + var _a = path.split('#'), module = _a[0], exportName = _a[1]; + var factoryClassSuffix = 'NgFactory'; + if (exportName === undefined) { + exportName = 'default'; + factoryClassSuffix = ''; + } + return import(this._config.factoryPathPrefix + module + this._config.factoryPathSuffix) + .then(function (module) { return module[exportName + factoryClassSuffix]; }) + .then(function (factory) { return _this.checkNotEmpty(factory, module, exportName); }); + }; + NSLazyModulesLoader_Generated.prototype.checkNotEmpty = function (value, modulePath, exportName) { + if (!value) { + throw new Error("Cannot find '" + exportName + "' in '" + modulePath + "'"); + } + return value; + }; + NSLazyModulesLoader_Generated = __decorate([ + nsNgCoreImport_Generated.Injectable(), + __param(1, nsNgCoreImport_Generated.Optional()), + __metadata("design:paramtypes", [nsNgCoreImport_Generated.Compiler, nsNgCoreImport_Generated.SystemJsNgModuleLoaderConfig]) + ], NSLazyModulesLoader_Generated); + return NSLazyModulesLoader_Generated; +}()); +`; \ No newline at end of file diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts new file mode 100644 index 00000000..d2a77522 --- /dev/null +++ b/utils/ast-utils.ts @@ -0,0 +1,127 @@ +// inspired by: +// https://github.com/angular/angular-cli/blob/d202480a1707be6575b2c8cf0383cfe6db44413c/packages/schematics/angular/utility/ast-utils.ts +// 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 + +import { dirname, join } from 'path'; +import * as ts from 'typescript'; +const { readFileSync, existsSync } = require("fs"); + +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); + + let bootstrapCall: ts.CallExpression | null = null; + + for (const node of allNodes) { + + let bootstrapCallNode: ts.Node | null = null; + bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, 'bootstrapModule'); + + // Walk up the parent until CallExpression is found. + while (bootstrapCallNode && bootstrapCallNode.parent + && bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression) { + + bootstrapCallNode = bootstrapCallNode.parent; + } + + if (bootstrapCallNode !== null && + bootstrapCallNode.parent !== undefined && + bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression) { + bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; + break; + } + } + + return bootstrapCall; +} + +export function findBootstrapModulePath(mainPath: string): string { + const bootstrapCall = findBootstrapModuleCall(mainPath); + 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 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) + .filter(imp => { + return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); + }) + .map((imp: ts.ImportDeclaration) => { + const modulePathStringLiteral = imp.moduleSpecifier as ts.StringLiteral; + + return modulePathStringLiteral.text; + })[0]; + + return bootstrapModuleRelativePath; +} + +export function getAppModulePath(mainPath: string): string { + const moduleRelativePath = findBootstrapModulePath(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) { + // throw new Error(node.getText()); + return node; + } + + let foundNode: ts.Node | null = null; + ts.forEachChild(node, childNode => { + foundNode = foundNode || findNode(childNode, kind, 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) + .filter((prop: ts.PropertyAssignment) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(sourceFile) == targetPropertyName; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == targetPropertyName; + } + return false; + }); +} \ No newline at end of file From c15e737c22fbb176fe521d67008f15b47ffd5060 Mon Sep 17 00:00:00 2001 From: Stanimira Vlaeva Date: Thu, 20 Dec 2018 13:16:26 +0200 Subject: [PATCH 2/5] fix pr comments --- index.js | 2 +- templates/webpack.angular.js | 3 + transformers/ns-replace-bootstrap.ts | 9 +- transformers/ns-replace-lazy-loader.spec.ts | 16 +- transformers/ns-replace-lazy-loader.ts | 155 +++++--------------- utils/ast-utils.ts | 111 +++++++++++++- utils/transformers-utils.ts | 8 + 7 files changed, 164 insertions(+), 140 deletions(-) create mode 100644 utils/transformers-utils.ts diff --git a/index.js b/index.js index 966eee41..9d7a8422 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const path = require("path"); const { existsSync } = require("fs"); -const { findBootstrapModulePath } = require("./utils/ast-utils") +const { findBootstrapModulePath } = require("./utils/ast-utils"); const { ANDROID_APP_PATH } = require("./androidProjectHelpers"); const { getPackageJson, diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index dd0a43d5..f389ecd4 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -61,6 +61,9 @@ module.exports = env => { ngCompilerTransformers.push(nsReplaceBootstrap); } + // 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 (env.externals.indexOf("@angular/core") > -1) { const appModuleRelativePath = nsWebpack.getMainModulePath(resolve(appFullPath, entryModule)); if (appModuleRelativePath) { diff --git a/transformers/ns-replace-bootstrap.ts b/transformers/ns-replace-bootstrap.ts index 26462da1..97930ee3 100644 --- a/transformers/ns-replace-bootstrap.ts +++ b/transformers/ns-replace-bootstrap.ts @@ -9,8 +9,9 @@ import { makeTransform, getFirstNode } from "@ngtools/webpack/src/transformers"; -import { workaroundResolve } from '@ngtools/webpack/src/compiler_host'; import { AngularCompilerPlugin } from '@ngtools/webpack'; +import { getResolvedEntryModule } from "../utils/transformers-utils"; + export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin): ts.TransformerFactory { const shouldTransform = (fileName) => !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); @@ -18,11 +19,7 @@ export function nsReplaceBootstrap(getNgCompiler: () => AngularCompilerPlugin): const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { const ops: TransformOperation[] = []; - const ngCompiler = getNgCompiler(); - - const entryModule = ngCompiler.entryModule - ? { path: workaroundResolve(ngCompiler.entryModule.path), className: getNgCompiler().entryModule.className } - : ngCompiler.entryModule; + const entryModule = getResolvedEntryModule(getNgCompiler()); if (!shouldTransform(sourceFile.fileName) || !entryModule) { return ops; diff --git a/transformers/ns-replace-lazy-loader.spec.ts b/transformers/ns-replace-lazy-loader.spec.ts index 5724baaf..590889da 100644 --- a/transformers/ns-replace-lazy-loader.spec.ts +++ b/transformers/ns-replace-lazy-loader.spec.ts @@ -1,10 +1,10 @@ -import { tags } from '@angular-devkit/core'; +import { tags } from "@angular-devkit/core"; import { createTypescriptContext, transformTypescript } from "@ngtools/webpack/src/transformers"; -import { nsReplaceLazyLoader, NgLazyLoaderCode, getConfigObjectSetupCode } from './ns-replace-lazy-loader'; -import { AngularCompilerPlugin } from '@ngtools/webpack'; +import { nsReplaceLazyLoader, NgLazyLoaderCode, getConfigObjectSetupCode } from "./ns-replace-lazy-loader"; +import { AngularCompilerPlugin } from "@ngtools/webpack"; -describe('@ngtools/webpack transformers', () => { - describe('ns-replace-lazy-loader', () => { +describe("@ngtools/webpack transformers", () => { + describe("ns-replace-lazy-loader", () => { const configObjectName = "testIdentifier"; const configObjectSetupCode = getConfigObjectSetupCode(configObjectName, "providers", "NgModuleFactoryLoader", "{ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }"); const testCases = [ @@ -115,7 +115,7 @@ describe('@ngtools/webpack transformers', () => { export { AppModule };` }, { - name: "should NOT add NgModuleFactoryLoader when its already defined", + name: "should NOT add NgModuleFactoryLoader when it's already defined", rawAppModule: ` import { NgModule } from "@angular/core"; import { NativeScriptModule } from "nativescript-angular/nativescript.module"; @@ -214,8 +214,8 @@ describe('@ngtools/webpack transformers', () => { const ngCompiler = { typeChecker: program.getTypeChecker(), entryModule: { - path: '/project/src/test-file', - className: 'AppModule', + path: "/project/src/test-file", + className: "AppModule", }, }; const transformer = nsReplaceLazyLoader(() => ngCompiler); diff --git a/transformers/ns-replace-lazy-loader.ts b/transformers/ns-replace-lazy-loader.ts index 2ac09f67..9a17e04d 100644 --- a/transformers/ns-replace-lazy-loader.ts +++ b/transformers/ns-replace-lazy-loader.ts @@ -3,8 +3,8 @@ // 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 -import { dirname, basename, extname, join } from 'path'; -import * as ts from 'typescript'; +import { dirname, basename, extname, join } from "path"; +import * as ts from "typescript"; import { StandardTransform, TransformOperation, @@ -13,20 +13,16 @@ import { ReplaceNodeOperation, makeTransform } from "@ngtools/webpack/src/transformers"; -import { workaroundResolve } from '@ngtools/webpack/src/compiler_host'; -import { AngularCompilerPlugin } from '@ngtools/webpack'; -import { findNode, getSourceNodes, getObjectPropertyMatches } from "../utils/ast-utils"; +import { AngularCompilerPlugin } from "@ngtools/webpack"; +import { findNode, 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) { let ops: TransformOperation[] = []; - const ngCompiler = getNgCompiler(); - - const entryModule = ngCompiler.entryModule - ? { path: workaroundResolve(ngCompiler.entryModule.path), className: getNgCompiler().entryModule.className } - : ngCompiler.entryModule; + const entryModule = getResolvedEntryModule(getNgCompiler()); const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName))); if (!entryModule || sourceFilePath !== entryModule.path) { return ops; @@ -50,7 +46,7 @@ export function addArrayPropertyValueToNgModule( newPropertyValueMatch: string, newPropertyValue: string ): TransformOperation[] { - const ngModuleConfigNodesInFile = getDecoratorMetadata(sourceFile, 'NgModule', '@angular/core'); + const ngModuleConfigNodesInFile = getDecoratorMetadata(sourceFile, "NgModule", "@angular/core"); let ngModuleConfigNode: any = ngModuleConfigNodesInFile && ngModuleConfigNodesInFile[0]; if (!ngModuleConfigNode) { return null; @@ -63,15 +59,16 @@ export function addArrayPropertyValueToNgModule( } const ngLazyLoaderNode = ts.createIdentifier(NgLazyLoaderCode); - if (ngModuleConfigNode.kind == ts.SyntaxKind.Identifier) { + if (ngModuleConfigNode.kind === ts.SyntaxKind.Identifier) { + 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, ngModuleConfigNode.getText()); + return findNode(imp, ts.SyntaxKind.Identifier, ngModuleConfigIndentifierNode.getText()); }); // will be undefined when the object is imported from another file const configObjectDeclaration = (configObjectDeclarationNodes && configObjectDeclarationNodes[0]); - const configObjectName = ngModuleConfigNode.escapedText.trim(); + const configObjectName = (ngModuleConfigIndentifierNode.escapedText).trim(); const configObjectSetupCode = getConfigObjectSetupCode(configObjectName, targetPropertyName, newPropertyValueMatch, newPropertyValue); const configObjectSetupNode = ts.createIdentifier(configObjectSetupCode); @@ -79,7 +76,7 @@ export function addArrayPropertyValueToNgModule( new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode), new AddNodeOperation(sourceFile, configObjectDeclaration || lastImport, undefined, configObjectSetupNode) ]; - } else if (ngModuleConfigNode.kind == ts.SyntaxKind.ObjectLiteralExpression) { + } else if (ngModuleConfigNode.kind === ts.SyntaxKind.ObjectLiteralExpression) { // cases like @NgModule({ bootstrap: ... }) const ngModuleConfigObjectNode = ngModuleConfigNode as ts.ObjectLiteralExpression; const matchingProperties: ts.ObjectLiteralElement[] = getObjectPropertyMatches(ngModuleConfigObjectNode, sourceFile, targetPropertyName); @@ -88,8 +85,8 @@ export function addArrayPropertyValueToNgModule( return null; } - if (matchingProperties.length == 0) { - if (ngModuleConfigObjectNode.properties.length == 0) { + if (matchingProperties.length === 0) { + if (ngModuleConfigObjectNode.properties.length === 0) { // empty object @NgModule({ }) return null; } @@ -100,7 +97,8 @@ export function addArrayPropertyValueToNgModule( return [ new AddNodeOperation(sourceFile, lastConfigObjPropertyNode, undefined, newTargetPropertyNode), - new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode) + ]; } @@ -125,113 +123,34 @@ export function addArrayPropertyValueToNgModule( const lastPropertyValueNode = targetPropertyValues[targetPropertyValues.length - 1]; const newPropertyValueNode = ts.createIdentifier(`${newPropertyValue}`); - return [new AddNodeOperation(sourceFile, lastPropertyValueNode, undefined, newPropertyValueNode), - new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; + return [ + new AddNodeOperation(sourceFile, lastPropertyValueNode, undefined, newPropertyValueNode), + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode) + ]; } else { // empty array @NgModule({ targetProperty: [ ] }) const newTargetPropertyValuesNode = ts.createIdentifier(`[${newPropertyValue}]`); - return [new ReplaceNodeOperation(sourceFile, targetPropertyValuesNode, newTargetPropertyValuesNode), - new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode)]; - } - } -} - -function getDecoratorMetadata(source: ts.SourceFile, identifier: string, - module: string): ts.Node[] { - const angularImports: { [name: string]: string } - = collectDeepNodes(source, ts.SyntaxKind.ImportDeclaration) - .map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source)) - .reduce((acc: { [name: string]: string }, current: { [name: string]: string }) => { - for (const key of Object.keys(current)) { - acc[key] = current[key]; - } - - return acc; - }, {}); - - return getSourceNodes(source) - .filter(node => { - return node.kind == ts.SyntaxKind.Decorator - && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; - }) - .map(node => (node as ts.Decorator).expression as ts.CallExpression) - .filter(expr => { - if (expr.expression.kind == ts.SyntaxKind.Identifier) { - const id = expr.expression as ts.Identifier; - - return id.getFullText(source) == identifier - && angularImports[id.getFullText(source)] === module; - } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { - // This covers foo.NgModule when importing * as foo. - const paExpr = expr.expression as ts.PropertyAccessExpression; - // If the left expression is not an identifier, just give up at that point. - if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { - return false; - } - - const id = paExpr.name.text; - const moduleId = (paExpr.expression as ts.Identifier).getText(source); - - return id === identifier && (angularImports[moduleId + '.'] === module); - } - - return false; - }) - .filter(expr => expr.arguments[0] - && (expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression || - expr.arguments[0].kind == ts.SyntaxKind.Identifier)) - .map(expr => expr.arguments[0] as ts.Node); -} - -function _angularImportsFromNode(node: ts.ImportDeclaration, - _sourceFile: ts.SourceFile): { [name: string]: string } { - const ms = node.moduleSpecifier; - let modulePath: string; - switch (ms.kind) { - case ts.SyntaxKind.StringLiteral: - modulePath = (ms as ts.StringLiteral).text; - break; - default: - return {}; - } - - if (!modulePath.startsWith('@angular/')) { - return {}; - } - - if (node.importClause) { - if (node.importClause.name) { - // This is of the form `import Name from 'path'`. Ignore. - return {}; - } else if (node.importClause.namedBindings) { - const nb = node.importClause.namedBindings; - if (nb.kind == ts.SyntaxKind.NamespaceImport) { - // This is of the form `import * as name from 'path'`. Return `name.`. - return { - [(nb as ts.NamespaceImport).name.text + '.']: modulePath, - }; - } else { - // This is of the form `import {a,b,c} from 'path'` - const namedImports = nb as ts.NamedImports; - - return namedImports.elements - .map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) - .reduce((acc: { [name: string]: string }, curr: string) => { - acc[curr] = modulePath; - - return acc; - }, {}); - } + return [ + new ReplaceNodeOperation(sourceFile, targetPropertyValuesNode, newTargetPropertyValuesNode), + new AddNodeOperation(sourceFile, lastImport, undefined, ngLazyLoaderNode) + ]; } - - return {}; - } else { - // This is of the form `import 'path';`. Nothing to do. - return {}; } } +// handles cases like @NgModule(myCoolConfig) by returning a code snippet for processing +// the config object and configuring its {{targetPropertyName}} based on the specified arguments +// e.g. +// if (!myCoolConfig.providers) { +// myCoolConfig.providers = []; +// } +// if (Array.isArray(myCoolConfig.providers)) { +// var wholeWordPropertyRegex = new RegExp("\bNgModuleFactoryLoader\b"); +// if (!myCoolConfig.providers.some(function (property) { return wholeWordPropertyRegex.test(property); })) { +// myCoolConfig.providers.push({ provide: nsNgCoreImport_Generated.NgModuleFactoryLoader, useClass: NSLazyModulesLoader_Generated }); +// } +// } export function getConfigObjectSetupCode(configObjectName: string, targetPropertyName: string, newPropertyValueMatch: string, newPropertyValue: string) { return ` if (!${configObjectName}.${targetPropertyName}) { @@ -298,4 +217,4 @@ var NSLazyModulesLoader_Generated = /** @class */ (function () { ], NSLazyModulesLoader_Generated); return NSLazyModulesLoader_Generated; }()); -`; \ No newline at end of file +`; diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index d2a77522..708bfff6 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -3,9 +3,10 @@ // 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 -import { dirname, join } from 'path'; -import * as ts from 'typescript'; -const { readFileSync, existsSync } = require("fs"); +import { dirname, join } from "path"; +import * as ts from "typescript"; +import { readFileSync, existsSync } from "fs"; +import { collectDeepNodes } from "@ngtools/webpack/src/transformers"; export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | null { if (!existsSync(mainPath)) { @@ -22,7 +23,7 @@ export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | n for (const node of allNodes) { let bootstrapCallNode: ts.Node | null = null; - bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, 'bootstrapModule'); + bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, "bootstrapModule"); // Walk up the parent until CallExpression is found. while (bootstrapCallNode && bootstrapCallNode.parent @@ -45,7 +46,7 @@ export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | n export function findBootstrapModulePath(mainPath: string): string { const bootstrapCall = findBootstrapModuleCall(mainPath); if (!bootstrapCall) { - throw new Error('Bootstrap call not found'); + throw new Error("Bootstrap call not found"); } const bootstrapModule = bootstrapCall.arguments[0]; @@ -80,7 +81,6 @@ export function getAppModulePath(mainPath: string): string { export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null { if (node.kind === kind && node.getText() === text) { - // throw new Error(node.getText()); return node; } @@ -124,4 +124,101 @@ export function getObjectPropertyMatches(objectNode: ts.ObjectLiteralExpression, } return false; }); -} \ No newline at end of file +} + + + +export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, + module: string): ts.Node[] { + const angularImports: { [name: string]: string } + = collectDeepNodes(source, ts.SyntaxKind.ImportDeclaration) + .map((node: ts.ImportDeclaration) => angularImportsFromNode(node, source)) + .reduce((acc: { [name: string]: string }, current: { [name: string]: string }) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, {}); + + return getSourceNodes(source) + .filter(node => { + return node.kind == ts.SyntaxKind.Decorator + && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return id.getFullText(source) == identifier + && angularImports[id.getFullText(source)] === module; + } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && (angularImports[moduleId + '.'] === module); + } + + return false; + }) + .filter(expr => expr.arguments[0] + && (expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression || + expr.arguments[0].kind == ts.SyntaxKind.Identifier)) + .map(expr => expr.arguments[0] as ts.Node); +} + +export function angularImportsFromNode(node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} diff --git a/utils/transformers-utils.ts b/utils/transformers-utils.ts new file mode 100644 index 00000000..ffbd89b6 --- /dev/null +++ b/utils/transformers-utils.ts @@ -0,0 +1,8 @@ +import { workaroundResolve } from "@ngtools/webpack/src/compiler_host"; +import { AngularCompilerPlugin } from "@ngtools/webpack"; + +export function getResolvedEntryModule(ngCompiler: AngularCompilerPlugin) { + return ngCompiler.entryModule + ? { path: workaroundResolve(ngCompiler.entryModule.path), className: ngCompiler.entryModule.className } + : ngCompiler.entryModule; +} \ No newline at end of file From 73411d8c5b5e00affd63ad4cd13f082b2c27b5db Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 20 Dec 2018 14:50:17 +0200 Subject: [PATCH 3/5] refactor: remove blank lines --- utils/ast-utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 708bfff6..2cec63c6 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -126,8 +126,6 @@ export function getObjectPropertyMatches(objectNode: ts.ObjectLiteralExpression, }); } - - export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, module: string): ts.Node[] { const angularImports: { [name: string]: string } From 4238daf36808c2b9068361856ba6d24ebaaf9edc Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Fri, 21 Dec 2018 11:27:10 +0200 Subject: [PATCH 4/5] fix: move a typescript specific logic in ast utils in order to avoid invalid import in JS applications --- index.js | 9 --------- templates/webpack.angular.js | 3 ++- utils/ast-utils.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 9d7a8422..1b3a317c 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ const path = require("path"); const { existsSync } = require("fs"); -const { findBootstrapModulePath } = require("./utils/ast-utils"); const { ANDROID_APP_PATH } = require("./androidProjectHelpers"); const { getPackageJson, @@ -41,14 +40,6 @@ exports.getEntryModule = function (appDirectory) { return entry; }; -exports.getMainModulePath = function (entryFilePath) { - try { - return findBootstrapModulePath(entryFilePath); - } catch (e) { - return null; - } -} - exports.getAppPath = (platform, projectDir) => { if (isIos(platform)) { const appName = path.basename(projectDir); diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index f389ecd4..a68bf838 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 { getMainModulePath } = require("nativescript-dev-webpack/utils/ast-utils"); const CleanWebpackPlugin = require("clean-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); @@ -65,7 +66,7 @@ module.exports = env => { // 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 (env.externals.indexOf("@angular/core") > -1) { - const appModuleRelativePath = nsWebpack.getMainModulePath(resolve(appFullPath, entryModule)); + const appModuleRelativePath = getMainModulePath(resolve(appFullPath, entryModule)); if (appModuleRelativePath) { const appModuleFolderPath = dirname(resolve(appFullPath, appModuleRelativePath)); // include the lazy loader inside app module diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 2cec63c6..bb4ae6a1 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -8,6 +8,14 @@ import * as ts from "typescript"; import { readFileSync, existsSync } from "fs"; import { collectDeepNodes } from "@ngtools/webpack/src/transformers"; +export function getMainModulePath(entryFilePath) { + try { + return findBootstrapModulePath(entryFilePath); + } catch (e) { + return null; + } +} + export function findBootstrapModuleCall(mainPath: string): ts.CallExpression | null { if (!existsSync(mainPath)) { throw new Error(`Main file (${mainPath}) not found`); From f7641dc8e9211bc001b5dc11c8bb0ee93c4621a7 Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Fri, 21 Dec 2018 14:06:41 +0200 Subject: [PATCH 5/5] fix: normalize the app module paths in order to avoid incorrect comparison on Windows --- transformers/ns-replace-lazy-loader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transformers/ns-replace-lazy-loader.ts b/transformers/ns-replace-lazy-loader.ts index 9a17e04d..324c9e9e 100644 --- a/transformers/ns-replace-lazy-loader.ts +++ b/transformers/ns-replace-lazy-loader.ts @@ -3,7 +3,7 @@ // 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 -import { dirname, basename, extname, join } from "path"; +import { dirname, basename, extname, join, normalize } from "path"; import * as ts from "typescript"; import { StandardTransform, @@ -24,7 +24,7 @@ export function nsReplaceLazyLoader(getNgCompiler: () => AngularCompilerPlugin): let ops: TransformOperation[] = []; const entryModule = getResolvedEntryModule(getNgCompiler()); const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName))); - if (!entryModule || sourceFilePath !== entryModule.path) { + if (!entryModule || normalize(sourceFilePath) !== normalize(entryModule.path)) { return ops; }