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..1b3a317c 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ const path = require("path"); const { existsSync } = require("fs"); - const { ANDROID_APP_PATH } = require("./androidProjectHelpers"); const { getPackageJson, @@ -12,22 +11,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,7 +34,7 @@ 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; @@ -72,10 +71,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..a68bf838 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -1,9 +1,11 @@ -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 { 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"); @@ -44,7 +46,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 +56,34 @@ module.exports = env => { const entryModule = `${nsWebpack.getEntryModule(appFullPath)}.ts`; const entryPath = `.${sep}${entryModule}`; + const ngCompilerTransformers = []; + const additionalLazyModuleResources = []; + if (aot) { + 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 = 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-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 new file mode 100644 index 00000000..590889da --- /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 it's 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..324c9e9e --- /dev/null +++ b/transformers/ns-replace-lazy-loader.ts @@ -0,0 +1,220 @@ +// 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, normalize } from "path"; +import * as ts from "typescript"; +import { + StandardTransform, + TransformOperation, + collectDeepNodes, + AddNodeOperation, + ReplaceNodeOperation, + makeTransform +} from "@ngtools/webpack/src/transformers"; +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 entryModule = getResolvedEntryModule(getNgCompiler()); + const sourceFilePath = join(dirname(sourceFile.fileName), basename(sourceFile.fileName, extname(sourceFile.fileName))); + if (!entryModule || normalize(sourceFilePath) !== normalize(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) { + 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()); + }); + // will be undefined when the object is imported from another file + const configObjectDeclaration = (configObjectDeclarationNodes && configObjectDeclarationNodes[0]); + + const configObjectName = (ngModuleConfigIndentifierNode.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) + ]; + } + } +} + +// 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}) { + ${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; +}()); +`; diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts new file mode 100644 index 00000000..bb4ae6a1 --- /dev/null +++ b/utils/ast-utils.ts @@ -0,0 +1,230 @@ +// 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"; +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`); + } + 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) { + 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; + }); +} + +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