Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): implement stable architect API f…
Browse files Browse the repository at this point in the history
…or extract-i18n
  • Loading branch information
clydin authored and hansl committed Mar 25, 2019
1 parent c840fcb commit a011863
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/builders.json
Expand Up @@ -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."
},
Expand Down
93 changes: 93 additions & 0 deletions 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<JsonObject & BrowserBuilderOptions>(
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<JsonObject & ExtractI18nBuilderOptions>(execute);
@@ -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",
Expand Down
Expand Up @@ -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(async () => {
await host.initialize().toPromise();
architect = (await createArchitect(host.root())).architect;
});

beforeEach(done => host.initialize().toPromise().then(done, done.fail));
afterEach(done => host.restore().toPromise().then(done, done.fail));
afterEach(() => host.restore().toPromise());

it('works', (done) => {
it('works', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');

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',
'<p i18n>Hello world <span i18n>inner</span></p>');

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', '<p i18n>i18n test</p>');
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', '<p i18n>i18n test</p>');
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', '<p i18n>i18n test</p>');
// 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', '<p i18n>i18n test</p>');
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);
});

0 comments on commit a011863

Please sign in to comment.