diff --git a/packages/angular_devkit/build_angular/builders.json b/packages/angular_devkit/build_angular/builders.json index fa89a35d33..7cdbbafb36 100644 --- a/packages/angular_devkit/build_angular/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -21,6 +21,7 @@ }, "extract-i18n": { "class": "./src/extract-i18n", + "implementation": "./src/extract-i18n/index2", "schema": "./src/extract-i18n/schema.json", "description": "Extract i18n strings from a browser app." }, diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/index2.ts b/packages/angular_devkit/build_angular/src/extract-i18n/index2.ts new file mode 100644 index 0000000000..ff81e34d80 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/extract-i18n/index2.ts @@ -0,0 +1,93 @@ +/** + * @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 { + BuilderContext, + createBuilder, + targetFromTargetString, +} from '@angular-devkit/architect/src/index2'; +import { runWebpack } from '@angular-devkit/build-webpack/src/webpack/index2'; +import { JsonObject } from '@angular-devkit/core'; +import * as path from 'path'; +import * as webpack from 'webpack'; +import { + getAotConfig, + getCommonConfig, + getStatsConfig, + getStylesConfig, +} from '../angular-cli-files/models/webpack-configs'; +import { Schema as BrowserBuilderOptions } from '../browser/schema'; +import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config'; +import { Schema as ExtractI18nBuilderOptions } from './schema'; + +function getI18nOutfile(format: string | undefined) { + switch (format) { + case 'xmb': + return 'messages.xmb'; + case 'xlf': + case 'xlif': + case 'xliff': + case 'xlf2': + case 'xliff2': + return 'messages.xlf'; + default: + throw new Error(`Unsupported format "${format}"`); + } +} + +class InMemoryOutputPlugin { + apply(compiler: webpack.Compiler): void { + // tslint:disable-next-line:no-any + compiler.outputFileSystem = new (webpack as any).MemoryOutputFileSystem(); + } +} + +export async function execute(options: ExtractI18nBuilderOptions, context: BuilderContext) { + const browserTarget = targetFromTargetString(options.browserTarget); + const browserOptions = await context.validateOptions( + await context.getTargetOptions(browserTarget), + await context.getBuilderNameForTarget(browserTarget), + ); + + // We need to determine the outFile name so that AngularCompiler can retrieve it. + let outFile = options.outFile || getI18nOutfile(options.i18nFormat); + if (options.outputPath) { + // AngularCompilerPlugin doesn't support genDir so we have to adjust outFile instead. + outFile = path.join(options.outputPath, outFile); + } + + const { config } = await generateBrowserWebpackConfigFromContext( + { + ...browserOptions, + optimization: { + scripts: false, + styles: false, + }, + i18nLocale: options.i18nLocale, + i18nFormat: options.i18nFormat, + i18nFile: outFile, + aot: true, + progress: options.progress, + assets: [], + scripts: [], + styles: [], + deleteOutputPath: false, + }, + context, + wco => [ + { plugins: [new InMemoryOutputPlugin()] }, + getCommonConfig(wco), + getAotConfig(wco, true), + getStylesConfig(wco), + getStatsConfig(wco), + ], + ); + + return runWebpack(config, context).toPromise(); +} + +export default createBuilder(execute); diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/schema.json b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json index 574a453c78..a40af39254 100644 --- a/packages/angular_devkit/build_angular/src/extract-i18n/schema.json +++ b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema", "title": "Extract i18n Target", "description": "Extract i18n target options for Build Facade.", "type": "object", diff --git a/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts b/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts index 2c69b92dd2..625722c3a8 100644 --- a/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts @@ -5,105 +5,119 @@ * 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 { DefaultTimeout, TestLogger, runTargetSpec } from '@angular-devkit/architect/testing'; +import { Architect } from '@angular-devkit/architect/src/index2'; +import { TestLogger } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; -import { tap } from 'rxjs/operators'; -import { extractI18nTargetSpec, host } from '../utils'; +import { createArchitect, extractI18nTargetSpec, host } from '../utils'; describe('Extract i18n Target', () => { const extractionFile = join(normalize('src'), 'messages.xlf'); + let architect: Architect; - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + }); - it('works', (done) => { + afterEach(() => host.restore().toPromise()); + + it('works', async () => { host.appendToFile('src/app/app.component.html', '

i18n test

'); - runTargetSpec(host, extractI18nTargetSpec).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - expect(host.scopedSync().exists((extractionFile))).toBe(true); - expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) - .toMatch(/i18n test/); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + const exists = host.scopedSync().exists(extractionFile); + expect(exists).toBe(true); + + if (exists) { + const content = virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)); + expect(content).toContain('i18n test'); + } }, 30000); - it('shows errors', (done) => { + it('shows errors', async () => { const logger = new TestLogger('i18n-errors'); host.appendToFile('src/app/app.component.html', '

Hello world inner

'); - runTargetSpec(host, extractI18nTargetSpec, {}, DefaultTimeout, logger).pipe( - tap((buildEvent) => { - expect(buildEvent.success).toBe(false); - const msg = 'Could not mark an element as translatable inside a translatable section'; - expect(logger.includes(msg)).toBe(true); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec, undefined, { logger }); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false })); + + await run.stop(); + + const msg = 'Could not mark an element as translatable inside a translatable section'; + expect(logger.includes(msg)).toBe(true); }, 30000); - it('supports locale', (done) => { + it('supports locale', async () => { host.appendToFile('src/app/app.component.html', '

i18n test

'); const overrides = { i18nLocale: 'fr' }; - runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - expect(host.scopedSync().exists((extractionFile))).toBe(true); - expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) - .toContain('source-language="fr"'); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + expect(host.scopedSync().exists((extractionFile))).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toContain('source-language="fr"'); }, 30000); - it('supports out file', (done) => { + it('supports out file', async () => { host.appendToFile('src/app/app.component.html', '

i18n test

'); const outFile = 'messages.fr.xlf'; const extractionFile = join(normalize('src'), outFile); const overrides = { outFile }; - runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - expect(host.scopedSync().exists(extractionFile)).toBe(true); - expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) - .toMatch(/i18n test/); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); }, 30000); - it('supports output path', (done) => { + it('supports output path', async () => { host.appendToFile('src/app/app.component.html', '

i18n test

'); // Note: this folder will not be created automatically. It must exist beforehand. const outputPath = 'app'; const extractionFile = join(normalize('src'), outputPath, 'messages.xlf'); const overrides = { outputPath }; - runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - expect(host.scopedSync().exists(extractionFile)).toBe(true); - expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) - .toMatch(/i18n test/); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); }, 30000); - it('supports i18n format', (done) => { + it('supports i18n format', async () => { host.appendToFile('src/app/app.component.html', '

i18n test

'); const extractionFile = join(normalize('src'), 'messages.xmb'); const overrides = { i18nFormat: 'xmb' }; - runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - expect(host.scopedSync().exists(extractionFile)).toBe(true); - expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) - .toMatch(/i18n test/); - }), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); }, 30000); });