feat(compiler-cli): private i18n API for the CLI (#13536)

Also change the Extractor API to align with the Codegen API (internal APIs)
This commit is contained in:
Victor Berchet 2016-12-19 11:56:10 -08:00 committed by Chuck Jazdzewski
parent 0e3981afc1
commit 6b65fc1286
6 changed files with 145 additions and 46 deletions

View File

@ -1,5 +1,5 @@
<div> <div>
<h1>hello world</h1> <h1 i18n>hello world</h1>
<a [routerLink]="['lazy']">lazy</a> <a [routerLink]="['lazy']">lazy</a>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>

View File

@ -19,7 +19,6 @@ import {AngularCompilerOptions, CodeGenerator, CompilerHostContext, NodeCompiler
const glob = require('glob'); const glob = require('glob');
/** /**
* Main method. * Main method.
* Standalone program that executes codegen using the ngtools API and tests that files were * Standalone program that executes codegen using the ngtools API and tests that files were
@ -30,6 +29,7 @@ function main() {
Promise.resolve() Promise.resolve()
.then(() => codeGenTest()) .then(() => codeGenTest())
.then(() => i18nTest())
.then(() => lazyRoutesTest()) .then(() => lazyRoutesTest())
.then(() => { .then(() => {
console.log('All done!'); console.log('All done!');
@ -42,7 +42,6 @@ function main() {
}); });
} }
function codeGenTest() { function codeGenTest() {
const basePath = path.join(__dirname, '../ngtools_src'); const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json'); const project = path.join(basePath, 'tsconfig-build.json');
@ -109,6 +108,67 @@ function codeGenTest() {
}); });
} }
function i18nTest() {
const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json');
const readResources: string[] = [];
const wroteFiles: string[] = [];
const config = tsc.readConfiguration(project, basePath);
const hostContext = new NodeCompilerHostContext();
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
const host: ts.CompilerHost = Object.assign(
{}, delegateHost,
{writeFile: (fileName: string, ...rest: any[]) => { wroteFiles.push(fileName); }});
const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host);
config.ngOptions.basePath = basePath;
console.log(`>>> running i18n extraction for ${project}`);
return __NGTOOLS_PRIVATE_API_2
.extractI18n({
basePath,
compilerOptions: config.parsed.options, program, host,
angularCompilerOptions: config.ngOptions,
i18nFormat: 'xlf',
readResource: (fileName: string) => {
readResources.push(fileName);
return hostContext.readResource(fileName);
},
})
.then(() => {
console.log(`>>> i18n extraction done, asserting read and wrote files`);
const allFiles = glob.sync(path.join(basePath, '**/*'), {nodir: true});
assert(wroteFiles.length == 1, `Expected a single message bundle file.`);
assert(
wroteFiles[0].endsWith('/ngtools_src/messages.xlf'),
`Expected the bundle file to be "message.xlf".`);
allFiles.forEach((fileName: string) => {
// Skip tsconfig.
if (fileName.match(/tsconfig-build.json$/)) {
return;
}
// Assert that file was read.
if (fileName.match(/\.css$/) || fileName.match(/\.html$/)) {
assert(
readResources.indexOf(fileName) != -1,
`Expected resource "${fileName}" to be read.`);
}
});
console.log(`done, no errors.`);
})
.catch((e: any) => {
console.error(e.stack);
console.error('Extraction failed');
throw e;
});
}
function lazyRoutesTest() { function lazyRoutesTest() {
const basePath = path.join(__dirname, '../ngtools_src'); const basePath = path.join(__dirname, '../ngtools_src');

View File

@ -14,41 +14,15 @@
// Must be imported first, because angular2 decorators throws on load. // Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata'; import 'reflect-metadata';
import * as compiler from '@angular/compiler';
import * as tsc from '@angular/tsc-wrapped'; import * as tsc from '@angular/tsc-wrapped';
import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Extractor} from './extractor'; import {Extractor} from './extractor';
function extract( function extract(
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions, ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions,
program: ts.Program, host: ts.CompilerHost) { program: ts.Program, host: ts.CompilerHost): Promise<void> {
const extractor = Extractor.create(ngOptions, cliOptions.i18nFormat, program, host); return Extractor.create(ngOptions, program, host).extract(cliOptions.i18nFormat);
const bundlePromise: Promise<compiler.MessageBundle> = extractor.extract();
return (bundlePromise).then(messageBundle => {
let ext: string;
let serializer: compiler.Serializer;
const format = (cliOptions.i18nFormat || 'xlf').toLowerCase();
switch (format) {
case 'xmb':
ext = 'xmb';
serializer = new compiler.Xmb();
break;
case 'xliff':
case 'xlf':
default:
ext = 'xlf';
serializer = new compiler.Xliff();
break;
}
const dstPath = path.join(ngOptions.genDir, `messages.${ext}`);
host.writeFile(dstPath, messageBundle.write(serializer), false);
});
} }
// Entry point // Entry point

View File

@ -15,27 +15,70 @@ import 'reflect-metadata';
import * as compiler from '@angular/compiler'; import * as compiler from '@angular/compiler';
import * as tsc from '@angular/tsc-wrapped'; import * as tsc from '@angular/tsc-wrapped';
import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompilerHost, ModuleResolutionHostAdapter} from './compiler_host'; import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host';
import {PathMappedCompilerHost} from './path_mapped_compiler_host';
export class Extractor { export class Extractor {
constructor( constructor(
private ngExtractor: compiler.Extractor, private ngCompilerHost: CompilerHost, private options: tsc.AngularCompilerOptions, private ngExtractor: compiler.Extractor,
public host: ts.CompilerHost, private ngCompilerHost: CompilerHost,
private program: ts.Program) {} private program: ts.Program) {}
extract(): Promise<compiler.MessageBundle> { extract(formatName: string): Promise<void> {
return this.ngExtractor.extract(this.program.getSourceFiles().map( // Checks the format and returns the extension
sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName))); const ext = this.getExtension(formatName);
const files = this.program.getSourceFiles().map(
sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName));
const promiseBundle = this.ngExtractor.extract(files);
return promiseBundle.then(bundle => {
const content = this.serialize(bundle, ext);
const dstPath = path.join(this.options.genDir, `messages.${ext}`);
this.host.writeFile(dstPath, content, false);
});
}
serialize(bundle: compiler.MessageBundle, ext: string): string {
let serializer: compiler.Serializer;
switch (ext) {
case 'xmb':
serializer = new compiler.Xmb();
break;
case 'xlf':
default:
serializer = new compiler.Xliff();
}
return bundle.write(serializer);
}
getExtension(formatName: string): string {
const format = (formatName || 'xlf').toLowerCase();
if (format === 'xmb') return 'xmb';
if (format === 'xlf' || format === 'xlif') return 'xlf';
throw new Error('Unsupported format "${formatName}"');
} }
static create( static create(
options: tsc.AngularCompilerOptions, translationsFormat: string, program: ts.Program, options: tsc.AngularCompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost,
moduleResolverHost: ts.ModuleResolutionHost, ngCompilerHost?: CompilerHost): Extractor { compilerHostContext?: CompilerHostContext, ngCompilerHost?: CompilerHost): Extractor {
if (!ngCompilerHost) if (!ngCompilerHost) {
ngCompilerHost = const usePathMapping = !!options.rootDirs && options.rootDirs.length > 0;
new CompilerHost(program, options, new ModuleResolutionHostAdapter(moduleResolverHost)); const context = compilerHostContext || new ModuleResolutionHostAdapter(tsCompilerHost);
ngCompilerHost = usePathMapping ? new PathMappedCompilerHost(program, options, context) :
new CompilerHost(program, options, context);
}
const {extractor: ngExtractor} = compiler.Extractor.create(ngCompilerHost); const {extractor: ngExtractor} = compiler.Extractor.create(ngCompilerHost);
return new Extractor(ngExtractor, ngCompilerHost, program);
return new Extractor(options, ngExtractor, tsCompilerHost, ngCompilerHost, program);
} }
} }

View File

@ -19,10 +19,10 @@ import * as ts from 'typescript';
import {CodeGenerator} from './codegen'; import {CodeGenerator} from './codegen';
import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host'; import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host';
import {Extractor} from './extractor';
import {listLazyRoutesOfModule} from './ngtools_impl'; import {listLazyRoutesOfModule} from './ngtools_impl';
import {PathMappedCompilerHost} from './path_mapped_compiler_host'; import {PathMappedCompilerHost} from './path_mapped_compiler_host';
export interface NgTools_InternalApi_NG2_CodeGen_Options { export interface NgTools_InternalApi_NG2_CodeGen_Options {
basePath: string; basePath: string;
compilerOptions: ts.CompilerOptions; compilerOptions: ts.CompilerOptions;
@ -50,9 +50,18 @@ export interface NgTools_InternalApi_NG2_ListLazyRoutes_Options {
// Every new property under this line should be optional. // Every new property under this line should be optional.
} }
export interface NgTools_InternalApi_NG_2_LazyRouteMap { [route: string]: string; } export interface NgTools_InternalApi_NG_2_LazyRouteMap { [route: string]: string; }
export interface NgTools_InternalApi_NG2_ExtractI18n_Options {
basePath: string;
compilerOptions: ts.CompilerOptions;
program: ts.Program;
host: ts.CompilerHost;
angularCompilerOptions: AngularCompilerOptions;
i18nFormat: string;
readResource: (fileName: string) => Promise<string>;
// Every new property under this line should be optional.
}
/** /**
* A ModuleResolutionHostAdapter that overrides the readResource() method with the one * A ModuleResolutionHostAdapter that overrides the readResource() method with the one
@ -94,7 +103,6 @@ export class NgTools_InternalApi_NG_2 {
return codeGenerator.codegen(); return codeGenerator.codegen();
} }
/** /**
* @internal * @internal
* @private * @private
@ -124,4 +132,19 @@ export class NgTools_InternalApi_NG_2 {
}, },
{}); {});
} }
/**
* @internal
* @private
*/
static extractI18n(options: NgTools_InternalApi_NG2_ExtractI18n_Options): Promise<void> {
const hostContext: CompilerHostContext =
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
// Create the i18n extractor.
const extractor = Extractor.create(
options.angularCompilerOptions, options.program, options.host, hostContext);
return extractor.extract(options.i18nFormat);
}
} }

View File

@ -28,7 +28,6 @@ import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {NgModuleResolver} from '../ng_module_resolver'; import {NgModuleResolver} from '../ng_module_resolver';
import {ParseError} from '../parse_util'; import {ParseError} from '../parse_util';
import {PipeResolver} from '../pipe_resolver'; import {PipeResolver} from '../pipe_resolver';
import {Console} from '../private_import_core';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {createOfflineCompileUrlResolver} from '../url_resolver'; import {createOfflineCompileUrlResolver} from '../url_resolver';