We now create 2 programs with exactly the same fileNames and
exactly the same `import` / `export` declarations,
allowing TS to reuse the structure of first program
completely. When passing in an oldProgram and the files didn’t change,
TS can also reuse the old program completely.
This is possible buy adding generated files to TS
in `host.geSourceFile` via `ts.SourceFile.referencedFiles`.
This commit also:
- has a minor side effect on how we generate shared stylesheets:
- previously every import in a stylesheet would generate a new
`.ngstyles.ts` file.
- now, we only generate 1 `.ngstyles.ts` file per entry in `@Component.styleUrls`.
This was required as we need to be able to determine the program files
without loading the resources (which can be async).
- makes all angular related methods in `CompilerHost`
optional, allowing to just use a regular `ts.CompilerHost` as `CompilerHost`.
- simplifies the logic around `Compiler.analyzeNgModules` by introducing `NgAnalyzedFile`.
Perf impact: 1.5s improvement in compiling angular io
PR Close #19275
526 lines
20 KiB
TypeScript
526 lines
20 KiB
TypeScript
/**
|
|
* @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 {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Serializer, StubEmitFlags, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError} from '@angular/compiler';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
|
|
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
|
|
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
|
|
|
|
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
|
import {TsCompilerAotCompilerTypeCheckHostAdapter} from './compiler_host';
|
|
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
|
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
|
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from './util';
|
|
|
|
const emptyModules: NgAnalyzedModules = {
|
|
ngModules: [],
|
|
ngModuleByPipeOrDirective: new Map(),
|
|
files: []
|
|
};
|
|
|
|
const defaultEmitCallback: TsEmitCallback =
|
|
({program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles,
|
|
customTransformers}) =>
|
|
program.emit(
|
|
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
|
|
|
|
|
|
class AngularCompilerProgram implements Program {
|
|
private metadataCache: LowerMetadataCache;
|
|
// Lazily initialized fields
|
|
private _typeCheckHost: TypeCheckHost;
|
|
private _compiler: AotCompiler;
|
|
private _tsProgram: ts.Program;
|
|
private _analyzedModules: NgAnalyzedModules|undefined;
|
|
private _structuralDiagnostics: Diagnostic[]|undefined;
|
|
private _programWithStubs: ts.Program|undefined;
|
|
private _generatedFiles: GeneratedFile[]|undefined;
|
|
private _generatedFileDiagnostics: Diagnostic[]|undefined;
|
|
private _semanticDiagnostics: {ts: ts.Diagnostic[], ng: Diagnostic[]}|undefined;
|
|
private _optionsDiagnostics: Diagnostic[] = [];
|
|
|
|
constructor(
|
|
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
|
|
private oldProgram?: Program) {
|
|
const [major, minor] = ts.version.split('.');
|
|
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
|
|
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
|
|
}
|
|
|
|
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
|
|
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,
|
|
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
|
|
this._optionsDiagnostics.push(...errors.map(e => ({
|
|
category: e.category,
|
|
messageText: e.messageText as string,
|
|
source: SOURCE,
|
|
code: DEFAULT_ERROR_CODE
|
|
})));
|
|
} else {
|
|
rootNames.push(indexName !);
|
|
this.host = bundleHost;
|
|
}
|
|
}
|
|
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
|
|
}
|
|
|
|
getTsProgram(): ts.Program { return this.tsProgram; }
|
|
|
|
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
|
|
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
|
|
}
|
|
|
|
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
|
|
return [...this._optionsDiagnostics, ...getNgOptionDiagnostics(this.options)];
|
|
}
|
|
|
|
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
|
ts.Diagnostic[] {
|
|
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
|
|
}
|
|
|
|
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
|
|
return this.structuralDiagnostics;
|
|
}
|
|
|
|
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
|
ts.Diagnostic[] {
|
|
return this.semanticDiagnostics.ts;
|
|
}
|
|
|
|
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
|
|
Diagnostic[] {
|
|
const compilerDiagnostics = this.generatedFileDiagnostics;
|
|
|
|
// If we have diagnostics during the parser phase the type check phase is not meaningful so skip
|
|
// it.
|
|
if (compilerDiagnostics && compilerDiagnostics.length) return compilerDiagnostics;
|
|
return this.semanticDiagnostics.ng;
|
|
}
|
|
|
|
loadNgStructureAsync(): Promise<void> {
|
|
if (this._analyzedModules) {
|
|
throw new Error('Angular structure already loaded');
|
|
}
|
|
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
|
|
return this._compiler.loadFilesAsync(analyzedFiles)
|
|
.catch(this.catchAnalysisError.bind(this))
|
|
.then(analyzedModules => {
|
|
if (this._analyzedModules) {
|
|
throw new Error('Angular structure loaded both synchronously and asynchronsly');
|
|
}
|
|
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
|
|
});
|
|
}
|
|
|
|
emit(
|
|
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
|
|
emitCallback = defaultEmitCallback}: {
|
|
emitFlags?: EmitFlags,
|
|
cancellationToken?: ts.CancellationToken,
|
|
customTransformers?: CustomTransformers,
|
|
emitCallback?: TsEmitCallback
|
|
} = {}): ts.EmitResult {
|
|
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);
|
|
}
|
|
const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = [];
|
|
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) ===
|
|
0) {
|
|
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
|
|
}
|
|
const emitResult = emitCallback({
|
|
program: this.tsProgram,
|
|
host: this.host,
|
|
options: this.options,
|
|
targetSourceFile: undefined,
|
|
writeFile: createWriteFileCallback(this.options, this.host, outSrcMapping), cancellationToken,
|
|
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
|
|
customTransformers: this.calculateTransforms(customTransformers)
|
|
});
|
|
|
|
if (!outSrcMapping.length) {
|
|
// if no files were emitted by TypeScript, also don't emit .json files
|
|
return emitResult;
|
|
}
|
|
|
|
const srcToOutPath = this.createSrcToOutPathMapper(outSrcMapping);
|
|
if (!this.options.skipTemplateCodegen) {
|
|
this.generatedFiles.forEach(gf => {
|
|
if (gf.source) {
|
|
this.host.writeFile(srcToOutPath(gf.genFileUrl), gf.source, false);
|
|
}
|
|
});
|
|
}
|
|
if (emitFlags & EmitFlags.Metadata) {
|
|
this.tsProgram.getSourceFiles().forEach(sf => {
|
|
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
|
|
const metadata = this.metadataCache.getMetadata(sf);
|
|
const metadataText = JSON.stringify([metadata]);
|
|
this.host.writeFile(
|
|
srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')), metadataText, false);
|
|
}
|
|
});
|
|
}
|
|
return emitResult;
|
|
}
|
|
|
|
// Private members
|
|
private get compiler(): AotCompiler {
|
|
if (!this._compiler) {
|
|
this.initSync();
|
|
}
|
|
return this._compiler !;
|
|
}
|
|
|
|
private get analyzedModules(): NgAnalyzedModules {
|
|
if (!this._analyzedModules) {
|
|
this.initSync();
|
|
}
|
|
return this._analyzedModules !;
|
|
}
|
|
|
|
private get structuralDiagnostics(): Diagnostic[] {
|
|
if (!this._structuralDiagnostics) {
|
|
this.initSync();
|
|
}
|
|
return this._structuralDiagnostics !;
|
|
}
|
|
|
|
private get tsProgram(): ts.Program {
|
|
if (!this._tsProgram) {
|
|
this.initSync();
|
|
}
|
|
return this._tsProgram !;
|
|
}
|
|
|
|
private get typeCheckHost(): TypeCheckHost {
|
|
if (!this._typeCheckHost) {
|
|
this.initSync();
|
|
}
|
|
return this._typeCheckHost !;
|
|
}
|
|
|
|
private get generatedFiles(): GeneratedFile[] {
|
|
if (!this._generatedFiles) {
|
|
this.generateFiles();
|
|
}
|
|
return this._generatedFiles !;
|
|
}
|
|
|
|
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
|
|
if (!this._generatedFileDiagnostics) {
|
|
this.generateFiles();
|
|
}
|
|
return this._generatedFileDiagnostics !;
|
|
}
|
|
|
|
private get semanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
|
|
return this._semanticDiagnostics ||
|
|
(this._semanticDiagnostics = this.generateSemanticDiagnostics());
|
|
}
|
|
|
|
private calculateTransforms(customTransformers?: CustomTransformers): ts.CustomTransformers {
|
|
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
|
if (!this.options.disableExpressionLowering) {
|
|
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache));
|
|
}
|
|
beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles));
|
|
if (customTransformers && customTransformers.beforeTs) {
|
|
beforeTs.push(...customTransformers.beforeTs);
|
|
}
|
|
const afterTs = customTransformers ? customTransformers.afterTs : undefined;
|
|
return {before: beforeTs, after: afterTs};
|
|
}
|
|
|
|
private createSrcToOutPathMapper(outSrcMappings:
|
|
Array<{sourceFile: ts.SourceFile, outFileName: string}>):
|
|
(srcFileName: string) => string {
|
|
let srcToOutPath: (srcFileName: string) => string;
|
|
if (this.options.outDir) {
|
|
// TODO(tbosch): talk to TypeScript team to expose their logic for calculating the `rootDir`
|
|
// if none was specified.
|
|
if (outSrcMappings.length === 0) {
|
|
throw new Error(`Can't calculate the rootDir without at least one outSrcMapping. `);
|
|
}
|
|
const firstEntry = outSrcMappings[0];
|
|
const entrySrcDir = path.dirname(firstEntry.sourceFile.fileName);
|
|
const entryOutDir = path.dirname(firstEntry.outFileName);
|
|
const commonSuffix = longestCommonSuffix(entrySrcDir, entryOutDir);
|
|
const rootDir = entrySrcDir.substring(0, entrySrcDir.length - commonSuffix.length);
|
|
srcToOutPath = (srcFileName) =>
|
|
path.resolve(this.options.outDir, path.relative(rootDir, srcFileName));
|
|
} else {
|
|
srcToOutPath = (srcFileName) => srcFileName;
|
|
}
|
|
return srcToOutPath;
|
|
}
|
|
|
|
private initSync() {
|
|
if (this._analyzedModules) {
|
|
return;
|
|
}
|
|
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
|
|
let analyzedModules: NgAnalyzedModules;
|
|
try {
|
|
analyzedModules = this._compiler.loadFilesSync(analyzedFiles);
|
|
} catch (e) {
|
|
analyzedModules = this.catchAnalysisError(e);
|
|
}
|
|
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
|
|
}
|
|
|
|
private _createProgramWithBasicStubs(): {
|
|
tmpProgram: ts.Program,
|
|
analyzedFiles: NgAnalyzedFile[],
|
|
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter
|
|
} {
|
|
if (this._analyzedModules) {
|
|
throw new Error(`Internal Error: already initalized!`);
|
|
}
|
|
const analyzedFiles: NgAnalyzedFile[] = [];
|
|
const codegen = (fileName: string) => {
|
|
if (this._analyzedModules) {
|
|
throw new Error(`Internal Error: already initalized!`);
|
|
}
|
|
const analyzedFile = this._compiler.analyzeFile(fileName);
|
|
analyzedFiles.push(analyzedFile);
|
|
const debug = fileName.endsWith('application_ref.ts');
|
|
return this._compiler.emitBasicStubs(analyzedFile);
|
|
};
|
|
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
|
|
this.rootNames, this.options, this.host, this.metadataCache, codegen);
|
|
const aotOptions = getAotCompilerOptions(this.options);
|
|
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
|
|
this._typeCheckHost = hostAdapter;
|
|
this._structuralDiagnostics = [];
|
|
|
|
const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined;
|
|
// Note: This is important to not produce a memory leak!
|
|
this.oldProgram = undefined;
|
|
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
|
|
return {tmpProgram, analyzedFiles, hostAdapter};
|
|
}
|
|
|
|
private _updateProgramWithTypeCheckStubs(
|
|
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
|
|
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter) {
|
|
this._analyzedModules = analyzedModules;
|
|
const genFiles = this._compiler.emitTypeCheckStubs(analyzedModules);
|
|
genFiles.forEach(gf => hostAdapter.updateGeneratedFile(gf));
|
|
this._tsProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, tmpProgram);
|
|
// Note: the new ts program should be completely reusable by TypeScript as:
|
|
// - we cache all the files in the hostAdapter
|
|
// - new new stubs use the exactly same imports/exports as the old once (we assert that in
|
|
// hostAdapter.updateGeneratedFile).
|
|
if (tsStructureIsReused(tmpProgram) !== StructureIsReused.Completely) {
|
|
throw new Error(`Internal Error: The structure of the program changed during codegen.`);
|
|
}
|
|
}
|
|
|
|
private catchAnalysisError(e: any): NgAnalyzedModules {
|
|
if (isSyntaxError(e)) {
|
|
const parserErrors = getParseErrors(e);
|
|
if (parserErrors && parserErrors.length) {
|
|
this._structuralDiagnostics =
|
|
parserErrors.map<Diagnostic>(e => ({
|
|
messageText: e.contextualMessage(),
|
|
category: ts.DiagnosticCategory.Error,
|
|
span: e.span,
|
|
source: SOURCE,
|
|
code: DEFAULT_ERROR_CODE
|
|
}));
|
|
} else {
|
|
this._structuralDiagnostics = [{
|
|
messageText: e.message,
|
|
category: ts.DiagnosticCategory.Error,
|
|
source: SOURCE,
|
|
code: DEFAULT_ERROR_CODE
|
|
}];
|
|
}
|
|
return emptyModules;
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
private generateFiles() {
|
|
const diags: Diagnostic[] = this._generatedFileDiagnostics = [];
|
|
try {
|
|
this._generatedFiles = this.compiler.emitAllImpls(this.analyzedModules);
|
|
} catch (e) {
|
|
this._generatedFiles = [];
|
|
if (isSyntaxError(e)) {
|
|
diags.push({
|
|
messageText: e.message,
|
|
category: ts.DiagnosticCategory.Error,
|
|
source: SOURCE,
|
|
code: DEFAULT_ERROR_CODE
|
|
});
|
|
return [];
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private generateSemanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
|
|
return translateDiagnostics(this.typeCheckHost, this.tsProgram.getSemanticDiagnostics());
|
|
}
|
|
}
|
|
|
|
export function createProgram(
|
|
{rootNames, options, host, oldProgram}:
|
|
{rootNames: string[], options: CompilerOptions, host: CompilerHost, oldProgram?: Program}):
|
|
Program {
|
|
return new AngularCompilerProgram(rootNames, options, host, oldProgram);
|
|
}
|
|
|
|
// Compute the AotCompiler options
|
|
function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
|
|
let missingTranslation = core.MissingTranslationStrategy.Warning;
|
|
|
|
switch (options.i18nInMissingTranslations) {
|
|
case 'ignore':
|
|
missingTranslation = core.MissingTranslationStrategy.Ignore;
|
|
break;
|
|
case 'error':
|
|
missingTranslation = core.MissingTranslationStrategy.Error;
|
|
break;
|
|
}
|
|
|
|
let translations: string = '';
|
|
|
|
if (options.i18nInFile) {
|
|
if (!options.i18nInLocale) {
|
|
throw new Error(`The translation file (${options.i18nInFile}) locale must be provided.`);
|
|
}
|
|
translations = fs.readFileSync(options.i18nInFile, 'utf8');
|
|
} else {
|
|
// No translations are provided, ignore any errors
|
|
// We still go through i18n to remove i18n attributes
|
|
missingTranslation = core.MissingTranslationStrategy.Ignore;
|
|
}
|
|
|
|
return {
|
|
locale: options.i18nInLocale,
|
|
i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation,
|
|
enableLegacyTemplate: options.enableLegacyTemplate,
|
|
enableSummariesForJit: true,
|
|
preserveWhitespaces: options.preserveWhitespaces,
|
|
fullTemplateTypeCheck: options.fullTemplateTypeCheck,
|
|
};
|
|
}
|
|
|
|
function createWriteFileCallback(
|
|
options: {skipTemplateCodegen?: boolean}, host: ts.CompilerHost,
|
|
outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}>) {
|
|
return (fileName: string, data: string, writeByteOrderMark: boolean,
|
|
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
|
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
|
|
const isGenerated = GENERATED_FILES.test(fileName);
|
|
if (sourceFile) {
|
|
outSrcMapping.push({outFileName: fileName, sourceFile});
|
|
}
|
|
if (isGenerated && options.skipTemplateCodegen) {
|
|
// Always generate the files if requested to ensure we capture any diagnostic errors but only
|
|
// don't emit them.
|
|
return;
|
|
}
|
|
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
|
};
|
|
}
|
|
|
|
function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
|
|
if (options.annotationsAs) {
|
|
switch (options.annotationsAs) {
|
|
case 'decorators':
|
|
case 'static fields':
|
|
break;
|
|
default:
|
|
return [{
|
|
messageText:
|
|
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
|
|
category: ts.DiagnosticCategory.Error,
|
|
source: SOURCE,
|
|
code: DEFAULT_ERROR_CODE
|
|
}];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function longestCommonSuffix(a: string, b: string): string {
|
|
let len = 0;
|
|
while (a.charCodeAt(a.length - 1 - len) === b.charCodeAt(b.length - 1 - len)) {
|
|
len++;
|
|
}
|
|
return a.substring(a.length - len);
|
|
}
|
|
|
|
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}"`);
|
|
}
|