refactor(compiler): use new ngc for i18n (#19095)

This also changes ngc to support all tsc command line arguments.
PR Close #19095
This commit is contained in:
Tobias Bosch 2017-09-12 15:53:17 -07:00 committed by Matias Niemelä
parent c8f742e288
commit bf94f878bc
13 changed files with 650 additions and 334 deletions

View File

@ -114,8 +114,7 @@ multi-lines</source>
`;
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$/));
});
});

View File

@ -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,

View File

@ -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);
}

View File

@ -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<string[]> {
// 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<compiler.MessageBundle> {
@ -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,

View File

@ -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<number> {
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<number> {
const cliOptions = new tsc.NgcCliOptions(args);
const project = args.p || args.project || '.';
args: string[], consoleError: (s: string) => void = console.error): Promise<number> {
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);
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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}"`);
}

View File

@ -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 = `<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE messagebundle [
<!ELEMENT messagebundle (msg)*>
<!ATTLIST messagebundle class CDATA #IMPLIED>
<!ELEMENT msg (#PCDATA|ph|source)*>
<!ATTLIST msg id CDATA #IMPLIED>
<!ATTLIST msg seq CDATA #IMPLIED>
<!ATTLIST msg name CDATA #IMPLIED>
<!ATTLIST msg desc CDATA #IMPLIED>
<!ATTLIST msg meaning CDATA #IMPLIED>
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
<!ATTLIST msg xml:space (default|preserve) "default">
<!ATTLIST msg is_hidden CDATA #IMPLIED>
<!ELEMENT source (#PCDATA)>
<!ELEMENT ph (#PCDATA|ex)*>
<!ATTLIST ph name CDATA #REQUIRED>
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="8136548302122759730" desc="desc" meaning="meaning"><source>src/module.ts:1</source>translate me</msg>
<msg id="3492007542396725315"><source>src/module.ts:2</source>Welcome</msg>
</messagebundle>
`;
const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="fr" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" datatype="html">
<source>translate me</source>
<context-group purpose="location">
<context context-type="sourcefile">src/module.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">desc</note>
<note priority="1" from="meaning">meaning</note>
</trans-unit>
<trans-unit id="65cc4ab3b4c438e07c89be2b677d08369fb62da2" datatype="html">
<source>Welcome</source>
<context-group purpose="location">
<context context-type="sourcefile">src/module.ts</context>
<context context-type="linenumber">2</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
`;
const EXPECTED_XLIFF2 = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en">
<file original="ng.template" id="ngi18n">
<unit id="8136548302122759730">
<notes>
<note category="description">desc</note>
<note category="meaning">meaning</note>
<note category="location">src/module.ts:1</note>
</notes>
<segment>
<source>translate me</source>
</segment>
</unit>
<unit id="3492007542396725315">
<notes>
<note category="location">src/module.ts:2</note>
</notes>
<segment>
<source>Welcome</source>
</segment>
</unit>
</file>
</xliff>
`;
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', [
`<div title="translate me" i18n-title="meaning|desc"></div>`,
`<p id="welcomeMessage"><!--i18n-->Welcome<!--/i18n--></p>`,
].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();
});
});

View File

@ -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<string> {
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', `<p class="greeting"> Hello {{name}}!</p>`);
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: \`
<div>
<greet [name]='name'></greet>
</div>
\`,
})
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', '<p> Hello {{name}} again!</p>'); }));
it('should recompile when the css file changes',
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); }));
});
});

View File

@ -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<string> {
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', `<p class="greeting"> Hello {{name}}!</p>`);
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: \`
<div>
<greet [name]='name'></greet>
</div>
\`,
})
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', '<p> Hello {{name}} again!</p>'); }));
it('should recompile when the css file changes',
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); }));
});
});

View File

@ -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[] {