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:
parent
c8f742e288
commit
bf94f878bc
|
@ -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$/));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}"`);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 }`); }));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }`); }));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[] {
|
||||
|
|
Loading…
Reference in New Issue