From bf94f878bcc84f16a78cd700e8be24f67d059604 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 12 Sep 2017 15:53:17 -0700 Subject: [PATCH] refactor(compiler): use new ngc for i18n (#19095) This also changes ngc to support all tsc command line arguments. PR Close #19095 --- .../integrationtest/test/i18n_spec.ts | 7 +- packages/compiler-cli/src/codegen.ts | 27 ++- packages/compiler-cli/src/extract_i18n.ts | 42 ++-- packages/compiler-cli/src/extractor.ts | 53 +--- packages/compiler-cli/src/main.ts | 124 ++++++---- packages/compiler-cli/src/ngtools_api.ts | 5 +- packages/compiler-cli/src/perform_compile.ts | 44 ++-- packages/compiler-cli/src/perform_watch.ts | 20 +- .../compiler-cli/src/transformers/program.ts | 89 ++++++- .../compiler-cli/test/extract_i18n_spec.ts | 226 ++++++++++++++++++ packages/compiler-cli/test/main_spec.ts | 156 +----------- packages/compiler-cli/test/ngc_spec.ts | 157 +++++++++++- packages/compiler/src/aot/compiler.ts | 34 +++ 13 files changed, 650 insertions(+), 334 deletions(-) create mode 100644 packages/compiler-cli/test/extract_i18n_spec.ts diff --git a/packages/compiler-cli/integrationtest/test/i18n_spec.ts b/packages/compiler-cli/integrationtest/test/i18n_spec.ts index dde96ecbe1..549b5744ff 100644 --- a/packages/compiler-cli/integrationtest/test/i18n_spec.ts +++ b/packages/compiler-cli/integrationtest/test/i18n_spec.ts @@ -114,8 +114,7 @@ multi-lines `; describe('template i18n extraction output', () => { - const outDir = ''; - const genDir = 'out'; + const outDir = 'out'; it('should extract i18n messages as xmb', () => { const xmbOutput = path.join(outDir, 'custom_file.xmb'); @@ -139,7 +138,7 @@ describe('template i18n extraction output', () => { }); it('should not emit js', () => { - const genOutput = path.join(genDir, ''); - expect(fs.existsSync(genOutput)).toBeFalsy(); + const files = fs.readdirSync(outDir); + files.forEach(f => expect(f).not.toMatch(/\.js$/)); }); }); diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index 59cb9b2ec6..3b7a7ca985 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -29,6 +29,13 @@ const PREAMBLE = `/** `; +export interface CodeGeneratorI18nOptions { + i18nFormat: string|null; + i18nFile: string|null; + locale: string|null; + missingTranslation: string|null; +} + export class CodeGenerator { constructor( private options: AngularCompilerOptions, private program: ts.Program, @@ -60,7 +67,7 @@ export class CodeGenerator { } static create( - options: AngularCompilerOptions, cliOptions: NgcCliOptions, program: ts.Program, + options: AngularCompilerOptions, i18nOptions: CodeGeneratorI18nOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost, compilerHostContext?: CompilerHostContext, ngCompilerHost?: CompilerHost): CodeGenerator { if (!ngCompilerHost) { @@ -70,16 +77,16 @@ export class CodeGenerator { new CompilerHost(program, options, context); } let transContent: string = ''; - if (cliOptions.i18nFile) { - if (!cliOptions.locale) { + if (i18nOptions.i18nFile) { + if (!i18nOptions.locale) { throw new Error( - `The translation file (${cliOptions.i18nFile}) locale must be provided. Use the --locale option.`); + `The translation file (${i18nOptions.i18nFile}) locale must be provided. Use the --locale option.`); } - transContent = readFileSync(cliOptions.i18nFile, 'utf8'); + transContent = readFileSync(i18nOptions.i18nFile, 'utf8'); } let missingTranslation = compiler.core.MissingTranslationStrategy.Warning; - if (cliOptions.missingTranslation) { - switch (cliOptions.missingTranslation) { + if (i18nOptions.missingTranslation) { + switch (i18nOptions.missingTranslation) { case 'error': missingTranslation = compiler.core.MissingTranslationStrategy.Error; break; @@ -91,7 +98,7 @@ export class CodeGenerator { break; default: throw new Error( - `Unknown option for missingTranslation (${cliOptions.missingTranslation}). Use either error, warning or ignore.`); + `Unknown option for missingTranslation (${i18nOptions.missingTranslation}). Use either error, warning or ignore.`); } } if (!transContent) { @@ -99,8 +106,8 @@ export class CodeGenerator { } const {compiler: aotCompiler} = compiler.createAotCompiler(ngCompilerHost, { translations: transContent, - i18nFormat: cliOptions.i18nFormat || undefined, - locale: cliOptions.locale || undefined, missingTranslation, + i18nFormat: i18nOptions.i18nFormat || undefined, + locale: i18nOptions.locale || undefined, missingTranslation, enableLegacyTemplate: options.enableLegacyTemplate === true, enableSummariesForJit: options.enableSummariesForJit !== false, preserveWhitespaces: options.preserveWhitespaces, diff --git a/packages/compiler-cli/src/extract_i18n.ts b/packages/compiler-cli/src/extract_i18n.ts index d5886e1b92..67867515e8 100644 --- a/packages/compiler-cli/src/extract_i18n.ts +++ b/packages/compiler-cli/src/extract_i18n.ts @@ -13,29 +13,33 @@ */ // Must be imported first, because Angular decorators throw on load. import 'reflect-metadata'; +import * as api from './transformers/api'; +import {ParsedConfiguration} from './perform_compile'; +import {mainSync, readCommandLineAndConfiguration} from './main'; -import * as tsc from '@angular/tsc-wrapped'; -import * as ts from 'typescript'; +export function main(args: string[], consoleError: (msg: string) => void = console.error): number { + const config = readXi18nCommandLineAndConfiguration(args); + return mainSync(args, consoleError, config); +} -import {Extractor} from './extractor'; +function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration { + const options: api.CompilerOptions = {}; + const parsedArgs = require('minimist')(args); + if (parsedArgs.outFile) options.i18nOutFile = parsedArgs.outFile; + if (parsedArgs.i18nFormat) options.i18nOutFormat = parsedArgs.i18nFormat; + if (parsedArgs.locale) options.i18nOutLocale = parsedArgs.locale; -function extract( - ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions, - program: ts.Program, host: ts.CompilerHost) { - return Extractor.create(ngOptions, program, host, cliOptions.locale) - .extract(cliOptions.i18nFormat !, cliOptions.outFile); + const config = readCommandLineAndConfiguration(args, options, [ + 'outFile', + 'i18nFormat', + 'locale', + ]); + // only emit the i18nBundle but nothing else. + return {...config, emitFlags: api.EmitFlags.I18nBundle}; } // Entry point if (require.main === module) { - const args = require('minimist')(process.argv.slice(2)); - const project = args.p || args.project || '.'; - const cliOptions = new tsc.I18nExtractionCliOptions(args); - tsc.main(project, cliOptions, extract, {noEmit: true}) - .then((exitCode: any) => process.exit(exitCode)) - .catch((e: any) => { - console.error(e.stack); - console.error('Extraction failed'); - process.exit(1); - }); -} + const args = process.argv.slice(2); + process.exitCode = main(args); +} \ No newline at end of file diff --git a/packages/compiler-cli/src/extractor.ts b/packages/compiler-cli/src/extractor.ts index e87a370d76..e4b23e607a 100644 --- a/packages/compiler-cli/src/extractor.ts +++ b/packages/compiler-cli/src/extractor.ts @@ -20,6 +20,7 @@ import * as ts from 'typescript'; import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host'; import {PathMappedCompilerHost} from './path_mapped_compiler_host'; +import {i18nExtract, i18nGetExtension, i18nSerialize} from './transformers/program'; export class Extractor { constructor( @@ -28,18 +29,8 @@ export class Extractor { private program: ts.Program) {} extract(formatName: string, outFile: string|null): Promise { - // Checks the format and returns the extension - const ext = this.getExtension(formatName); - - const promiseBundle = this.extractBundle(); - - return promiseBundle.then(bundle => { - const content = this.serialize(bundle, formatName); - const dstFile = outFile || `messages.${ext}`; - const dstPath = path.join(this.options.genDir !, dstFile); - this.host.writeFile(dstPath, content, false); - return [dstPath]; - }); + return this.extractBundle().then( + bundle => i18nExtract(formatName, outFile, this.host, this.options, bundle)); } extractBundle(): Promise { @@ -50,44 +41,10 @@ export class Extractor { } serialize(bundle: compiler.MessageBundle, formatName: string): string { - const format = formatName.toLowerCase(); - let serializer: compiler.Serializer; - - switch (format) { - case 'xmb': - serializer = new compiler.Xmb(); - break; - case 'xliff2': - case 'xlf2': - serializer = new compiler.Xliff2(); - break; - case 'xlf': - case 'xliff': - default: - serializer = new compiler.Xliff(); - } - return bundle.write( - serializer, (sourcePath: string) => this.options.basePath ? - path.relative(this.options.basePath, sourcePath) : - sourcePath); + return i18nSerialize(bundle, formatName, this.options); } - getExtension(formatName: string): string { - const format = (formatName || 'xlf').toLowerCase(); - - switch (format) { - case 'xmb': - return 'xmb'; - case 'xlf': - case 'xlif': - case 'xliff': - case 'xlf2': - case 'xliff2': - return 'xlf'; - } - - throw new Error(`Unsupported format "${formatName}"`); - } + getExtension(formatName: string): string { return i18nGetExtension(formatName); } static create( options: tsc.AngularCompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost, diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 1b00ae298e..50bc5cac69 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -18,24 +18,25 @@ import * as tsickle from 'tsickle'; import * as api from './transformers/api'; import * as ngc from './transformers/entry_points'; -import {calcProjectFileAndBasePath, exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile'; +import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile'; import {performWatchCompilation, createPerformWatchHost} from './perform_watch'; import {isSyntaxError} from '@angular/compiler'; import {CodeGenerator} from './codegen'; +// TODO(tbosch): remove this old entrypoint once we drop `disableTransformerPipeline`. export function main( args: string[], consoleError: (s: string) => void = console.error): Promise { - const parsedArgs = require('minimist')(args); - if (parsedArgs.w || parsedArgs.watch) { - const result = watchMode(parsedArgs, consoleError); - return Promise.resolve(exitCodeFromResult(result.firstCompileResult)); - } - const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs); + let {project, rootNames, options, errors: configErrors, watch} = + readNgcCommandLineAndConfiguration(args); if (configErrors.length) { return Promise.resolve(reportErrorsAndExit(options, configErrors, consoleError)); } + if (watch) { + const result = watchMode(project, options, consoleError); + return Promise.resolve(reportErrorsAndExit({}, result.firstCompileResult, consoleError)); + } if (options.disableTransformerPipeline) { - return disabledTransformerPipelineNgcMain(parsedArgs, consoleError); + return disabledTransformerPipelineNgcMain(args, consoleError); } const {diagnostics: compileDiags} = performCompilation({rootNames, options, emitCallback: createEmitCallback(options)}); @@ -43,14 +44,19 @@ export function main( } export function mainSync( - args: string[], consoleError: (s: string) => void = console.error): number { - const parsedArgs = require('minimist')(args); - const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs); + args: string[], consoleError: (s: string) => void = console.error, + config?: NgcParsedConfiguration): number { + let {project, rootNames, options, errors: configErrors, watch, emitFlags} = + config || readNgcCommandLineAndConfiguration(args); if (configErrors.length) { return reportErrorsAndExit(options, configErrors, consoleError); } - const {diagnostics: compileDiags} = - performCompilation({rootNames, options, emitCallback: createEmitCallback(options)}); + if (watch) { + const result = watchMode(project, options, consoleError); + return reportErrorsAndExit({}, result.firstCompileResult, consoleError); + } + const {diagnostics: compileDiags} = performCompilation( + {rootNames, options, emitFlags, emitCallback: createEmitCallback(options)}); return reportErrorsAndExit(options, compileDiags, consoleError); } @@ -85,58 +91,80 @@ function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback { }); } -function projectOf(args: any): string { - return (args && (args.p || args.project)) || '.'; +export interface NgcParsedConfiguration extends ParsedConfiguration { watch?: boolean; } + +function readNgcCommandLineAndConfiguration(args: string[]): NgcParsedConfiguration { + const options: api.CompilerOptions = {}; + const parsedArgs = require('minimist')(args); + if (parsedArgs.i18nFile) options.i18nInFile = parsedArgs.i18nFile; + if (parsedArgs.i18nFormat) options.i18nInFormat = parsedArgs.i18nFormat; + if (parsedArgs.locale) options.i18nInLocale = parsedArgs.locale; + const mt = parsedArgs.missingTranslation; + if (mt === 'error' || mt === 'warning' || mt === 'ignore') { + options.i18nInMissingTranslations = mt; + } + const config = readCommandLineAndConfiguration( + args, options, ['i18nFile', 'i18nFormat', 'locale', 'missingTranslation', 'watch']); + const watch = parsedArgs.w || parsedArgs.watch; + return {...config, watch}; } -function readCommandLineAndConfiguration(args: any): ParsedConfiguration { - const project = projectOf(args); +export function readCommandLineAndConfiguration( + args: string[], existingOptions: api.CompilerOptions = {}, + ngCmdLineOptions: string[] = []): ParsedConfiguration { + let cmdConfig = ts.parseCommandLine(args); + const project = cmdConfig.options.project || '.'; + const cmdErrors = cmdConfig.errors.filter(e => { + if (typeof e.messageText === 'string') { + const msg = e.messageText; + return !ngCmdLineOptions.some(o => msg.indexOf(o) >= 0); + } + return true; + }); + if (cmdErrors.length) { + return { + project, + rootNames: [], + options: cmdConfig.options, + errors: cmdErrors, + emitFlags: api.EmitFlags.Default + }; + } const allDiagnostics: Diagnostics = []; - const config = readConfiguration(project); - const options = mergeCommandLineParams(args, config.options); + const config = readConfiguration(project, cmdConfig.options); + const options = {...config.options, ...existingOptions}; if (options.locale) { options.i18nInLocale = options.locale; } - return {project, rootNames: config.rootNames, options, errors: config.errors}; + return { + project, + rootNames: config.rootNames, options, + errors: config.errors, + emitFlags: config.emitFlags + }; } function reportErrorsAndExit( options: api.CompilerOptions, allDiagnostics: Diagnostics, consoleError: (s: string) => void = console.error): number { - const exitCode = allDiagnostics.some(d => d.category === ts.DiagnosticCategory.Error) ? 1 : 0; if (allDiagnostics.length) { consoleError(formatDiagnostics(options, allDiagnostics)); } - return exitCode; + return exitCodeFromResult(allDiagnostics); } -export function watchMode(args: any, consoleError: (s: string) => void) { - const project = projectOf(args); - const {projectFile, basePath} = calcProjectFileAndBasePath(project); - const config = readConfiguration(project); - return performWatchCompilation(createPerformWatchHost(projectFile, diagnostics => { - consoleError(formatDiagnostics(config.options, diagnostics)); - }, options => createEmitCallback(options))); -} - -function mergeCommandLineParams( - cliArgs: {[k: string]: string}, options: api.CompilerOptions): api.CompilerOptions { - // TODO: also merge in tsc command line parameters by calling - // ts.readCommandLine. - if (cliArgs.i18nFile) options.i18nInFile = cliArgs.i18nFile; - if (cliArgs.i18nFormat) options.i18nInFormat = cliArgs.i18nFormat; - if (cliArgs.locale) options.i18nInLocale = cliArgs.locale; - const mt = cliArgs.missingTranslation; - if (mt === 'error' || mt === 'warning' || mt === 'ignore') { - options.i18nInMissingTranslations = mt; - } - return options; +export function watchMode( + project: string, options: api.CompilerOptions, consoleError: (s: string) => void) { + return performWatchCompilation(createPerformWatchHost(project, diagnostics => { + consoleError(formatDiagnostics(options, diagnostics)); + }, options, options => createEmitCallback(options))); } function disabledTransformerPipelineNgcMain( - args: any, consoleError: (s: string) => void = console.error): Promise { - const cliOptions = new tsc.NgcCliOptions(args); - const project = args.p || args.project || '.'; + args: string[], consoleError: (s: string) => void = console.error): Promise { + const parsedArgs = require('minimist')(args); + const cliOptions = new tsc.NgcCliOptions(parsedArgs); + const project = parsedArgs.p || parsedArgs.project || '.'; return tsc.main(project, cliOptions, disabledTransformerPipelineCodegen) .then(() => 0) .catch(e => { @@ -150,7 +178,7 @@ function disabledTransformerPipelineNgcMain( } function disabledTransformerPipelineCodegen( - ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program, + ngOptions: api.CompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program, host: ts.CompilerHost) { if (ngOptions.enableSummariesForJit === undefined) { // default to false @@ -162,5 +190,5 @@ function disabledTransformerPipelineCodegen( // CLI entry point if (require.main === module) { const args = process.argv.slice(2); - main(args).then((exitCode: number) => process.exitCode = exitCode); + process.exitCode = mainSync(args); } diff --git a/packages/compiler-cli/src/ngtools_api.ts b/packages/compiler-cli/src/ngtools_api.ts index 66f7921505..eff9d22509 100644 --- a/packages/compiler-cli/src/ngtools_api.ts +++ b/packages/compiler-cli/src/ngtools_api.ts @@ -90,12 +90,11 @@ export class NgTools_InternalApi_NG_2 { const hostContext: CompilerHostContext = new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host); - const cliOptions: NgcCliOptions = { + const i18nOptions = { i18nFormat: options.i18nFormat !, i18nFile: options.i18nFile !, locale: options.locale !, missingTranslation: options.missingTranslation !, - basePath: options.basePath }; const ngOptions = options.angularCompilerOptions; if (ngOptions.enableSummariesForJit === undefined) { @@ -105,7 +104,7 @@ export class NgTools_InternalApi_NG_2 { // Create the Code Generator. const codeGenerator = - CodeGenerator.create(ngOptions, cliOptions, options.program, options.host, hostContext); + CodeGenerator.create(ngOptions, i18nOptions, options.program, options.host, hostContext); return codeGenerator.codegen(); } diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index e22aa04463..65f4d1560a 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -7,7 +7,6 @@ */ import {isSyntaxError, syntaxError} from '@angular/compiler'; -import {createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; @@ -57,6 +56,7 @@ export interface ParsedConfiguration { project: string; options: api.CompilerOptions; rootNames: string[]; + emitFlags: api.EmitFlags; errors: Diagnostics; } @@ -82,7 +82,13 @@ export function readConfiguration( let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); if (error) { - return {project, errors: [error], rootNames: [], options: {}}; + return { + project, + errors: [error], + rootNames: [], + options: {}, + emitFlags: api.EmitFlags.Default + }; } const parseConfigHost = { useCaseSensitiveFileNames: true, @@ -95,7 +101,11 @@ export function readConfiguration( const rootNames = parsed.fileNames.map(f => path.normalize(f)); const options = createNgCompilerOptions(basePath, config, parsed.options); - return {project: projectFile, rootNames, options, errors: parsed.errors}; + let emitFlags = api.EmitFlags.Default; + if (!(options.skipMetadataEmit || options.flatModuleOutFile)) { + emitFlags |= api.EmitFlags.Metadata; + } + return {project: projectFile, rootNames, options, errors: parsed.errors, emitFlags}; } catch (e) { const errors: Diagnostics = [{ category: ts.DiagnosticCategory.Error, @@ -103,7 +113,7 @@ export function readConfiguration( source: api.SOURCE, code: api.UNKNOWN_ERROR_CODE }]; - return {project: '', errors, rootNames: [], options: {}}; + return {project: '', errors, rootNames: [], options: {}, emitFlags: api.EmitFlags.Default}; } } @@ -113,32 +123,27 @@ export interface PerformCompilationResult { emitResult?: ts.EmitResult; } -export function exitCodeFromResult(result: PerformCompilationResult | undefined): number { - if (!result) { - // If we didn't get a result we should return failure. - return 1; - } - if (!result.diagnostics || result.diagnostics.length === 0) { +export function exitCodeFromResult(diags: Diagnostics | undefined): number { + if (!diags || diags.length === 0) { // If we have a result and didn't get any errors, we succeeded. return 0; } // Return 2 if any of the errors were unknown. - return result.diagnostics.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? - 2 : - 1; + return diags.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1; } export function performCompilation({rootNames, options, host, oldProgram, emitCallback, gatherDiagnostics = defaultGatherDiagnostics, - customTransformers}: { + customTransformers, emitFlags = api.EmitFlags.Default}: { rootNames: string[], options: api.CompilerOptions, host?: api.CompilerHost, oldProgram?: api.Program, emitCallback?: api.TsEmitCallback, gatherDiagnostics?: (program: api.Program) => Diagnostics, - customTransformers?: api.CustomTransformers + customTransformers?: api.CustomTransformers, + emitFlags?: api.EmitFlags }): PerformCompilationResult { const [major, minor] = ts.version.split('.'); @@ -159,12 +164,7 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa allDiagnostics.push(...gatherDiagnostics(program !)); if (!hasErrors(allDiagnostics)) { - emitResult = program !.emit({ - emitCallback, - customTransformers, - emitFlags: api.EmitFlags.Default | - ((options.skipMetadataEmit || options.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) - }); + emitResult = program !.emit({emitCallback, customTransformers, emitFlags}); allDiagnostics.push(...emitResult.diagnostics); return {diagnostics: allDiagnostics, program, emitResult}; } @@ -223,4 +223,4 @@ function defaultGatherDiagnostics(program: api.Program): Diagnostics { function hasErrors(diags: Diagnostics) { return diags.some(d => d.category === ts.DiagnosticCategory.Error); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index 40f7735f72..1e64e89525 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -53,14 +53,15 @@ export interface PerformWatchHost { export function createPerformWatchHost( configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void, + existingOptions?: ts.CompilerOptions, createEmitCallback?: (options: api.CompilerOptions) => api.TsEmitCallback): PerformWatchHost { return { reportDiagnostics: reportDiagnostics, createCompilerHost: options => createCompilerHost({options}), - readConfiguration: () => readConfiguration(configFileName), + readConfiguration: () => readConfiguration(configFileName, existingOptions), createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined, onFileChange: (listeners) => { - const parsed = readConfiguration(configFileName); + const parsed = readConfiguration(configFileName, existingOptions); function stubReady(cb: () => void) { process.nextTick(cb); } if (parsed.errors && parsed.errors.length) { reportDiagnostics(parsed.errors); @@ -104,11 +105,8 @@ export function createPerformWatchHost( /** * The logic in this function is adapted from `tsc.ts` from TypeScript. */ -export function performWatchCompilation(host: PerformWatchHost): { - close: () => void, - ready: (cb: () => void) => void, - firstCompileResult: PerformCompilationResult | undefined -} { +export function performWatchCompilation(host: PerformWatchHost): + {close: () => void, ready: (cb: () => void) => void, firstCompileResult: Diagnostics} { let cachedProgram: api.Program|undefined; // Program cached from last compilation let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost cached from last compilation let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation @@ -133,13 +131,13 @@ export function performWatchCompilation(host: PerformWatchHost): { } // Invoked to perform initial compilation or re-compilation in watch mode - function doCompilation() { + function doCompilation(): Diagnostics { if (!cachedOptions) { cachedOptions = host.readConfiguration(); } if (cachedOptions.errors && cachedOptions.errors.length) { host.reportDiagnostics(cachedOptions.errors); - return; + return cachedOptions.errors; } if (!cachedCompilerHost) { // TODO(chuckj): consider avoiding re-generating factories for libraries. @@ -168,7 +166,7 @@ export function performWatchCompilation(host: PerformWatchHost): { host.reportDiagnostics(compileResult.diagnostics); } - const exitCode = exitCodeFromResult(compileResult); + const exitCode = exitCodeFromResult(compileResult.diagnostics); if (exitCode == 0) { cachedProgram = compileResult.program; host.reportDiagnostics([ChangeDiagnostics.Compilation_complete_Watching_for_file_changes]); @@ -176,7 +174,7 @@ export function performWatchCompilation(host: PerformWatchHost): { host.reportDiagnostics([ChangeDiagnostics.Compilation_failed_Watching_for_file_changes]); } - return compileResult; + return compileResult.diagnostics; } function resetOptions() { diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 3cbc53fbbc..3d686a37f8 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, NgAnalyzedModules, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; +import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, MessageBundle, NgAnalyzedModules, Serializer, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; import {createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; @@ -56,7 +56,7 @@ class AngularCompilerProgram implements Program { constructor( private rootNames: string[], private options: CompilerOptions, private host: CompilerHost, oldProgram?: Program) { - if (options.flatModuleOutFile && !options.skipMetadataEmit) { + if (options.flatModuleOutFile) { const {host: bundleHost, indexName, errors} = createBundleIndexHost(options, rootNames, host); if (errors) { // TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli, @@ -141,17 +141,27 @@ class AngularCompilerProgram implements Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback }): ts.EmitResult { - return emitCallback({ - program: this.programWithStubs, - host: this.host, - options: this.options, - targetSourceFile: undefined, - writeFile: - createWriteFileCallback(emitFlags, this.host, this.metadataCache, this.generatedFiles), - cancellationToken, - emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, - customTransformers: this.calculateTransforms(customTransformers) - }); + if (emitFlags & EmitFlags.I18nBundle) { + const locale = this.options.i18nOutLocale || null; + const file = this.options.i18nOutFile || null; + const format = this.options.i18nOutFormat || null; + const bundle = this.compiler.emitMessageBundle(this.analyzedModules, locale); + i18nExtract(format, file, this.host, this.options, bundle); + } + if (emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) { + return emitCallback({ + program: this.programWithStubs, + host: this.host, + options: this.options, + targetSourceFile: undefined, + writeFile: + createWriteFileCallback(emitFlags, this.host, this.metadataCache, this.generatedFiles), + cancellationToken, + emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, + customTransformers: this.calculateTransforms(customTransformers) + }); + } + return {emitSkipped: true, diagnostics: [], emittedFiles: []}; } // Private members @@ -506,3 +516,56 @@ function createProgramWithStubsHost( this.generatedFiles.has(fileName) || originalHost.fileExists(fileName); }; } + +export function i18nExtract( + formatName: string | null, outFile: string | null, host: ts.CompilerHost, + options: CompilerOptions, bundle: MessageBundle): string[] { + formatName = formatName || 'null'; + // Checks the format and returns the extension + const ext = i18nGetExtension(formatName); + const content = i18nSerialize(bundle, formatName, options); + const dstFile = outFile || `messages.${ext}`; + const dstPath = path.resolve(options.outDir || options.basePath, dstFile); + host.writeFile(dstPath, content, false); + return [dstPath]; +} + +export function i18nSerialize( + bundle: MessageBundle, formatName: string, options: CompilerOptions): string { + const format = formatName.toLowerCase(); + let serializer: Serializer; + + switch (format) { + case 'xmb': + serializer = new Xmb(); + break; + case 'xliff2': + case 'xlf2': + serializer = new Xliff2(); + break; + case 'xlf': + case 'xliff': + default: + serializer = new Xliff(); + } + return bundle.write( + serializer, (sourcePath: string) => + options.basePath ? path.relative(options.basePath, sourcePath) : sourcePath); +} + +export function i18nGetExtension(formatName: string): string { + const format = (formatName || 'xlf').toLowerCase(); + + switch (format) { + case 'xmb': + return 'xmb'; + case 'xlf': + case 'xlif': + case 'xliff': + case 'xlf2': + case 'xliff2': + return 'xlf'; + } + + throw new Error(`Unsupported format "${formatName}"`); +} diff --git a/packages/compiler-cli/test/extract_i18n_spec.ts b/packages/compiler-cli/test/extract_i18n_spec.ts new file mode 100644 index 0000000000..ff2477b8c8 --- /dev/null +++ b/packages/compiler-cli/test/extract_i18n_spec.ts @@ -0,0 +1,226 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import {main} from '../src/extract_i18n'; + +function getNgRootDir() { + const moduleFilename = module.filename.replace(/\\/g, '/'); + const distIndex = moduleFilename.indexOf('/dist/all'); + return moduleFilename.substr(0, distIndex); +} + +const EXPECTED_XMB = ` + + + + + + + + + + + + + + + + + + + +]> + + src/module.ts:1translate me + src/module.ts:2Welcome + +`; + +const EXPECTED_XLIFF = ` + + + + + translate me + + src/module.ts + 1 + + desc + meaning + + + Welcome + + src/module.ts + 2 + + + + + +`; + +const EXPECTED_XLIFF2 = ` + + + + + desc + meaning + src/module.ts:1 + + + translate me + + + + + src/module.ts:2 + + + Welcome + + + + +`; + +describe('extract_i18n command line', () => { + let basePath: string; + let outDir: string; + let write: (fileName: string, content: string) => void; + let errorSpy: jasmine.Spy&((s: string) => void); + + function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') { + write('tsconfig.json', tsconfig); + } + + beforeEach(() => { + errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); + basePath = makeTempDir(); + write = (fileName: string, content: string) => { + const dir = path.dirname(fileName); + if (dir != '.') { + const newDir = path.join(basePath, dir); + if (!fs.existsSync(newDir)) fs.mkdirSync(newDir); + } + fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); + }; + write('tsconfig-base.json', `{ + "compilerOptions": { + "experimentalDecorators": true, + "skipLibCheck": true, + "noImplicitAny": true, + "types": [], + "outDir": "built", + "rootDir": ".", + "baseUrl": ".", + "declaration": true, + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "lib": ["es6", "dom"], + "typeRoots": ["node_modules/@types"] + } + }`); + outDir = path.resolve(basePath, 'built'); + const ngRootDir = getNgRootDir(); + const nodeModulesPath = path.resolve(basePath, 'node_modules'); + fs.mkdirSync(nodeModulesPath); + fs.symlinkSync( + path.resolve(ngRootDir, 'dist', 'all', '@angular'), + path.resolve(nodeModulesPath, '@angular')); + fs.symlinkSync( + path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs')); + }); + + function writeSources() { + write('src/basic.html', [ + `
`, + `

Welcome

`, + ].join('\n')); + write('src/module.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'basic', + templateUrl: './basic.html', + }) + export class BasicCmp {} + + @NgModule({ + declarations: [BasicCmp] + }) + export class I18nModule {} + `); + } + + it('should extract xmb', () => { + writeConfig(); + writeSources(); + + const exitCode = + main(['-p', basePath, '--i18nFormat=xmb', '--outFile=custom_file.xmb'], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const xmbOutput = path.join(outDir, 'custom_file.xmb'); + expect(fs.existsSync(xmbOutput)).toBeTruthy(); + const xmb = fs.readFileSync(xmbOutput, {encoding: 'utf-8'}); + expect(xmb).toEqual(EXPECTED_XMB); + }); + + it('should extract xlf', () => { + writeConfig(); + writeSources(); + + const exitCode = main(['-p', basePath, '--i18nFormat=xlf', '--locale=fr'], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const xlfOutput = path.join(outDir, 'messages.xlf'); + expect(fs.existsSync(xlfOutput)).toBeTruthy(); + const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'}); + expect(xlf).toEqual(EXPECTED_XLIFF); + }); + + it('should extract xlf2', () => { + writeConfig(); + writeSources(); + + const exitCode = + main(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const xlfOutput = path.join(outDir, 'messages.xliff2.xlf'); + expect(fs.existsSync(xlfOutput)).toBeTruthy(); + const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'}); + expect(xlf).toEqual(EXPECTED_XLIFF2); + }); + + it('should not emit js', () => { + writeConfig(); + writeSources(); + + const exitCode = + main(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const moduleOutput = path.join(outDir, 'src', 'module.js'); + expect(fs.existsSync(moduleOutput)).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/test/main_spec.ts b/packages/compiler-cli/test/main_spec.ts index 281e21bd95..1243d05c9c 100644 --- a/packages/compiler-cli/test/main_spec.ts +++ b/packages/compiler-cli/test/main_spec.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main, watchMode} from '../src/main'; +import {main} from '../src/main'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -163,7 +163,7 @@ describe('compiler-cli with disableTransformerPipeline', () => { expect(errorSpy).toHaveBeenCalled(); expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); - expect(exitCode).toEqual(1); + expect(exitCode).toEqual(2); done(); }) .catch(e => done.fail(e)); @@ -310,156 +310,4 @@ describe('compiler-cli with disableTransformerPipeline', () => { .catch(e => done.fail(e)); }); }); - - describe('watch mode', () => { - let timer: (() => void)|undefined = undefined; - let results: ((message: string) => void)|undefined = undefined; - let originalTimeout: number; - - function trigger() { - const delay = 1000; - setTimeout(() => { - const t = timer; - timer = undefined; - if (!t) { - fail('Unexpected state. Timer was not set.'); - } else { - t(); - } - }, delay); - } - - function whenResults(): Promise { - return new Promise(resolve => { - results = message => { - resolve(message); - results = undefined; - }; - }); - } - - function errorSpy(message: string): void { - if (results) results(message); - } - - beforeEach(() => { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - const timerToken = 100; - spyOn(ts.sys, 'setTimeout').and.callFake((callback: () => void) => { - timer = callback; - return timerToken; - }); - spyOn(ts.sys, 'clearTimeout').and.callFake((token: number) => { - if (token == timerToken) { - timer = undefined; - } - }); - - write('greet.html', `

Hello {{name}}!

`); - write('greet.css', `p.greeting { color: #eee }`); - write('greet.ts', ` - import {Component, Input} from '@angular/core'; - - @Component({ - selector: 'greet', - templateUrl: 'greet.html', - styleUrls: ['greet.css'] - }) - export class Greet { - @Input() - name: string; - } - `); - - write('app.ts', ` - import {Component} from '@angular/core' - - @Component({ - selector: 'my-app', - template: \` -
- -
- \`, - }) - export class App { - name:string; - constructor() { - this.name = \`Angular!\` - } - }`); - - write('module.ts', ` - import {NgModule} from '@angular/core'; - import {Greet} from './greet'; - import {App} from './app'; - - @NgModule({ - declarations: [Greet, App] - }) - export class MyModule {} - `); - }); - - afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); - - function writeAppConfig(location: string) { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "compilerOptions": { - "outDir": "${location}" - } - }`); - } - - function expectRecompile(cb: () => void) { - return (done: DoneFn) => { - writeAppConfig('dist'); - const compile = watchMode({p: basePath}, errorSpy); - - return new Promise(resolve => { - compile.ready(() => { - cb(); - - // Allow the watch callbacks to occur and trigger the timer. - trigger(); - - // Expect the file to trigger a result. - whenResults().then(message => { - expect(message).toMatch(/File change detected/); - compile.close(); - done(); - resolve(); - }); - }); - }); - }; - } - - it('should recompile when config file changes', expectRecompile(() => writeAppConfig('dist2'))); - - it('should recompile when a ts file changes', expectRecompile(() => { - write('greet.ts', ` - import {Component, Input} from '@angular/core'; - - @Component({ - selector: 'greet', - templateUrl: 'greet.html', - styleUrls: ['greet.css'], - }) - export class Greet { - @Input() - name: string; - age: number; - } - `); - })); - - it('should recomiple when the html file changes', - expectRecompile(() => { write('greet.html', '

Hello {{name}} again!

'); })); - - it('should recompile when the css file changes', - expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); })); - }); }); diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 78d052e82e..6479c4a019 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {mainSync} from '../src/main'; +import {mainSync, readCommandLineAndConfiguration, watchMode} from '../src/main'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -163,7 +163,7 @@ describe('ngc transformer command-line', () => { expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); - expect(exitCode).toEqual(1); + expect(exitCode).toEqual(2); }); it('should report errors for ngfactory files that are not referenced by root files', () => { @@ -914,4 +914,157 @@ describe('ngc transformer command-line', () => { shouldExist('app/main.js'); }); }); + + describe('watch mode', () => { + let timer: (() => void)|undefined = undefined; + let results: ((message: string) => void)|undefined = undefined; + let originalTimeout: number; + + function trigger() { + const delay = 1000; + setTimeout(() => { + const t = timer; + timer = undefined; + if (!t) { + fail('Unexpected state. Timer was not set.'); + } else { + t(); + } + }, delay); + } + + function whenResults(): Promise { + return new Promise(resolve => { + results = message => { + resolve(message); + results = undefined; + }; + }); + } + + function errorSpy(message: string): void { + if (results) results(message); + } + + beforeEach(() => { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + const timerToken = 100; + spyOn(ts.sys, 'setTimeout').and.callFake((callback: () => void) => { + timer = callback; + return timerToken; + }); + spyOn(ts.sys, 'clearTimeout').and.callFake((token: number) => { + if (token == timerToken) { + timer = undefined; + } + }); + + write('greet.html', `

Hello {{name}}!

`); + write('greet.css', `p.greeting { color: #eee }`); + write('greet.ts', ` + import {Component, Input} from '@angular/core'; + + @Component({ + selector: 'greet', + templateUrl: 'greet.html', + styleUrls: ['greet.css'] + }) + export class Greet { + @Input() + name: string; + } + `); + + write('app.ts', ` + import {Component} from '@angular/core' + + @Component({ + selector: 'my-app', + template: \` +
+ +
+ \`, + }) + export class App { + name:string; + constructor() { + this.name = \`Angular!\` + } + }`); + + write('module.ts', ` + import {NgModule} from '@angular/core'; + import {Greet} from './greet'; + import {App} from './app'; + + @NgModule({ + declarations: [Greet, App] + }) + export class MyModule {} + `); + }); + + afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); + + function writeAppConfig(location: string) { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "outDir": "${location}" + } + }`); + } + + function expectRecompile(cb: () => void) { + return (done: DoneFn) => { + writeAppConfig('dist'); + const config = readCommandLineAndConfiguration(['-p', basePath]); + const compile = watchMode(config.project, config.options, errorSpy); + + return new Promise(resolve => { + compile.ready(() => { + cb(); + + // Allow the watch callbacks to occur and trigger the timer. + trigger(); + + // Expect the file to trigger a result. + whenResults().then(message => { + expect(message).toMatch(/File change detected/); + compile.close(); + done(); + resolve(); + }); + }); + }); + }; + } + + it('should recompile when config file changes', expectRecompile(() => writeAppConfig('dist2'))); + + it('should recompile when a ts file changes', expectRecompile(() => { + write('greet.ts', ` + import {Component, Input} from '@angular/core'; + + @Component({ + selector: 'greet', + templateUrl: 'greet.html', + styleUrls: ['greet.css'], + }) + export class Greet { + @Input() + name: string; + age: number; + } + `); + })); + + it('should recomiple when the html file changes', + expectRecompile(() => { write('greet.html', '

Hello {{name}} again!

'); })); + + it('should recompile when the css file changes', + expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); })); + }); }); diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index b3e6a6cb5a..98ec4b3cdb 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -8,11 +8,15 @@ import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; +import {MessageBundle} from '../i18n/message_bundle'; import {Identifiers, createTokenForExternalReference} from '../identifiers'; import {CompileMetadataResolver} from '../metadata_resolver'; +import {HtmlParser} from '../ml_parser/html_parser'; +import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; +import {ParseError} from '../parse_util'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; import {TemplateParser} from '../template_parser/template_parser'; @@ -77,6 +81,36 @@ export class AotCompiler { return flatten(sourceModules); } + emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle { + const errors: ParseError[] = []; + const htmlParser = new HtmlParser(); + + // TODO(vicb): implicit tags & attributes + const messageBundle = new MessageBundle(htmlParser, [], {}, locale); + + analyzeResult.files.forEach(file => { + const compMetas: CompileDirectiveMetadata[] = []; + file.directives.forEach(directiveType => { + const dirMeta = this._metadataResolver.getDirectiveMetadata(directiveType); + if (dirMeta && dirMeta.isComponent) { + compMetas.push(dirMeta); + } + }); + compMetas.forEach(compMeta => { + const html = compMeta.template !.template !; + const interpolationConfig = + InterpolationConfig.fromArray(compMeta.template !.interpolation); + errors.push(...messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig) !); + }); + }); + + if (errors.length) { + throw new Error(errors.map(e => e.toString()).join('\n')); + } + + return messageBundle; + } + private _compileStubFile( srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[]): GeneratedFile[] {