diff --git a/config/ngc.config.js b/config/ngc.config.js deleted file mode 100644 index 1237e296..00000000 --- a/config/ngc.config.js +++ /dev/null @@ -1,10 +0,0 @@ - -module.exports = { - - include: [ - './**/*.d.ts', - './app/app.module.ts', - './app/main.prod.ts' - ] - -}; diff --git a/config/webpack.config.js b/config/webpack.config.js index 6c815ffd..78c4bda5 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -6,41 +6,13 @@ var ionicWebpackFactory = require(ionicWebpackFactoryPath); function getEntryPoint() { if (process.env.IONIC_ENV === 'prod') { - return '{{TMP}}/app/main.prod.js'; + return '{{SRC}}/app/main.ts'; } - return '{{SRC}}/app/main.dev.ts'; + return '{{SRC}}/app/main.ts'; } function getPlugins() { - if (process.env.IONIC_ENV === 'prod') { - return [ - // This helps ensure the builds are consistent if source hasn't changed: - new webpack.optimize.OccurrenceOrderPlugin(), - - // Try to dedupe duplicated modules, if any: - // Add this back in when Angular fixes the issue: https://github.com/angular/angular-cli/issues/1587 - //new DedupePlugin() - ]; - } - - // for dev builds, use our custom environment - return [ - ionicWebpackFactory.getIonicEnvironmentPlugin() - ]; -} - -function getSourcemapLoader() { - if (process.env.IONIC_ENV === 'prod') { - // TODO figure out the disk loader, it's not working yet - return []; - } - - return [ - { - test: /\.ts$/, - loader: path.join(process.env.IONIC_APP_SCRIPTS_DIR, 'dist', 'webpack', 'typescript-sourcemap-loader-memory.js') - } - ]; + return [ionicWebpackFactory.getIonicEnvironmentPlugin()]; } function getDevtool() { @@ -53,6 +25,7 @@ function getDevtool() { } module.exports = { + bail: true, entry: getEntryPoint(), output: { path: '{{BUILD}}', @@ -71,8 +44,12 @@ module.exports = { { test: /\.json$/, loader: 'json-loader' + }, + { + test: /\.(ts|ngfactory.js)$/, + loader: path.join(process.env.IONIC_APP_SCRIPTS_DIR, 'dist', 'webpack', 'typescript-sourcemap-loader-memory.js') } - ].concat(getSourcemapLoader()) + ] }, plugins: getPlugins(), diff --git a/src/aot/aot-compiler.ts b/src/aot/aot-compiler.ts new file mode 100644 index 00000000..2aad6f1d --- /dev/null +++ b/src/aot/aot-compiler.ts @@ -0,0 +1,151 @@ +import { readFileSync } from 'fs'; +import { extname } from 'path'; + +import 'reflect-metadata'; +import { CompilerOptions, createProgram, ParsedCommandLine, Program, transpileModule, TranspileOptions, TranspileOutput } from 'typescript'; +import { CodeGenerator, NgcCliOptions, NodeReflectorHostContext, ReflectorHost, StaticReflector }from '@angular/compiler-cli'; +import { tsc } from '@angular/tsc-wrapped/src/tsc'; +import AngularCompilerOptions from '@angular/tsc-wrapped/src/options'; + +import { HybridFileSystem } from '../util/hybrid-file-system'; +import { getInstance as getHybridFileSystem } from '../util/hybrid-file-system-factory'; +import { getInstance } from '../aot/compiler-host-factory'; +import { NgcCompilerHost } from '../aot/compiler-host'; +import { patchReflectorHost } from '../aot/reflector-host'; +import { removeDecorators } from '../util/typescript-utils'; +import { getMainFileTypescriptContentForAotBuild } from './utils'; +import { printDiagnostics, clearDiagnostics, DiagnosticsType } from '../logger/logger-diagnostics'; +import { runTypeScriptDiagnostics } from '../logger/logger-typescript'; +import { BuildError } from '../util/errors'; +import { changeExtension } from '../util/helpers'; +import { BuildContext } from '../util/interfaces'; + +export class AotCompiler { + + private tsConfig: ParsedTsConfig; + private angularCompilerOptions: AngularCompilerOptions; + private program: Program; + private reflector: StaticReflector; + private reflectorHost: ReflectorHost; + private compilerHost: NgcCompilerHost; + private fileSystem: HybridFileSystem; + + constructor(private context: BuildContext, private options: AotOptions) { + this.tsConfig = getNgcConfig(this.context, this.options.tsConfigPath); + + this.angularCompilerOptions = Object.assign({}, this.tsConfig.ngOptions, { + basePath: this.options.rootDir, + entryPoint: this.options.entryPoint + }); + + this.fileSystem = getHybridFileSystem(); + this.compilerHost = getInstance(this.tsConfig.parsed.options); + this.program = createProgram(this.tsConfig.parsed.fileNames, this.tsConfig.parsed.options, this.compilerHost); + this.reflectorHost = new ReflectorHost(this.program, this.compilerHost, this.angularCompilerOptions); + this.reflector = new StaticReflector(this.reflectorHost); + } + + compile() { + clearDiagnostics(this.context, DiagnosticsType.TypeScript); + const i18nOptions: NgcCliOptions = { + i18nFile: undefined, + i18nFormat: undefined, + locale: undefined, + basePath: this.options.rootDir + }; + + // Create the Code Generator. + const codeGenerator = CodeGenerator.create( + this.angularCompilerOptions, + i18nOptions, + this.program, + this.compilerHost, + new NodeReflectorHostContext(this.compilerHost) + ); + + // We need to temporarily patch the CodeGenerator until either it's patched or allows us + // to pass in our own ReflectorHost. + patchReflectorHost(codeGenerator); + return codeGenerator.codegen({transitiveModules: true}) + .then(() => { + // Create a new Program, based on the old one. This will trigger a resolution of all + // transitive modules, which include files that might just have been generated. + this.program = createProgram(this.tsConfig.parsed.fileNames, this.tsConfig.parsed.options, this.compilerHost, this.program); + + const tsDiagnostics = this.program.getSyntacticDiagnostics() + .concat(this.program.getSemanticDiagnostics()) + .concat(this.program.getOptionsDiagnostics()); + + if (tsDiagnostics.length) { + throw tsDiagnostics; + } + }) + .then(() => { + for ( const fileName of this.tsConfig.parsed.fileNames) { + const content = readFileSync(fileName).toString(); + this.context.fileCache.set(fileName, { path: fileName, content: content}); + } + }) + .then(() => { + const mainContent = getMainFileTypescriptContentForAotBuild(); + this.context.fileCache.set(this.options.entryPoint, { path: this.options.entryPoint, content: mainContent}); + }) + .then(() => { + const tsFiles = this.context.fileCache.getAll().filter(file => extname(file.path) === '.ts' && file.path.indexOf('.d.ts') === -1); + for (const tsFile of tsFiles) { + const cleanedFileContent = removeDecorators(tsFile.path, tsFile.content); + tsFile.content = cleanedFileContent; + const transpileOutput = this.transpileFileContent(tsFile.path, cleanedFileContent, this.tsConfig.parsed.options); + const diagnostics = runTypeScriptDiagnostics(this.context, transpileOutput.diagnostics); + if (diagnostics.length) { + // darn, we've got some things wrong, transpile failed :( + printDiagnostics(this.context, DiagnosticsType.TypeScript, diagnostics, true, true); + throw new BuildError(); + } + + const jsFilePath = changeExtension(tsFile.path, '.js'); + this.fileSystem.addVirtualFile(jsFilePath, transpileOutput.outputText); + this.fileSystem.addVirtualFile(jsFilePath + '.map', transpileOutput.sourceMapText); + } + }); + } + + transpileFileContent(fileName: string, sourceText: string, options: CompilerOptions): TranspileOutput { + const sourceFile = this.program.getSourceFile(fileName); + const diagnostics = this.program.getSyntacticDiagnostics(sourceFile) + .concat(this.program.getSemanticDiagnostics(sourceFile)) + .concat(this.program.getDeclarationDiagnostics(sourceFile)); + if (diagnostics.length) { + throw diagnostics; + } + + const transpileOptions: TranspileOptions = { + compilerOptions: options, + fileName: fileName, + reportDiagnostics: true + }; + + return transpileModule(sourceText, transpileOptions); + } +} + +export interface AotOptions { + tsConfigPath: string; + rootDir: string; + entryPoint: string; +} + +export function getNgcConfig(context: BuildContext, tsConfigPath?: string): ParsedTsConfig { + + const tsConfigFile = tsc.readConfiguration(tsConfigPath, process.cwd()); + if (!tsConfigFile) { + throw new BuildError(`tsconfig: invalid tsconfig file, "${tsConfigPath}"`); + + } + return tsConfigFile; +} + +export interface ParsedTsConfig { + parsed: ParsedCommandLine; + ngOptions: AngularCompilerOptions; +} diff --git a/src/aot/compiler-host-factory.ts b/src/aot/compiler-host-factory.ts new file mode 100644 index 00000000..aae3ab88 --- /dev/null +++ b/src/aot/compiler-host-factory.ts @@ -0,0 +1,12 @@ +import { CompilerOptions } from 'typescript'; +import { NgcCompilerHost } from './compiler-host'; +import { getInstance as getFileSystemInstance } from '../util/hybrid-file-system-factory'; + +let instance: NgcCompilerHost = null; + +export function getInstance(options: CompilerOptions) { + if (!instance) { + instance = new NgcCompilerHost(options, getFileSystemInstance()); + } + return instance; +} diff --git a/src/aot/compiler-host.ts b/src/aot/compiler-host.ts new file mode 100644 index 00000000..5d090e15 --- /dev/null +++ b/src/aot/compiler-host.ts @@ -0,0 +1,103 @@ +import { CancellationToken, CompilerHost, CompilerOptions, createCompilerHost, ScriptTarget, SourceFile } from 'typescript'; +import { VirtualFileSystem } from '../util/interfaces'; +import { getTypescriptSourceFile } from '../util/typescript-utils'; + +export interface OnErrorFn { + (message: string): void; +} + +export class NgcCompilerHost implements CompilerHost { + private sourceFileMap: Map; + private diskCompilerHost: CompilerHost; + + constructor(private options: CompilerOptions, private fileSystem: VirtualFileSystem, private setParentNodes = true) { + this.diskCompilerHost = createCompilerHost(this.options, this.setParentNodes); + this.sourceFileMap = new Map(); + } + + fileExists(filePath: string): boolean { + const fileContent = this.fileSystem.getFileContent(filePath); + if (fileContent) { + return true; + } + return this.diskCompilerHost.fileExists(filePath); + } + + readFile(filePath: string): string { + const fileContent = this.fileSystem.getFileContent(filePath); + if (fileContent) { + return fileContent; + } + return this.diskCompilerHost.readFile(filePath); + } + + directoryExists(directoryPath: string): boolean { + const stats = this.fileSystem.getDirectoryStats(directoryPath); + if (stats) { + return true; + } + return this.diskCompilerHost.directoryExists(directoryPath); + } + + getFiles(directoryPath: string): string[] { + return this.fileSystem.getFileNamesInDirectory(directoryPath); + } + + getDirectories(directoryPath: string): string[] { + const subdirs = this.fileSystem.getSubDirs(directoryPath); + + let delegated: string[]; + try { + delegated = this.diskCompilerHost.getDirectories(directoryPath); + } catch (e) { + delegated = []; + } + return delegated.concat(subdirs); + } + + getSourceFile(filePath: string, languageVersion: ScriptTarget, onError?: OnErrorFn) { + const existingSourceFile = this.sourceFileMap.get(filePath); + if (existingSourceFile) { + return existingSourceFile; + } + // we haven't created a source file for this yet, so try to use what's in memory + const fileContentFromMemory = this.fileSystem.getFileContent(filePath); + if (fileContentFromMemory) { + const typescriptSourceFile = getTypescriptSourceFile(filePath, fileContentFromMemory, languageVersion, this.setParentNodes); + this.sourceFileMap.set(filePath, typescriptSourceFile); + return typescriptSourceFile; + } + // dang, it's not in memory, load it from disk and cache it + const diskSourceFile = this.diskCompilerHost.getSourceFile(filePath, languageVersion, onError); + this.sourceFileMap.set(filePath, diskSourceFile); + return diskSourceFile; + } + + getCancellationToken(): CancellationToken { + return this.diskCompilerHost.getCancellationToken(); + } + + getDefaultLibFileName(options: CompilerOptions) { + return this.diskCompilerHost.getDefaultLibFileName(options); + } + + writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) { + this.fileSystem.addVirtualFile(fileName, data); + } + + getCurrentDirectory(): string { + return this.diskCompilerHost.getCurrentDirectory(); + } + + getCanonicalFileName(fileName: string): string { + return this.diskCompilerHost.getCanonicalFileName(fileName); + } + + useCaseSensitiveFileNames(): boolean { + return this.diskCompilerHost.useCaseSensitiveFileNames(); + } + + getNewLine(): string { + return this.diskCompilerHost.getNewLine(); + } +} diff --git a/src/aot/optimization.ts b/src/aot/optimization.ts new file mode 100644 index 00000000..9d3af19e --- /dev/null +++ b/src/aot/optimization.ts @@ -0,0 +1,47 @@ +import { removeDecorators } from '../util/typescript-utils'; + +export function optimizeJavascript(filePath: string, fileContent: string) { + fileContent = removeDecorators(filePath, fileContent); + fileContent = purgeDecoratorStatements(filePath, fileContent, ['@angular']); + fileContent = purgeCtorStatements(filePath, fileContent, ['@angular']); + fileContent = purgeKnownContent(filePath, fileContent, ['@angular']); + + return fileContent; +} + +export function purgeDecoratorStatements(filePath: string, fileContent: string, exclusions: string[]) { + const exclude = shouldExclude(filePath, exclusions); + if (exclude) { + return fileContent.replace(DECORATORS_REGEX, ''); + } + return fileContent; +} + +export function purgeCtorStatements(filePath: string, fileContent: string, exclusions: string[]) { + const exclude = shouldExclude(filePath, exclusions); + if (exclude) { + return fileContent.replace(CTOR_PARAM_REGEX, ''); + } + return fileContent; +} + +export function purgeKnownContent(filePath: string, fileContent: string, exclusions: string[]) { + const exclude = shouldExclude(filePath, exclusions); + if (exclude) { + return fileContent.replace(TREE_SHAKEABLE_IMPORTS, ''); + } + return fileContent; +} + +function shouldExclude(filePath: string, exclusions: string[]) { + for (const exclusion in exclusions) { + if (filePath.includes(exclusion)) { + return true; + } + } + return false; +} + +const DECORATORS_REGEX = /(.+)\.decorators[\s\S\n]*?([\s\S\n]*?)];/igm; +const CTOR_PARAM_REGEX = /(.+).ctorParameters[\s\S\n]*?([\s\S\n]*?)];/igm; +const TREE_SHAKEABLE_IMPORTS = /\/\* AoT Remove Start[\s\S\n]*?([\s\S\n]*?)AoT Remove End \*\//igm; diff --git a/src/aot/reflector-host.ts b/src/aot/reflector-host.ts new file mode 100644 index 00000000..6db2e693 --- /dev/null +++ b/src/aot/reflector-host.ts @@ -0,0 +1,25 @@ +import { CodeGenerator } from '@angular/compiler-cli'; + +/** + * Patch the CodeGenerator instance to use a custom reflector host. + */ +export function patchReflectorHost(codeGenerator: CodeGenerator) { + const reflectorHost = (codeGenerator as any).reflectorHost; + const oldGIP = reflectorHost.getImportPath; + + reflectorHost.getImportPath = function(containingFile: string, importedFile: string): string { + // Hack together SCSS and LESS files URLs so that they match what the default ReflectorHost + // is expected. We only do that for shimmed styles. + const m = importedFile.match(/(.*)(\..+)(\.shim)(\..+)/); + if (!m) { + return oldGIP.call(this, containingFile, importedFile); + } + + // We call the original, with `css` in its name instead of the extension, and replace the + // extension from the result. + const [, baseDirAndName, styleExt, shim, ext] = m; + const result = oldGIP.call(this, containingFile, baseDirAndName + '.css' + shim + ext); + + return result.replace(/\.css\./, styleExt + '.'); + }; +} diff --git a/src/aot/utils.ts b/src/aot/utils.ts new file mode 100644 index 00000000..6642fdb2 --- /dev/null +++ b/src/aot/utils.ts @@ -0,0 +1,10 @@ +export function getMainFileTypescriptContentForAotBuild() { + return ` +import { platformBrowser } from '@angular/platform-browser'; +import { enableProdMode } from '@angular/core'; + +import { AppModuleNgFactory } from './app.module.ngfactory'; + +enableProdMode(); +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);`; +} diff --git a/src/build.ts b/src/build.ts index 12a16b5a..3cd54044 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,7 +1,7 @@ import { FILE_CHANGE_EVENT, FILE_DELETE_EVENT } from './util/constants'; import { BuildContext, BuildState, BuildUpdateMessage, ChangedFile } from './util/interfaces'; import { BuildError } from './util/errors'; -import { readFileAsync } from './util/helpers'; +import { readFileAsync, setContext } from './util/helpers'; import { bundle, bundleUpdate } from './bundle'; import { clean } from './clean'; import { copy } from './copy'; @@ -18,7 +18,7 @@ import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpil export function build(context: BuildContext) { context = generateContext(context); - + setContext(context); const logger = new Logger(`build ${(context.isProd ? 'prod' : 'dev')}`); return buildWorker(context) diff --git a/src/ngc.ts b/src/ngc.ts index 901d35d7..cb266020 100644 --- a/src/ngc.ts +++ b/src/ngc.ts @@ -1,13 +1,7 @@ -import { basename, join } from 'path'; -import { BuildContext, TaskInfo } from './util/interfaces'; -import { BuildError } from './util/errors'; -import { copy as fsCopy, emptyDirSync, outputJsonSync, readFileSync, statSync } from 'fs-extra'; -import { fillConfigDefaults, generateContext, getUserConfigFile, getNodeBinExecutable } from './util/config'; -import { getTsConfigPath } from './transpile'; import { Logger } from './logger/logger'; -import { objectAssign } from './util/helpers'; -import * as ts from 'typescript'; - +import { generateContext, getUserConfigFile} from './util/config'; +import { BuildContext, TaskInfo } from './util/interfaces'; +import { AotCompiler } from './aot/aot-compiler'; export function ngc(context?: BuildContext, configFile?: string) { context = generateContext(context); @@ -24,179 +18,15 @@ export function ngc(context?: BuildContext, configFile?: string) { }); } - export function ngcWorker(context: BuildContext, configFile: string) { - // first make a copy of src TS files - // and copy them into the tmp directory - return copySrcTsToTmpDir(context).then(() => { - return runNgc(context, configFile); - }); -} - - -function runNgc(context: BuildContext, configFile: string) { - return new Promise((resolve, reject) => { - const ngcConfig: NgcConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); - - // make a copy of the users src tsconfig file - // and save the modified copy into the tmp directory - createTmpTsConfig(context, ngcConfig); - - const ngcCmd = getNodeBinExecutable(context, 'ngc'); - if (!ngcCmd) { - reject(new BuildError(`Unable to find Angular Compiler "ngc" command: ${ngcCmd}. Please ensure @angular/compiler-cli has been installed with NPM.`)); - return; - } - - // let's kick off the actual ngc command on our copied TS files - // use the user's ngc in their node_modules to ensure ngc - // versioned and working along with the user's ng2 version - const spawn = require('cross-spawn'); - const ngcCmdArgs = [ - '--project', getTmpTsConfigPath(context) - ]; - - Logger.debug(`run: ${ngcCmd} ${ngcCmdArgs.join(' ')}`); - - // would love to not use spawn here but import and run ngc directly - const cp = spawn(ngcCmd, ngcCmdArgs); - - let errorMsgs: string[] = []; - - cp.stdout.on('data', (data: string) => { - Logger.info(data); - }); - - cp.stderr.on('data', (data: string) => { - if (data) { - data.toString().split('\n').forEach(line => { - if (!line.trim().length) { - // if it's got no data then don't bother - return; - } - if (line.substr(0, 4) === ' ' || line === 'Compilation failed') { - // if it's indented then it's some callstack message we don't care about - return; - } - // split by the : character, then rebuild the line until it's too long - // and make a new line - const lineSections = line.split(': '); - let msgSections: string[] = []; - for (var i = 0; i < lineSections.length; i++) { - msgSections.push(lineSections[i]); - if (msgSections.join(': ').length > 40) { - errorMsgs.push(msgSections.join(': ')); - msgSections = []; - } - } - if (msgSections.length) { - errorMsgs.push(msgSections.join(': ')); - } - }); - } - }); - - cp.on('close', (code: string) => { - if (errorMsgs.length) { - errorMsgs.forEach(errorMsg => { - Logger.error(errorMsg); - }); - reject(new BuildError()); - - } else { - resolve(); - } - }); - }); + const compiler = new AotCompiler(context, { entryPoint: process.env.IONIC_APP_ENTRY_POINT_PATH, rootDir: context.rootDir, tsConfigPath: process.env.IONIC_TS_CONFIG_PATH }); + return compiler.compile(); } - -function createTmpTsConfig(context: BuildContext, ngcConfig: NgcConfig) { - // create the tsconfig from the original src - const tsConfigPath = getTsConfigPath(context); - const tsConfigFile = ts.readConfigFile(tsConfigPath, path => readFileSync(path, 'utf8')); - - if (!tsConfigFile || !tsConfigFile.config) { - throw new BuildError(`invalid tsconfig: ${tsConfigPath}`); - } - - if (!tsConfigFile.config.compilerOptions) { - throw new BuildError(`invalid tsconfig compilerOptions: ${tsConfigPath}`); - } - - // delete outDir if it's set since we only want - // to compile to the same directory we're in - delete tsConfigFile.config.compilerOptions.outDir; - - const mergedConfig = objectAssign({}, tsConfigFile.config, ngcConfig); - - // save the modified copy into the tmp directory - outputJsonSync(getTmpTsConfigPath(context), mergedConfig); -} - - -function copySrcTsToTmpDir(context: BuildContext) { - return new Promise((resolve, reject) => { - - // ensure the tmp directory is ready to go - try { - emptyDirSync(context.tmpDir); - } catch (e) { - reject(new BuildError(`tmpDir error: ${e}`)); - return; - } - - const copyOpts: any = { - filter: filterCopyFiles - }; - - Logger.debug(`copySrcTsToTmpDir, srcDir: ${context.srcDir} to tmpDir: ${context.tmpDir}`); - - fsCopy(context.srcDir, context.tmpDir, copyOpts, (err) => { - if (err) { - reject(new BuildError(err)); - } else { - resolve(); - } - }); - }); -} - - -function filterCopyFiles(filePath: any, hoop: any) { - let shouldInclude = false; - - try { - const stats = statSync(filePath); - if (stats.isDirectory()) { - shouldInclude = (EXCLUDE_DIRS.indexOf(basename(filePath)) < 0); - - } else { - shouldInclude = (filePath.endsWith('.ts') || filePath.endsWith('.html')); - } - } catch (e) {} - - return shouldInclude; -} - - -export function getTmpTsConfigPath(context: BuildContext) { - return join(context.tmpDir, 'tsconfig.json'); -} - - -const EXCLUDE_DIRS = ['assets', 'theme']; - - const taskInfo: TaskInfo = { fullArg: '--ngc', shortArg: '-n', envVar: 'IONIC_NGC', packageConfig: 'ionic_ngc', - defaultConfigFile: 'ngc.config' + defaultConfigFile: null }; - - -export interface NgcConfig { - include: string[]; -} diff --git a/src/rollup.ts b/src/rollup.ts index 39941f3a..062fac51 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -1,7 +1,7 @@ import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config'; -import { ionCompiler } from './plugins/ion-compiler'; +import { ionicRollupResolverPlugin, PLUGIN_NAME } from './rollup/ionic-rollup-resolver-plugin'; import { join, isAbsolute, normalize, sep } from 'path'; import { Logger } from './logger/logger'; import * as rollupBundler from 'rollup'; @@ -52,16 +52,7 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise rollupConfig.entry = replacePathVars(context, normalize(rollupConfig.entry)); rollupConfig.dest = replacePathVars(context, normalize(rollupConfig.dest)); - if (!context.isProd) { - // ngc does full production builds itself and the bundler - // will already have receive transpiled and AoT templates - - // dev mode auto-adds the ion-compiler plugin, which will inline - // templates and transpile source typescript code to JS before bundling - rollupConfig.plugins.unshift( - ionCompiler(context) - ); - } + addRollupPluginIfNecessary(context, rollupConfig.plugins); // tell rollup to use a previous bundle as its starting point rollupConfig.cache = cachedBundle; @@ -73,8 +64,6 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise Logger.debug(`entry: ${rollupConfig.entry}, dest: ${rollupConfig.dest}, cache: ${rollupConfig.cache}, format: ${rollupConfig.format}`); - checkDeprecations(context, rollupConfig); - // bundle the app then create create css rollupBundler.rollup(rollupConfig) .then((bundle: RollupBundle) => { @@ -114,6 +103,20 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise }); } +function addRollupPluginIfNecessary(context: BuildContext, plugins: any[]) { + let found = false; + for (const plugin of plugins) { + if (plugin.name === PLUGIN_NAME) { + found = true; + break; + } + } + if (!found) { + // always add the Ionic plugin to the front of the list + plugins.unshift(ionicRollupResolverPlugin(context)); + } +} + export function getRollupConfig(context: BuildContext, configFile: string): RollupConfig { configFile = getUserConfigFile(context, taskInfo, configFile); @@ -130,19 +133,6 @@ export function getOutputDest(context: BuildContext, rollupConfig: RollupConfig) return rollupConfig.dest; } - -function checkDeprecations(context: BuildContext, rollupConfig: RollupConfig) { - if (!context.isProd) { - if (rollupConfig.entry.indexOf('.tmp') > -1 || rollupConfig.entry.endsWith('.js')) { - // warning added 2016-10-05, v0.0.29 - throw new BuildError('\nDev builds no longer use the ".tmp" directory. Please update your rollup config\'s\n' + - 'entry to use your "src" directory\'s "main.dev.ts" TypeScript file.\n' + - 'For example, the entry for dev builds should be: "src/app/main.dev.ts"'); - - } - } -} - export function invalidateCache() { cachedBundle = null; } diff --git a/src/plugins/ion-compiler.ts b/src/rollup/ionic-rollup-resolver-plugin.ts similarity index 62% rename from src/plugins/ion-compiler.ts rename to src/rollup/ionic-rollup-resolver-plugin.ts index c6f2bbcb..236ab986 100644 --- a/src/plugins/ion-compiler.ts +++ b/src/rollup/ionic-rollup-resolver-plugin.ts @@ -1,34 +1,68 @@ +import { readFileSync } from 'fs'; import { changeExtension } from '../util/helpers'; import { BuildContext } from '../util/interfaces'; import { Logger } from '../logger/logger'; import { dirname, join, resolve } from 'path'; import * as pluginutils from 'rollup-pluginutils'; +import { optimizeJavascript } from '../aot/optimization'; +export const PLUGIN_NAME = 'ion-rollup-resolver'; -export function ionCompiler(context: BuildContext) { +export function ionicRollupResolverPlugin(context: BuildContext) { const filter = pluginutils.createFilter(INCLUDE, EXCLUDE); - return { - name: 'ion-compiler', + name: PLUGIN_NAME, transform(sourceText: string, sourcePath: string): any { + if (!filter(sourcePath)) { return null; } const jsSourcePath = changeExtension(sourcePath, '.js'); + const mapPath = jsSourcePath + '.map'; + if (context.fileCache) { - const file = context.fileCache.get(jsSourcePath); - const map = context.fileCache.get(jsSourcePath + '.map'); + let file = context.fileCache.get(jsSourcePath); + let map = context.fileCache.get(mapPath); + + // if the file and map aren't in memory, load them and cache them for future use + try { + if (!file) { + const content = readFileSync(jsSourcePath).toString(); + file = { path: jsSourcePath, content: content}; + context.fileCache.set(jsSourcePath, file); + } + } catch (ex) { + Logger.debug(`transform: Failed to load ${jsSourcePath} from disk`); + } + + try { + if (!map) { + const content = readFileSync(mapPath).toString(); + map = { path: mapPath, content: content}; + context.fileCache.set(mapPath, map); + } + } catch (ex) { + Logger.debug(`transform: Failed to load source map ${mapPath} from disk`); + // just return null and fallback to the default behavior + return null; + } + if (!file || !file.content) { Logger.debug(`transform: unable to find ${jsSourcePath}`); return null; } - let mapContent: any = null; - if (map.content) { + // remove decorators if prod build + if (context.isProd) { + file.content = optimizeJavascript(jsSourcePath, file.content); + } + + let mapContent: string = null; + if (map && map.content) { try { mapContent = JSON.parse(map.content); } catch (ex) { @@ -55,7 +89,6 @@ export function ionCompiler(context: BuildContext) { return file.content; } } - return null; } }; @@ -94,5 +127,5 @@ export function resolveId(importee: string, importer: string, context: BuildCont } -const INCLUDE = ['*.ts+(|x)', '**/*.ts+(|x)']; +const INCLUDE = ['*.ts+(|x)', '*.js+(|x)', '**/*.ts+(|x)', '**/*.js+(|x)']; const EXCLUDE = ['*.d.ts', '**/*.d.ts']; diff --git a/src/serve.ts b/src/serve.ts index 504e5354..52027ce2 100644 --- a/src/serve.ts +++ b/src/serve.ts @@ -1,5 +1,6 @@ import { BuildContext } from './util/interfaces'; import { generateContext, getConfigValue, hasConfigValue } from './util/config'; +import { setContext } from './util/helpers'; import { Logger } from './logger/logger'; import { watch } from './watch'; import open from './util/open'; @@ -16,7 +17,7 @@ const DEV_SERVER_DEFAULT_HOST = '0.0.0.0'; export function serve(context?: BuildContext) { context = generateContext(context); - + setContext(context); const config: ServeConfig = { httpPort: getHttpServerPort(context), host: getHttpServerHost(context), diff --git a/src/spec/ion-compiler.spec.ts b/src/spec/ion-rollup-resolver-plugin.spec.ts similarity index 98% rename from src/spec/ion-compiler.spec.ts rename to src/spec/ion-rollup-resolver-plugin.spec.ts index 2517d7a0..51fa11eb 100644 --- a/src/spec/ion-compiler.spec.ts +++ b/src/spec/ion-rollup-resolver-plugin.spec.ts @@ -1,11 +1,11 @@ import { FileCache } from '../util/file-cache'; import { BuildContext } from '../util/interfaces'; import { dirname, join, resolve } from 'path'; -import { resolveId } from '../plugins/ion-compiler'; +import { resolveId } from '../rollup/ionic-rollup-resolver-plugin'; const importer = '/Users/dan/Dev/ionic-conference-app/src/app/app.module.ts'; -describe('ion-compiler', () => { +describe('ion-rollup-resolver', () => { describe('resolveId', () => { diff --git a/src/util/config.ts b/src/util/config.ts index 7090dc30..4462336c 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -47,6 +47,12 @@ export function generateContext(context?: BuildContext): BuildContext { const sourceMapValue = getConfigValue(context, '--sourceMap', null, ENV_VAR_SOURCE_MAP, ENV_VAR_SOURCE_MAP.toLowerCase(), 'eval'); setProcessEnvVar(ENV_VAR_SOURCE_MAP, sourceMapValue); + const tsConfigPathValue = getConfigValue(context, '--tsconfigPath', null, ENV_TS_CONFIG_PATH, ENV_TS_CONFIG_PATH.toLowerCase(), join(context.rootDir, 'tsconfig.json')); + setProcessEnvVar(ENV_TS_CONFIG_PATH, tsConfigPathValue); + + const appEntryPointPathValue = getConfigValue(context, '--appEntryPointPath', null, ENV_APP_ENTRY_POINT_PATH, ENV_APP_ENTRY_POINT_PATH.toLowerCase(), join(context.srcDir, 'app', 'main.ts')); + setProcessEnvVar(ENV_APP_ENTRY_POINT_PATH, appEntryPointPathValue); + if (!isValidBundler(context.bundler)) { context.bundler = bundlerStrategy(context); } @@ -401,6 +407,8 @@ const ENV_VAR_WWW_DIR = 'IONIC_WWW_DIR'; const ENV_VAR_BUILD_DIR = 'IONIC_BUILD_DIR'; const ENV_VAR_APP_SCRIPTS_DIR = 'IONIC_APP_SCRIPTS_DIR'; const ENV_VAR_SOURCE_MAP = 'IONIC_SOURCE_MAP'; +const ENV_TS_CONFIG_PATH = 'IONIC_TS_CONFIG_PATH'; +const ENV_APP_ENTRY_POINT_PATH = 'IONIC_APP_ENTRY_POINT_PATH'; export const BUNDLER_ROLLUP = 'rollup'; export const BUNDLER_WEBPACK = 'webpack'; diff --git a/src/util/hybrid-file-system-factory.ts b/src/util/hybrid-file-system-factory.ts new file mode 100644 index 00000000..8858fd8a --- /dev/null +++ b/src/util/hybrid-file-system-factory.ts @@ -0,0 +1,11 @@ +import { HybridFileSystem } from './hybrid-file-system'; +import { getContext } from './helpers'; + +let instance: HybridFileSystem = null; + +export function getInstance() { + if (!instance) { + instance = new HybridFileSystem(getContext().fileCache); + } + return instance; +} diff --git a/src/util/hybrid-file-system.ts b/src/util/hybrid-file-system.ts new file mode 100644 index 00000000..eeabfe11 --- /dev/null +++ b/src/util/hybrid-file-system.ts @@ -0,0 +1,107 @@ +import { basename, dirname } from 'path'; +import { FileSystem, VirtualFileSystem } from './interfaces'; +import { FileCache } from './file-cache'; +import { VirtualDirStats, VirtualFileStats } from './virtual-file-utils'; + +export class HybridFileSystem implements FileSystem, VirtualFileSystem { + + private filesStats: { [filePath: string]: VirtualFileStats } = {}; + private directoryStats: { [filePath: string]: VirtualDirStats } = {}; + private originalFileSystem: FileSystem; + + constructor(private fileCache: FileCache) { + } + + setFileSystem(fs: FileSystem) { + this.originalFileSystem = fs; + } + + isSync() { + return this.originalFileSystem.isSync(); + } + + stat(path: string, callback: Function): any { + // first check the fileStats + const fileStat = this.filesStats[path]; + if (fileStat) { + return callback(null, fileStat); + } + // then check the directory stats + const directoryStat = this.directoryStats[path]; + if (directoryStat) { + return callback(null, directoryStat); + } + // fallback to list + return this.originalFileSystem.stat(path, callback); + } + + readdir(path: string, callback: Function): any { + return this.originalFileSystem.readdir(path, callback); + } + + readJson(path: string, callback: Function): any { + return this.originalFileSystem.readJson(path, callback); + } + + readlink(path: string, callback: Function): any { + return this.originalFileSystem.readlink(path, (err: Error, response: any) => { + callback(err, response); + }); + } + + purge(pathsToPurge: string[]): void { + if (this.fileCache) { + for (const path of pathsToPurge) { + this.fileCache.remove(path); + } + } + } + + readFile(path: string, callback: Function): any { + const file = this.fileCache.get(path); + if (file) { + callback(null, new Buffer(file.content)); + return; + } + return this.originalFileSystem.readFile(path, callback); + } + + addVirtualFile(filePath: string, fileContent: string) { + this.fileCache.set(filePath, { path: filePath, content: fileContent }); + const fileStats = new VirtualFileStats(filePath, fileContent); + this.filesStats[filePath] = fileStats; + const directoryPath = dirname(filePath); + const directoryStats = new VirtualDirStats(directoryPath); + this.directoryStats[directoryPath] = directoryStats; + } + + getFileContent(filePath: string) { + const file = this.fileCache.get(filePath); + if (file) { + return file.content; + } + return null; + } + + getDirectoryStats(path: string): VirtualDirStats { + return this.directoryStats[path]; + } + + getSubDirs(directoryPath: string): string[] { + return Object.keys(this.directoryStats) + .filter(filePath => dirname(filePath) === directoryPath) + .map(filePath => basename(directoryPath)); + } + + getFileNamesInDirectory(directoryPath: string): string[] { + return Object.keys(this.filesStats).filter(filePath => dirname(filePath) === directoryPath).map(filePath => basename(filePath)); + } + + getAllFileStats(): { [filePath: string]: VirtualFileStats } { + return this.filesStats; + } + + getAllDirStats(): { [filePath: string]: VirtualDirStats } { + return this.directoryStats; + } +} diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts index b5251787..d27d921f 100644 --- a/src/util/interfaces.ts +++ b/src/util/interfaces.ts @@ -1,5 +1,5 @@ import { FileCache } from './file-cache'; - +import { VirtualDirStats, VirtualFileStats } from './virtual-file-utils'; export interface BuildContext { rootDir?: string; @@ -100,8 +100,31 @@ export interface BuildUpdateMessage { reloadApp: boolean; } + export interface ChangedFile { event: string; filePath: string; ext: string; } + + +export interface FileSystem { + isSync(): boolean; + stat(path: string, callback: Function): any; + readdir(path: string, callback: Function): any; + readFile(path: string, callback: Function): any; + readJson(path: string, callback: Function): any; + readlink(path: string, callback: Function): any; + purge(what: any): void; +}; + + +export interface VirtualFileSystem { + addVirtualFile(filePath: string, fileContent: string): void; + getFileContent(filePath: string): string; + getDirectoryStats(path: string): VirtualDirStats; + getSubDirs(directoryPath: string): string[]; + getFileNamesInDirectory(directoryPath: string): string[]; + getAllFileStats(): { [filePath: string]: VirtualFileStats }; + getAllDirStats(): { [filePath: string]: VirtualDirStats }; +}; diff --git a/src/util/typescript-utils.ts b/src/util/typescript-utils.ts new file mode 100644 index 00000000..91520d23 --- /dev/null +++ b/src/util/typescript-utils.ts @@ -0,0 +1,26 @@ +import { createSourceFile, Node, ScriptTarget, SourceFile, SyntaxKind} from 'typescript'; + +export function getTypescriptSourceFile(filePath: string, fileContent: string, languageVersion: ScriptTarget, setParentNodes: boolean): SourceFile { + return createSourceFile(filePath, fileContent, languageVersion, setParentNodes); +} + +export function removeDecorators(fileName: string, source: string): string { + const sourceFile = createSourceFile(fileName, source, ScriptTarget.Latest); + const decorators = findNodes(sourceFile, sourceFile, SyntaxKind.Decorator, true); + decorators.sort((a, b) => b.pos - a.pos); + decorators.forEach(d => { + source = source.slice(0, d.pos) + source.slice(d.end); + }); + + return source; +} + +export function findNodes(sourceFile: SourceFile, node: Node, kind: SyntaxKind, keepGoing = false): Node[] { + if (node.kind === kind && !keepGoing) { + return [node]; + } + + return node.getChildren(sourceFile).reduce((result, n) => { + return result.concat(findNodes(sourceFile, n, kind, keepGoing)); + }, node.kind === kind ? [node] : []); +} diff --git a/src/util/virtual-file-utils.ts b/src/util/virtual-file-utils.ts new file mode 100644 index 00000000..9d509fcd --- /dev/null +++ b/src/util/virtual-file-utils.ts @@ -0,0 +1,67 @@ +import { Stats } from 'fs'; + +const dev = Math.floor(Math.random() * 10000); + +export class VirtualStats implements Stats { + protected _ctime = new Date(); + protected _mtime = new Date(); + protected _atime = new Date(); + protected _btime = new Date(); + protected _dev = dev; + protected _ino = Math.floor(Math.random() * 100000); + protected _mode = parseInt('777', 8); // RWX for everyone. + protected _uid = process.env['UID'] || 0; + protected _gid = process.env['GID'] || 0; + + constructor(protected _path: string) {} + + isFile() { return false; } + isDirectory() { return false; } + isBlockDevice() { return false; } + isCharacterDevice() { return false; } + isSymbolicLink() { return false; } + isFIFO() { return false; } + isSocket() { return false; } + + get dev() { return this._dev; } + get ino() { return this._ino; } + get mode() { return this._mode; } + get nlink() { return 1; } // Default to 1 hard link. + get uid() { return this._uid; } + get gid() { return this._gid; } + get rdev() { return 0; } + get size() { return 0; } + get blksize() { return 512; } + get blocks() { return Math.ceil(this.size / this.blksize); } + get atime() { return this._atime; } + get mtime() { return this._mtime; } + get ctime() { return this._ctime; } + get birthtime() { return this._btime; } +} + +export class VirtualDirStats extends VirtualStats { + constructor(_fileName: string) { + super(_fileName); + } + + isDirectory() { return true; } + + get size() { return 1024; } +} + +export class VirtualFileStats extends VirtualStats { + + constructor(_fileName: string, private _content: string) { + super(_fileName); + } + + get content() { return this._content; } + set content(v: string) { + this._content = v; + this._mtime = new Date(); + } + + isFile() { return true; } + + get size() { return this._content.length; } +} diff --git a/src/webpack.ts b/src/webpack.ts index a1b021cd..fdcef74a 100644 --- a/src/webpack.ts +++ b/src/webpack.ts @@ -1,7 +1,5 @@ -import { FileCache } from './util/file-cache'; -import { BuildContext, BuildState, ChangedFile, File, TaskInfo } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError, IgnorableError } from './util/errors'; -import { changeExtension, readFileAsync, setContext } from './util/helpers'; import { emit, EventType } from './util/events'; import { join } from 'path'; import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config'; @@ -29,8 +27,6 @@ export function webpack(context: BuildContext, configFile: string) { context = generateContext(context); configFile = getUserConfigFile(context, taskInfo, configFile); - Logger.debug('Webpack: Setting Context on shared singleton'); - setContext(context); const logger = new Logger('webpack'); return webpackWorker(context, configFile) diff --git a/src/webpack/hybrid-file-system.ts b/src/webpack/hybrid-file-system.ts deleted file mode 100644 index 73059397..00000000 --- a/src/webpack/hybrid-file-system.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FileCache } from '../util/file-cache'; - -export class HybridFileSystem implements FileSystem { - constructor(private fileCache: FileCache, private originalFileSystem: FileSystem) { - } - - isSync() { - return this.originalFileSystem.isSync(); - } - - stat(path: string, callback: Function): any { - return this.originalFileSystem.stat(path, callback); - } - - readdir(path: string, callback: Function): any { - return this.originalFileSystem.readdir(path, callback); - } - - readJson(path: string, callback: Function): any { - return this.originalFileSystem.readJson(path, callback); - } - - readlink(path: string, callback: Function): any { - return this.originalFileSystem.readlink(path, callback); - } - - purge(pathsToPurge: string[]): void { - if (this.fileCache) { - for (const path of pathsToPurge) { - this.fileCache.remove(path); - } - } - } - - readFile(path: string, callback: Function): any { - if (this.fileCache) { - const file = this.fileCache.get(path); - if (file) { - callback(null, new Buffer(file.content)); - return; - } - } - return this.originalFileSystem.readFile(path, callback); - } - -} - -export interface FileSystem { - isSync(): boolean; - stat(path: string, callback: Function): any; - readdir(path: string, callback: Function): any; - readFile(path: string, callback: Function): any; - readJson(path: string, callback: Function): any; - readlink(path: string, callback: Function): any; - purge(what: any): void; -}; diff --git a/src/webpack/ionic-environment-plugin.ts b/src/webpack/ionic-environment-plugin.ts index b721498a..9c6158f2 100644 --- a/src/webpack/ionic-environment-plugin.ts +++ b/src/webpack/ionic-environment-plugin.ts @@ -1,6 +1,6 @@ import { FileCache } from '../util/file-cache'; import { Logger } from '../logger/logger'; -import { HybridFileSystem } from './hybrid-file-system'; +import { getInstance } from '../util/hybrid-file-system-factory'; import { WatchMemorySystem } from './watch-memory-system'; export class IonicEnvironmentPlugin { @@ -10,14 +10,60 @@ export class IonicEnvironmentPlugin { apply(compiler: any) { compiler.plugin('environment', (otherCompiler: any, callback: Function) => { Logger.debug('[IonicEnvironmentPlugin] apply: creating environment plugin'); - const hybridFileSystem = new HybridFileSystem(this.fileCache, compiler.inputFileSystem); + const hybridFileSystem = getInstance(); + hybridFileSystem.setFileSystem(compiler.inputFileSystem); compiler.inputFileSystem = hybridFileSystem; compiler.resolvers.normal.fileSystem = compiler.inputFileSystem; compiler.resolvers.context.fileSystem = compiler.inputFileSystem; compiler.resolvers.loader.fileSystem = compiler.inputFileSystem; - - // TODO - we can set-up the output file system here for in-memory serving compiler.watchFileSystem = new WatchMemorySystem(this.fileCache); + + // do a bunch of webpack specific stuff here, so cast to an any + // populate the content of the file system with any virtual files + // inspired by populateWebpackResolver method in Angular's webpack plugin + const webpackFileSystem: any = hybridFileSystem; + const fileStatsDictionary = hybridFileSystem.getAllFileStats(); + const dirStatsDictionary = hybridFileSystem.getAllDirStats(); + + this.initializeWebpackFileSystemCaches(webpackFileSystem); + + for (const filePath of Object.keys(fileStatsDictionary)) { + const stats = fileStatsDictionary[filePath]; + webpackFileSystem._statStorage.data[filePath] = [null, stats]; + webpackFileSystem._readFileStorage.data[filePath] = [null, stats.content]; + } + + for (const dirPath of Object.keys(dirStatsDictionary)) { + const stats = dirStatsDictionary[dirPath]; + const fileNames = hybridFileSystem.getFileNamesInDirectory(dirPath); + const dirNames = hybridFileSystem.getSubDirs(dirPath); + webpackFileSystem._statStorage.data[dirPath] = [null, stats]; + webpackFileSystem._readdirStorage.data[dirPath] = [null, fileNames.concat(dirNames)]; + } + }); } + + private initializeWebpackFileSystemCaches(webpackFileSystem: any) { + if (!webpackFileSystem._statStorage) { + webpackFileSystem._statStorage = { }; + } + if (!webpackFileSystem._statStorage.data) { + webpackFileSystem._statStorage.data = []; + } + + if (!webpackFileSystem._readFileStorage) { + webpackFileSystem._readFileStorage = { }; + } + if (!webpackFileSystem._readFileStorage.data) { + webpackFileSystem._readFileStorage.data = []; + } + + if (!webpackFileSystem._readdirStorage) { + webpackFileSystem._readdirStorage = { }; + } + if (!webpackFileSystem._readdirStorage.data) { + webpackFileSystem._readdirStorage.data = []; + } + } } diff --git a/src/webpack/ionic-webpack-factory.ts b/src/webpack/ionic-webpack-factory.ts index 5cf02294..6d75aeab 100644 --- a/src/webpack/ionic-webpack-factory.ts +++ b/src/webpack/ionic-webpack-factory.ts @@ -10,4 +10,4 @@ export function getIonicEnvironmentPlugin() { export function getSourceMapperFunction(): Function { return provideCorrectSourcePath; -} \ No newline at end of file +} diff --git a/src/webpack/typescript-sourcemap-loader-disk.ts b/src/webpack/typescript-sourcemap-loader-disk.ts deleted file mode 100644 index 15f37445..00000000 --- a/src/webpack/typescript-sourcemap-loader-disk.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Logger } from '../logger/logger'; -import { changeExtension, readFileAsync } from '../util/helpers'; - -module.exports = function typescriptSourcemapLoaderDisk(source: string, map: string) { - this.cacheable(); - var callback = this.async(); - - if ( this.resourcePath.indexOf('node_modules') > -1) { - // it's not a source file, so use the default - callback(null, source, map); - } else { - // it's a src file - loadBetterSourceMap(this.resourcePath, callback, source, map); - } -}; - -function loadBetterSourceMap(javascriptFilePath: string, callback: Function, originalSource: any, originalMap: any) { - const sourceMapPath = javascriptFilePath + '.map'; - const tsFilePath = changeExtension(javascriptFilePath, '.ts'); - - let sourceMapContent: string = null; - let typescriptFileContent: string = null; - - const readSourceMapPromise = readFileAsync(sourceMapPath); - readSourceMapPromise.then((content: string) => { - sourceMapContent = content; - }); - - const readTsFilePromise = readFileAsync(tsFilePath); - readTsFilePromise.then((content: string) => { - typescriptFileContent = content; - }); - - let promises: Promise[] = []; - promises.push(readSourceMapPromise); - promises.push(readTsFilePromise); - - Promise.all(promises) - .then(() => { - if (!sourceMapContent || !sourceMapContent.length) { - throw new Error('Failed to read sourcemap file'); - } else if (!typescriptFileContent || !typescriptFileContent.length) { - throw new Error('Failed to read typescript file'); - } else { - return JSON.parse(sourceMapContent); - } - }).then((sourceMapObject: any) => { - if (!sourceMapObject.sourcesContent || sourceMapObject.sourcesContent.length === 0) { - Logger.debug(`loadBetterSourceMap: Assigning Typescript content to source map for ${javascriptFilePath}`); - sourceMapObject.sourcesContent = [typescriptFileContent]; - } - callback(null, originalSource, sourceMapObject); - }).catch((err: Error) => { - Logger.debug(`Failed to generate typescript sourcemaps for ${javascriptFilePath}: ${err.message}`); - // just use the default - callback(null, originalSource, originalMap); - }); - -} diff --git a/src/webpack/typescript-sourcemap-loader-memory.ts b/src/webpack/typescript-sourcemap-loader-memory.ts index 6293fea8..1ba0de25 100644 --- a/src/webpack/typescript-sourcemap-loader-memory.ts +++ b/src/webpack/typescript-sourcemap-loader-memory.ts @@ -1,27 +1,56 @@ import { normalize, resolve } from 'path'; -import { changeExtension, getContext} from '../util/helpers'; +import { changeExtension, getContext, readFileAsync} from '../util/helpers'; +import { File } from '../util/interfaces'; +import { FileCache } from '../util/file-cache'; module.exports = function typescriptSourcemapLoaderMemory(source: string, map: any) { this.cacheable(); var callback = this.async(); const context = getContext(); - const absolutePath = resolve(normalize(this.resourcePath)); + const absolutePath = resolve(normalize(this.resourcePath)); const javascriptPath = changeExtension(this.resourcePath, '.js'); - const javascriptFile = context.fileCache.get(javascriptPath); const sourceMapPath = javascriptPath + '.map'; - const sourceMapFile = context.fileCache.get(sourceMapPath); - let sourceMapObject = map; - if (sourceMapFile) { - sourceMapObject = JSON.parse(sourceMapFile.content); - sourceMapObject.sources = [absolutePath]; - if (!sourceMapObject.sourcesContent || sourceMapObject.sourcesContent.length === 0) { - sourceMapObject.sourcesContent = [source]; - } - } + let javascriptFile: File = null; + let mapFile: File = null; - callback(null, javascriptFile.content, sourceMapObject); + const promises: Promise[] = []; + let readJavascriptFilePromise = readFile(context.fileCache, javascriptPath); + promises.push(readJavascriptFilePromise); + readJavascriptFilePromise.then(file => { + javascriptFile = file; + }); + let readJavascriptMapFilePromise = readFile(context.fileCache, sourceMapPath); + promises.push(readJavascriptMapFilePromise); + readJavascriptMapFilePromise.then(file => { + mapFile = file; + }); + + Promise.all(promises).then(() => { + let sourceMapObject = map; + if (mapFile) { + sourceMapObject = JSON.parse(mapFile.content); + sourceMapObject.sources = [absolutePath]; + if (!sourceMapObject.sourcesContent || sourceMapObject.sourcesContent.length === 0) { + sourceMapObject.sourcesContent = [source]; + } + } + callback(null, javascriptFile.content, sourceMapObject); + }); }; +function readFile(fileCache: FileCache, filePath: string) { + let file = fileCache.get(filePath); + if (file) { + return Promise.resolve(file); + } + return readFileAsync(filePath).then((fileContent: string) => { + const file = { path: filePath, content: fileContent} + fileCache.set(filePath, file); + return file; + }).catch(err => { + return null; + }); +} \ No newline at end of file