From 1eb2a878713f1ea8d25219d4bdd8f9561ba59f2d Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Thu, 1 Dec 2016 12:27:25 +0100 Subject: [PATCH 1/8] feature(i18n): implement i18n command Implement i18n messages extractor. Contrary to @angular/complier-cli's command it will not throw an error if a resource is not found. --- packages/@angular/cli/addon/index.js | 1 + packages/angular-cli/commands/xi18n.ts | 30 +++++++++++ packages/angular-cli/tasks/extract-i18n.ts | 60 ++++++++++++++++++++++ tests/acceptance/extract-i18n.spec.js | 51 ++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 packages/angular-cli/commands/xi18n.ts create mode 100644 packages/angular-cli/tasks/extract-i18n.ts create mode 100644 tests/acceptance/extract-i18n.spec.js diff --git a/packages/@angular/cli/addon/index.js b/packages/@angular/cli/addon/index.js index 5bc9aceb51e6..00cd26fe6672 100644 --- a/packages/@angular/cli/addon/index.js +++ b/packages/@angular/cli/addon/index.js @@ -32,6 +32,7 @@ module.exports = { 'version': require('../commands/version').default, 'completion': require('../commands/completion').default, 'doc': require('../commands/doc').default, + 'xi18n': require('../commands/xi18n').default, // Easter eggs. 'make-this-awesome': require('../commands/easter-egg').default, diff --git a/packages/angular-cli/commands/xi18n.ts b/packages/angular-cli/commands/xi18n.ts new file mode 100644 index 000000000000..636848778b1c --- /dev/null +++ b/packages/angular-cli/commands/xi18n.ts @@ -0,0 +1,30 @@ +const Command = require('../ember-cli/lib/models/command'); + +import {Extracti18nTask} from '../tasks/extract-i18n'; + +const Xi18nCommand = Command.extend({ + name: 'xi18n', + description: 'Extracts i18n messages from source code.', + works: 'insideProject', + availableOptions: [ + { + name: 'format', + type: String, + default: 'xliff', + aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xliff'}]} + ], + run: function (commandOptions: any) { + + const xi18nTask = new Extracti18nTask({ + ui: this.ui, + project: this.project, + i18nFormat: commandOptions.format + }); + + return xi18nTask.run(); + } +}); + + +export default Xi18nCommand; + diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/angular-cli/tasks/extract-i18n.ts new file mode 100644 index 000000000000..f3f4097202b4 --- /dev/null +++ b/packages/angular-cli/tasks/extract-i18n.ts @@ -0,0 +1,60 @@ +const Task = require('../ember-cli/lib/models/task'); + +import * as compiler from '@angular/compiler'; +import {Extractor} from '@angular/compiler-cli'; +import * as tsc from '@angular/tsc-wrapped'; +import * as ts from 'typescript'; +import * as path from 'path'; + +export const Extracti18nTask = Task.extend({ + run: function () { + const project = path.resolve(this.project.root, 'src'); + const cliOptions = new tsc.I18nExtractionCliOptions({ + i18nFormat: this.i18nFormat + }); + + function extract ( + ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions, + program: ts.Program, host: ts.CompilerHost) { + + const resourceLoader: compiler.ResourceLoader = { + get: (s: string) => { + if (!host.fileExists(s)) { + // Return empty string to avoid extractor stop processing + return Promise.resolve(''); + } + return Promise.resolve(host.readFile(s)); + } + }; + const extractor = + Extractor.create(ngOptions, cliOptions.i18nFormat, program, host, resourceLoader); + + const bundlePromise: Promise = 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: + const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser()); + ext = 'xlf'; + serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG); + break; + } + + const dstPath = path.join(ngOptions.genDir, `messages.${ext}`); + host.writeFile(dstPath, messageBundle.write(serializer), false); + }); + } + + return tsc.main(project, cliOptions, extract); + } +}); diff --git a/tests/acceptance/extract-i18n.spec.js b/tests/acceptance/extract-i18n.spec.js new file mode 100644 index 000000000000..bf4b51b6f69a --- /dev/null +++ b/tests/acceptance/extract-i18n.spec.js @@ -0,0 +1,51 @@ +const tmp = require('../helpers/tmp'); +const ng = require('../helpers/ng'); +const existsSync = require('exists-sync'); +const path = require('path'); +const root = process.cwd(); +const expect = require('chai').expect; + +// TODO: Enable when tests get validated +describe.skip('Acceptance: ng xi18n', function() { + beforeEach(function() { + this.timeout(180000); + return tmp.setup('./tmp').then(function() { + process.chdir('./tmp'); + }).then(function() { + return ng(['new', 'foo', '--link-cli']); + }); + }); + + afterEach(function() { + this.timeout(10000); + + return tmp.teardown('./tmp'); + }); + + it('ng xi18n', function() { + this.timeout(10000); + + const appRoot = path.join(root, 'tmp/foo'); + const messagesPath = path.join(appRoot, 'src/messages.xlf'); + + return ng(['xi18n']) + .then(() => { + expect(existsSync(messagesPath)).to.equal(true); + }); + + }); + + it('ng xi18n --format=xmb', function() { + this.timeout(10000); + + const appRoot = path.join(root, 'tmp/foo'); + const messagesPath = path.join(appRoot, 'src/messages.xmb'); + + return ng(['xi18n', '--format=xmb']) + .then(() => { + expect(existsSync(messagesPath)).to.equal(true); + }); + + }); + +}); From 878282e729ffb35522d2a29da02b3a0535947f2a Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Fri, 2 Dec 2016 10:36:52 +0100 Subject: [PATCH 2/8] Remove acceptance tests and create e2e tests --- tests/acceptance/extract-i18n.spec.js | 51 ------------------------- tests/e2e/tests/i18n/extract-default.ts | 19 +++++++++ tests/e2e/tests/i18n/extract-xmb.ts | 19 +++++++++ 3 files changed, 38 insertions(+), 51 deletions(-) delete mode 100644 tests/acceptance/extract-i18n.spec.js create mode 100644 tests/e2e/tests/i18n/extract-default.ts create mode 100644 tests/e2e/tests/i18n/extract-xmb.ts diff --git a/tests/acceptance/extract-i18n.spec.js b/tests/acceptance/extract-i18n.spec.js deleted file mode 100644 index bf4b51b6f69a..000000000000 --- a/tests/acceptance/extract-i18n.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -const tmp = require('../helpers/tmp'); -const ng = require('../helpers/ng'); -const existsSync = require('exists-sync'); -const path = require('path'); -const root = process.cwd(); -const expect = require('chai').expect; - -// TODO: Enable when tests get validated -describe.skip('Acceptance: ng xi18n', function() { - beforeEach(function() { - this.timeout(180000); - return tmp.setup('./tmp').then(function() { - process.chdir('./tmp'); - }).then(function() { - return ng(['new', 'foo', '--link-cli']); - }); - }); - - afterEach(function() { - this.timeout(10000); - - return tmp.teardown('./tmp'); - }); - - it('ng xi18n', function() { - this.timeout(10000); - - const appRoot = path.join(root, 'tmp/foo'); - const messagesPath = path.join(appRoot, 'src/messages.xlf'); - - return ng(['xi18n']) - .then(() => { - expect(existsSync(messagesPath)).to.equal(true); - }); - - }); - - it('ng xi18n --format=xmb', function() { - this.timeout(10000); - - const appRoot = path.join(root, 'tmp/foo'); - const messagesPath = path.join(appRoot, 'src/messages.xmb'); - - return ng(['xi18n', '--format=xmb']) - .then(() => { - expect(existsSync(messagesPath)).to.equal(true); - }); - - }); - -}); diff --git a/tests/e2e/tests/i18n/extract-default.ts b/tests/e2e/tests/i18n/extract-default.ts new file mode 100644 index 000000000000..aceaad2f4e4d --- /dev/null +++ b/tests/e2e/tests/i18n/extract-default.ts @@ -0,0 +1,19 @@ +import {join} from 'path'; +import {ng} from '../../utils/process'; +import { + expectFileToExist, deleteFile, writeFile, + expectFileToMatch +} from '../../utils/fs'; + + +export default function() { + const testComponentDir = join('src/app', 'i18n-test'); + return ng('generate', 'component', 'i18n-test') + .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) + .then(() => writeFile( + join(testComponentDir, 'i18n-test.component.html'), + '

Hello world

')) + .then(() => ng('xi18n')) + .then(() => expectFileToExist(join('src', 'messages.xlf'))) + .then(() => expectFileToMatch(join('src', 'messages.xlf'), /Hello world/)); +} diff --git a/tests/e2e/tests/i18n/extract-xmb.ts b/tests/e2e/tests/i18n/extract-xmb.ts new file mode 100644 index 000000000000..82da395be659 --- /dev/null +++ b/tests/e2e/tests/i18n/extract-xmb.ts @@ -0,0 +1,19 @@ +import {join} from 'path'; +import {ng} from '../../utils/process'; +import { + expectFileToExist, deleteFile, writeFile, + expectFileToMatch +} from '../../utils/fs'; + + +export default function() { + const testComponentDir = join('src/app', 'i18n-test'); + return ng('generate', 'component', 'i18n-test') + .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) + .then(() => writeFile( + join(testComponentDir, 'i18n-test.component.html'), + '

Hello world

')) + .then(() => ng('xi18n', '--format=xmb')) + .then(() => expectFileToExist(join('src', 'messages.xmb'))) + .then(() => expectFileToMatch(join('src', 'messages.xmb'), /Hello world/)); +} From 0bd69d1462a85b52be7f8d90b59604d2cf10b151 Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Wed, 7 Dec 2016 12:02:18 +0100 Subject: [PATCH 3/8] Reject task if format argument is unknown --- packages/angular-cli/tasks/extract-i18n.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/angular-cli/tasks/extract-i18n.ts index f3f4097202b4..10b16c7ae849 100644 --- a/packages/angular-cli/tasks/extract-i18n.ts +++ b/packages/angular-cli/tasks/extract-i18n.ts @@ -5,9 +5,11 @@ import {Extractor} from '@angular/compiler-cli'; import * as tsc from '@angular/tsc-wrapped'; import * as ts from 'typescript'; import * as path from 'path'; +import * as chalk from 'chalk'; export const Extracti18nTask = Task.extend({ run: function () { + const ui = this.ui; const project = path.resolve(this.project.root, 'src'); const cliOptions = new tsc.I18nExtractionCliOptions({ i18nFormat: this.i18nFormat @@ -35,7 +37,6 @@ export const Extracti18nTask = Task.extend({ let ext: string; let serializer: compiler.Serializer; const format = (cliOptions.i18nFormat || 'xlf').toLowerCase(); - switch (format) { case 'xmb': ext = 'xmb'; @@ -43,11 +44,12 @@ export const Extracti18nTask = Task.extend({ break; case 'xliff': case 'xlf': - default: const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser()); ext = 'xlf'; serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG); break; + default: + throw new Error('Unknown i18n output format. For available formats, see \`ng help\`.'); } const dstPath = path.join(ngOptions.genDir, `messages.${ext}`); @@ -55,6 +57,9 @@ export const Extracti18nTask = Task.extend({ }); } - return tsc.main(project, cliOptions, extract); + return tsc.main(project, cliOptions, extract) + .catch((e) => { + ui.writeLine(chalk.red(e.message)); + }); } }); From a28da7df2de28dce4275cf7252cbb053c5cd01e9 Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Wed, 25 Jan 2017 21:21:41 +0100 Subject: [PATCH 4/8] Feature redone to use ngtools api --- .../webpack/src/extract_i18n_plugin.ts | 166 ++++++++++++++++++ packages/angular-cli/commands/xi18n.ts | 22 ++- .../models/webpack-extract-i18n.ts | 25 +++ .../models/webpack-xi18n-config.ts | 43 +++++ packages/angular-cli/tasks/extract-i18n.ts | 96 +++++----- tests/e2e/tests/i18n/extract-output.ts | 19 ++ tests/e2e/tests/i18n/extract-xmb.ts | 2 +- 7 files changed, 315 insertions(+), 58 deletions(-) create mode 100644 packages/@ngtools/webpack/src/extract_i18n_plugin.ts create mode 100644 packages/angular-cli/models/webpack-extract-i18n.ts create mode 100644 packages/angular-cli/models/webpack-xi18n-config.ts create mode 100644 tests/e2e/tests/i18n/extract-output.ts diff --git a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts new file mode 100644 index 000000000000..0e040599a653 --- /dev/null +++ b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts @@ -0,0 +1,166 @@ +import * as ts from 'typescript'; +import * as path from 'path'; +import * as fs from 'fs'; + +import {__NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli'; + +import {Tapable} from './webpack'; +import {WebpackResourceLoader} from './resource_loader'; + +export interface ExtractI18nPluginOptions { + tsConfigPath: string; + basePath?: string; + genDir?: string; + i18nFormat?: string; + exclude?: string[]; +} + +export class ExtractI18nPlugin implements Tapable { + private _resourceLoader: WebpackResourceLoader; + + private _donePromise: Promise; + private _compiler: any = null; + private _compilation: any = null; + + private _tsConfigPath: string; + private _basePath: string; + private _genDir: string; + private _rootFilePath: string[]; + private _compilerOptions: any = null; + private _angularCompilerOptions: any = null; + //private _compilerHost: WebpackCompilerHost; + private _compilerHost: ts.CompilerHost; + private _program: ts.Program; + + private _i18nFormat: string; + + constructor(options: ExtractI18nPluginOptions) { + this._setupOptions(options); + } + + private _setupOptions(options: ExtractI18nPluginOptions) { + if (!options.hasOwnProperty('tsConfigPath')) { + throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); + } + this._tsConfigPath = options.tsConfigPath; + + // Check the base path. + const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); + let basePath = maybeBasePath; + if (fs.statSync(maybeBasePath).isFile()) { + basePath = path.dirname(basePath); + } + if (options.hasOwnProperty('basePath')) { + basePath = path.resolve(process.cwd(), options.basePath); + } + + let tsConfigJson: any = null; + try { + tsConfigJson = JSON.parse(fs.readFileSync(this._tsConfigPath, 'utf8')); + } catch (err) { + throw new Error(`An error happened while parsing ${this._tsConfigPath} JSON: ${err}.`); + } + const tsConfig = ts.parseJsonConfigFileContent( + tsConfigJson, ts.sys, basePath, null, this._tsConfigPath); + + let fileNames = tsConfig.fileNames; + if (options.hasOwnProperty('exclude')) { + let exclude: string[] = typeof options.exclude == 'string' + ? [options.exclude as string] : (options.exclude as string[]); + + exclude.forEach((pattern: string) => { + const basePathPattern = '(' + basePath.replace(/\\/g, '/') + .replace(/[\-\[\]\/{}()+?.\\^$|*]/g, '\\$&') + ')?'; + pattern = pattern + // Replace windows path separators with forward slashes. + .replace(/\\/g, '/') + // Escape characters that are used normally in regexes, except stars. + .replace(/[\-\[\]{}()+?.\\^$|]/g, '\\$&') + // Two stars replacement. + .replace(/\*\*/g, '(?:.*)') + // One star replacement. + .replace(/\*/g, '(?:[^/]*)') + // Escape characters from the basePath and make sure it's forward slashes. + .replace(/^/, basePathPattern); + + const re = new RegExp('^' + pattern + '$'); + fileNames = fileNames.filter(x => !x.replace(/\\/g, '/').match(re)); + }); + } else { + fileNames = fileNames.filter(fileName => !/\.spec\.ts$/.test(fileName)); + } + this._rootFilePath = fileNames; + + // By default messages will be generated in basePath + let genDir = basePath; + + if(options.hasOwnProperty('genDir')) { + genDir = path.resolve(process.cwd(), options.genDir); + } + + console.log(genDir); + + this._compilerOptions = tsConfig.options; + this._angularCompilerOptions = Object.assign( + { genDir }, + this._compilerOptions, + tsConfig.raw['angularCompilerOptions'], + { basePath } + ); + + this._basePath = basePath; + this._genDir = genDir; + + //this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); + this._compilerHost = ts.createCompilerHost(this._compilerOptions, true); + this._program = ts.createProgram( + this._rootFilePath, this._compilerOptions, this._compilerHost); + + if (options.hasOwnProperty('i18nFormat')) { + this._i18nFormat = options.i18nFormat; + } + } + + apply(compiler: any) { + this._compiler = compiler; + + compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb)); + + compiler.plugin('after-emit', (compilation: any, cb: any) => { + this._donePromise = null; + this._compilation = null; + compilation._ngToolsWebpackXi18nPluginInstance = null; + cb(); + }); + } + + private _make(compilation: any, cb: (err?: any, request?: any) => void) { + this._compilation = compilation; + if(this._compilation._ngToolsWebpackXi18nPluginInstance) { + return cb(new Error('An @ngtools/webpack xi18n plugin already exist for this compilation.')); + } + + this._compilation._ngToolsWebpackXi18nPluginInstance = this; + + this._resourceLoader = new WebpackResourceLoader(compilation); + + this._donePromise = Promise.resolve() + .then(() => { + return __NGTOOLS_PRIVATE_API_2.extractI18n({ + basePath: this._basePath, + compilerOptions: this._compilerOptions, + program: this._program, + host: this._compilerHost, + angularCompilerOptions: this._angularCompilerOptions, + i18nFormat: this._i18nFormat, + + readResource: (path: string) => this._resourceLoader.get(path) + }); + }) + .then(() => cb(), (err: any) => { + compilation.errors.push(err); + cb(); + }); + + } +} diff --git a/packages/angular-cli/commands/xi18n.ts b/packages/angular-cli/commands/xi18n.ts index 636848778b1c..496054f3a1dc 100644 --- a/packages/angular-cli/commands/xi18n.ts +++ b/packages/angular-cli/commands/xi18n.ts @@ -2,26 +2,36 @@ const Command = require('../ember-cli/lib/models/command'); import {Extracti18nTask} from '../tasks/extract-i18n'; +export interface Xi18nOptions { + outputPath?: string; + verbose?: boolean; + i18nFormat?: string; +} + const Xi18nCommand = Command.extend({ name: 'xi18n', description: 'Extracts i18n messages from source code.', works: 'insideProject', availableOptions: [ { - name: 'format', + name: 'i18n-format', type: String, - default: 'xliff', - aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xliff'}]} + default: 'xlf', + aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xlf'}] + }, + { name: 'output-path', type: 'Path', default: null, aliases: ['o']}, + { name: 'verbose', type: Boolean, default: false}, + { name: 'progress', type: Boolean, default: true } + ], run: function (commandOptions: any) { const xi18nTask = new Extracti18nTask({ ui: this.ui, - project: this.project, - i18nFormat: commandOptions.format + project: this.project }); - return xi18nTask.run(); + return xi18nTask.run(commandOptions); } }); diff --git a/packages/angular-cli/models/webpack-extract-i18n.ts b/packages/angular-cli/models/webpack-extract-i18n.ts new file mode 100644 index 000000000000..e38fd57573e2 --- /dev/null +++ b/packages/angular-cli/models/webpack-extract-i18n.ts @@ -0,0 +1,25 @@ +import * as path from 'path'; +import {ExtractI18nPlugin} from '../../@ngtools/webpack/src/extract_i18n_plugin'; + +export const getWebpackExtractI18nConfig = function( + projectRoot: string, + appConfig: any, + genDir: string, + i18nFormat: string):any { + + let exclude: string[] = []; + if (appConfig.test) { + exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); + } + + return { + plugins: [ + new ExtractI18nPlugin({ + tsConfigPath: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig), + exclude: exclude, + genDir: genDir, + i18nFormat: i18nFormat + }) + ] + } +}; diff --git a/packages/angular-cli/models/webpack-xi18n-config.ts b/packages/angular-cli/models/webpack-xi18n-config.ts new file mode 100644 index 000000000000..5085582e53ed --- /dev/null +++ b/packages/angular-cli/models/webpack-xi18n-config.ts @@ -0,0 +1,43 @@ +import {CliConfig} from './config'; +import {NgCliWebpackConfig} from './webpack-config'; +const webpackMerge = require('webpack-merge'); +import {getWebpackExtractI18nConfig} from './webpack-extract-i18n'; + +export class XI18nWebpackConfig extends NgCliWebpackConfig { + + public config: any; + + constructor( + ngCliProject: any, + genDir: string, + buildDir: string, + i18nFormat: string, + verbose: boolean = false, progress: boolean = true) { + super( + ngCliProject, + 'development', + 'dev', + buildDir, + null, + null, + null, + null, + false, + true, + true, + verbose, + progress, + null, + 'none', + true); + + const appConfig = CliConfig.fromProject().config.apps[0]; + + let config = this.config; + const extractI18nConfig = + getWebpackExtractI18nConfig(this.ngCliProject.root, appConfig, genDir, i18nFormat); + config = webpackMerge(config, extractI18nConfig); + + this.config = config; + } +} diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/angular-cli/tasks/extract-i18n.ts index 10b16c7ae849..e7226ccf6e72 100644 --- a/packages/angular-cli/tasks/extract-i18n.ts +++ b/packages/angular-cli/tasks/extract-i18n.ts @@ -1,65 +1,59 @@ +import * as webpack from 'webpack'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; + const Task = require('../ember-cli/lib/models/task'); -import * as compiler from '@angular/compiler'; -import {Extractor} from '@angular/compiler-cli'; -import * as tsc from '@angular/tsc-wrapped'; -import * as ts from 'typescript'; -import * as path from 'path'; -import * as chalk from 'chalk'; +import {XI18nWebpackConfig} from '../models/webpack-xi18n-config'; +import {CliConfig} from '../models/config'; + export const Extracti18nTask = Task.extend({ - run: function () { - const ui = this.ui; - const project = path.resolve(this.project.root, 'src'); - const cliOptions = new tsc.I18nExtractionCliOptions({ - i18nFormat: this.i18nFormat - }); + run: function (runTaskOptions: any) { - function extract ( - ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions, - program: ts.Program, host: ts.CompilerHost) { + const project = this.project; - const resourceLoader: compiler.ResourceLoader = { - get: (s: string) => { - if (!host.fileExists(s)) { - // Return empty string to avoid extractor stop processing - return Promise.resolve(''); - } - return Promise.resolve(host.readFile(s)); - } - }; - const extractor = - Extractor.create(ngOptions, cliOptions.i18nFormat, program, host, resourceLoader); + const appConfig = CliConfig.fromProject().config.apps[0]; - const bundlePromise: Promise = extractor.extract(); + const buildDir = '.tmp'; + const genDir = runTaskOptions.outputPath || appConfig.root; - 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': - const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser()); - ext = 'xlf'; - serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG); - break; - default: - throw new Error('Unknown i18n output format. For available formats, see \`ng help\`.'); + const config = new XI18nWebpackConfig( + project, + genDir, + buildDir, + runTaskOptions.i18nFormat, + runTaskOptions.verbose, + runTaskOptions.progress + ).config; + + const webpackCompiler = webpack(config); + //const statsConfig = getWebpackStatsConfig(runTaskOptions.verbose); + + return new Promise((resolve, reject) => { + const callback: webpack.compiler.CompilerCallback = (err, stats) => { + if (err) { + return reject(err); } - const dstPath = path.join(ngOptions.genDir, `messages.${ext}`); - host.writeFile(dstPath, messageBundle.write(serializer), false); - }); - } + if (stats.hasErrors()) { + reject(); + } else { + resolve(); + } + }; - return tsc.main(project, cliOptions, extract) - .catch((e) => { - ui.writeLine(chalk.red(e.message)); + webpackCompiler.run(callback); + }) + .then(() => { + // Deletes temporary build folder + rimraf.sync(path.resolve(project.root, buildDir)); + }) + .catch((err: Error) => { + if (err) { + this.ui.writeError('\nAn error occured during the i18n extraction:\n' + ((err && err.stack) || err)); + } + throw err; }); } }); diff --git a/tests/e2e/tests/i18n/extract-output.ts b/tests/e2e/tests/i18n/extract-output.ts new file mode 100644 index 000000000000..73b57e32413f --- /dev/null +++ b/tests/e2e/tests/i18n/extract-output.ts @@ -0,0 +1,19 @@ +import {join} from 'path'; +import {ng} from '../../utils/process'; +import { + expectFileToExist, deleteFile, writeFile, + expectFileToMatch +} from '../../utils/fs'; + + +export default function() { + const testComponentDir = join('src/app', 'i18n-test'); + return ng('generate', 'component', 'i18n-test') + .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) + .then(() => writeFile( + join(testComponentDir, 'i18n-test.component.html'), + '

Hello world

')) + .then(() => ng('xi18n -o src/locale')) + .then(() => expectFileToExist(join('src', 'locale', 'messages.xlf'))) + .then(() => expectFileToMatch(join('src', 'locale', 'messages.xlf'), /Hello world/)); +} diff --git a/tests/e2e/tests/i18n/extract-xmb.ts b/tests/e2e/tests/i18n/extract-xmb.ts index 82da395be659..1df116f0aa07 100644 --- a/tests/e2e/tests/i18n/extract-xmb.ts +++ b/tests/e2e/tests/i18n/extract-xmb.ts @@ -13,7 +13,7 @@ export default function() { .then(() => writeFile( join(testComponentDir, 'i18n-test.component.html'), '

Hello world

')) - .then(() => ng('xi18n', '--format=xmb')) + .then(() => ng('xi18n', '--i18n-format=xmb')) .then(() => expectFileToExist(join('src', 'messages.xmb'))) .then(() => expectFileToMatch(join('src', 'messages.xmb'), /Hello world/)); } From f5de5be6e61f09ab8a399c5593a3600316ec80aa Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Wed, 25 Jan 2017 21:44:43 +0100 Subject: [PATCH 5/8] Fix code after rebase master --- .../models/webpack-xi18n-config.ts | 50 +++++++++---------- packages/angular-cli/tasks/extract-i18n.ts | 11 ++-- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/angular-cli/models/webpack-xi18n-config.ts b/packages/angular-cli/models/webpack-xi18n-config.ts index 5085582e53ed..6fd712e11215 100644 --- a/packages/angular-cli/models/webpack-xi18n-config.ts +++ b/packages/angular-cli/models/webpack-xi18n-config.ts @@ -1,43 +1,39 @@ +import * as path from 'path'; + import {CliConfig} from './config'; import {NgCliWebpackConfig} from './webpack-config'; const webpackMerge = require('webpack-merge'); import {getWebpackExtractI18nConfig} from './webpack-extract-i18n'; +export interface XI18WebpackOptions { + genDir?: string, + buildDir?: string, + i18nFormat?: string, + verbose?: boolean, + progress?: boolean +} export class XI18nWebpackConfig extends NgCliWebpackConfig { public config: any; - constructor( - ngCliProject: any, - genDir: string, - buildDir: string, - i18nFormat: string, - verbose: boolean = false, progress: boolean = true) { - super( - ngCliProject, - 'development', - 'dev', - buildDir, - null, - null, - null, - null, - false, - true, - true, - verbose, - progress, - null, - 'none', - true); + constructor(extractOptions: XI18WebpackOptions) { + + super({ + target: 'development', + verbose: extractOptions.verbose, + progress: extractOptions.progress + }); + const configPath = CliConfig.configFilePath(); + const projectRoot = path.dirname(configPath); const appConfig = CliConfig.fromProject().config.apps[0]; - let config = this.config; const extractI18nConfig = - getWebpackExtractI18nConfig(this.ngCliProject.root, appConfig, genDir, i18nFormat); - config = webpackMerge(config, extractI18nConfig); + getWebpackExtractI18nConfig(projectRoot, + appConfig, + extractOptions.genDir, + extractOptions.i18nFormat); - this.config = config; + this.config = webpackMerge([this.config, extractI18nConfig]); } } diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/angular-cli/tasks/extract-i18n.ts index e7226ccf6e72..cb4fc5b6e17d 100644 --- a/packages/angular-cli/tasks/extract-i18n.ts +++ b/packages/angular-cli/tasks/extract-i18n.ts @@ -18,14 +18,13 @@ export const Extracti18nTask = Task.extend({ const buildDir = '.tmp'; const genDir = runTaskOptions.outputPath || appConfig.root; - const config = new XI18nWebpackConfig( - project, + const config = new XI18nWebpackConfig({ genDir, buildDir, - runTaskOptions.i18nFormat, - runTaskOptions.verbose, - runTaskOptions.progress - ).config; + i18nFormat: runTaskOptions.i18nFormat, + verbose: runTaskOptions.verbose, + progress: runTaskOptions.progress + }).config; const webpackCompiler = webpack(config); //const statsConfig = getWebpackStatsConfig(runTaskOptions.verbose); From ccc7aed5ef772f3a220cf5dca7ecd410546da15d Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Thu, 26 Jan 2017 17:30:02 +0100 Subject: [PATCH 6/8] Fix e2e tests after rebase master --- .../@angular/cli/models/webpack-configs/index.ts | 1 + .../@ngtools/webpack/src/extract_i18n_plugin.ts | 13 ++++++------- packages/@ngtools/webpack/src/index.ts | 1 + packages/angular-cli/commands/xi18n.ts | 2 +- .../xi18n.ts} | 6 +++--- packages/angular-cli/models/webpack-xi18n-config.ts | 12 ++++++------ packages/angular-cli/tasks/extract-i18n.ts | 4 ++-- tests/e2e/tests/i18n/extract-default.ts | 2 +- tests/e2e/tests/i18n/extract-output.ts | 2 +- tests/e2e/tests/i18n/extract-xmb.ts | 2 +- 10 files changed, 23 insertions(+), 22 deletions(-) rename packages/angular-cli/models/{webpack-extract-i18n.ts => webpack-configs/xi18n.ts} (81%) diff --git a/packages/@angular/cli/models/webpack-configs/index.ts b/packages/@angular/cli/models/webpack-configs/index.ts index dc3c96bac237..64d0358482cf 100644 --- a/packages/@angular/cli/models/webpack-configs/index.ts +++ b/packages/@angular/cli/models/webpack-configs/index.ts @@ -4,3 +4,4 @@ export * from './production'; export * from './styles'; export * from './typescript'; export * from './utils'; +export * from './xi18n'; diff --git a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts index 0e040599a653..3c4188f00792 100644 --- a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts +++ b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts @@ -28,7 +28,7 @@ export class ExtractI18nPlugin implements Tapable { private _rootFilePath: string[]; private _compilerOptions: any = null; private _angularCompilerOptions: any = null; - //private _compilerHost: WebpackCompilerHost; + // private _compilerHost: WebpackCompilerHost; private _compilerHost: ts.CompilerHost; private _program: ts.Program; @@ -94,12 +94,10 @@ export class ExtractI18nPlugin implements Tapable { // By default messages will be generated in basePath let genDir = basePath; - if(options.hasOwnProperty('genDir')) { + if (options.hasOwnProperty('genDir')) { genDir = path.resolve(process.cwd(), options.genDir); } - console.log(genDir); - this._compilerOptions = tsConfig.options; this._angularCompilerOptions = Object.assign( { genDir }, @@ -111,7 +109,7 @@ export class ExtractI18nPlugin implements Tapable { this._basePath = basePath; this._genDir = genDir; - //this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); + // this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); this._compilerHost = ts.createCompilerHost(this._compilerOptions, true); this._program = ts.createProgram( this._rootFilePath, this._compilerOptions, this._compilerHost); @@ -136,8 +134,9 @@ export class ExtractI18nPlugin implements Tapable { private _make(compilation: any, cb: (err?: any, request?: any) => void) { this._compilation = compilation; - if(this._compilation._ngToolsWebpackXi18nPluginInstance) { - return cb(new Error('An @ngtools/webpack xi18n plugin already exist for this compilation.')); + if (this._compilation._ngToolsWebpackXi18nPluginInstance) { + return cb(new Error('An @ngtools/webpack xi18n plugin already exist for ' + + 'this compilation.')); } this._compilation._ngToolsWebpackXi18nPluginInstance = this; diff --git a/packages/@ngtools/webpack/src/index.ts b/packages/@ngtools/webpack/src/index.ts index 067d1e724697..ae975e53884f 100644 --- a/packages/@ngtools/webpack/src/index.ts +++ b/packages/@ngtools/webpack/src/index.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; export * from './plugin'; +export * from './extract_i18n_plugin'; export {ngcLoader as default} from './loader'; export {PathsPlugin} from './paths-plugin'; diff --git a/packages/angular-cli/commands/xi18n.ts b/packages/angular-cli/commands/xi18n.ts index 496054f3a1dc..066319bf9c2c 100644 --- a/packages/angular-cli/commands/xi18n.ts +++ b/packages/angular-cli/commands/xi18n.ts @@ -19,7 +19,7 @@ const Xi18nCommand = Command.extend({ default: 'xlf', aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xlf'}] }, - { name: 'output-path', type: 'Path', default: null, aliases: ['o']}, + { name: 'output-path', type: 'Path', default: null, aliases: ['op']}, { name: 'verbose', type: Boolean, default: false}, { name: 'progress', type: Boolean, default: true } diff --git a/packages/angular-cli/models/webpack-extract-i18n.ts b/packages/angular-cli/models/webpack-configs/xi18n.ts similarity index 81% rename from packages/angular-cli/models/webpack-extract-i18n.ts rename to packages/angular-cli/models/webpack-configs/xi18n.ts index e38fd57573e2..8130fcbaeb2b 100644 --- a/packages/angular-cli/models/webpack-extract-i18n.ts +++ b/packages/angular-cli/models/webpack-configs/xi18n.ts @@ -1,11 +1,11 @@ import * as path from 'path'; -import {ExtractI18nPlugin} from '../../@ngtools/webpack/src/extract_i18n_plugin'; +import {ExtractI18nPlugin} from '@ngtools/webpack'; export const getWebpackExtractI18nConfig = function( projectRoot: string, appConfig: any, genDir: string, - i18nFormat: string):any { + i18nFormat: string): any { let exclude: string[] = []; if (appConfig.test) { @@ -21,5 +21,5 @@ export const getWebpackExtractI18nConfig = function( i18nFormat: i18nFormat }) ] - } + }; }; diff --git a/packages/angular-cli/models/webpack-xi18n-config.ts b/packages/angular-cli/models/webpack-xi18n-config.ts index 6fd712e11215..8338d5bf1052 100644 --- a/packages/angular-cli/models/webpack-xi18n-config.ts +++ b/packages/angular-cli/models/webpack-xi18n-config.ts @@ -3,14 +3,14 @@ import * as path from 'path'; import {CliConfig} from './config'; import {NgCliWebpackConfig} from './webpack-config'; const webpackMerge = require('webpack-merge'); -import {getWebpackExtractI18nConfig} from './webpack-extract-i18n'; +import {getWebpackExtractI18nConfig} from './webpack-configs'; export interface XI18WebpackOptions { - genDir?: string, - buildDir?: string, - i18nFormat?: string, - verbose?: boolean, - progress?: boolean + genDir?: string; + buildDir?: string; + i18nFormat?: string; + verbose?: boolean; + progress?: boolean; } export class XI18nWebpackConfig extends NgCliWebpackConfig { diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/angular-cli/tasks/extract-i18n.ts index cb4fc5b6e17d..4fdea3d210a5 100644 --- a/packages/angular-cli/tasks/extract-i18n.ts +++ b/packages/angular-cli/tasks/extract-i18n.ts @@ -27,7 +27,6 @@ export const Extracti18nTask = Task.extend({ }).config; const webpackCompiler = webpack(config); - //const statsConfig = getWebpackStatsConfig(runTaskOptions.verbose); return new Promise((resolve, reject) => { const callback: webpack.compiler.CompilerCallback = (err, stats) => { @@ -50,7 +49,8 @@ export const Extracti18nTask = Task.extend({ }) .catch((err: Error) => { if (err) { - this.ui.writeError('\nAn error occured during the i18n extraction:\n' + ((err && err.stack) || err)); + this.ui.writeError('\nAn error occured during the i18n extraction:\n' + + ((err && err.stack) || err)); } throw err; }); diff --git a/tests/e2e/tests/i18n/extract-default.ts b/tests/e2e/tests/i18n/extract-default.ts index aceaad2f4e4d..9b5fd0d2f683 100644 --- a/tests/e2e/tests/i18n/extract-default.ts +++ b/tests/e2e/tests/i18n/extract-default.ts @@ -13,7 +13,7 @@ export default function() { .then(() => writeFile( join(testComponentDir, 'i18n-test.component.html'), '

Hello world

')) - .then(() => ng('xi18n')) + .then(() => ng('xi18n', '--no-progress')) .then(() => expectFileToExist(join('src', 'messages.xlf'))) .then(() => expectFileToMatch(join('src', 'messages.xlf'), /Hello world/)); } diff --git a/tests/e2e/tests/i18n/extract-output.ts b/tests/e2e/tests/i18n/extract-output.ts index 73b57e32413f..c1a13ccb3e31 100644 --- a/tests/e2e/tests/i18n/extract-output.ts +++ b/tests/e2e/tests/i18n/extract-output.ts @@ -13,7 +13,7 @@ export default function() { .then(() => writeFile( join(testComponentDir, 'i18n-test.component.html'), '

Hello world

')) - .then(() => ng('xi18n -o src/locale')) + .then(() => ng('xi18n', '--no-progress', '--output-path', 'src/locale')) .then(() => expectFileToExist(join('src', 'locale', 'messages.xlf'))) .then(() => expectFileToMatch(join('src', 'locale', 'messages.xlf'), /Hello world/)); } diff --git a/tests/e2e/tests/i18n/extract-xmb.ts b/tests/e2e/tests/i18n/extract-xmb.ts index 1df116f0aa07..f01324642f9b 100644 --- a/tests/e2e/tests/i18n/extract-xmb.ts +++ b/tests/e2e/tests/i18n/extract-xmb.ts @@ -13,7 +13,7 @@ export default function() { .then(() => writeFile( join(testComponentDir, 'i18n-test.component.html'), '

Hello world

')) - .then(() => ng('xi18n', '--i18n-format=xmb')) + .then(() => ng('xi18n', '--no-progress', '--i18n-format', 'xmb')) .then(() => expectFileToExist(join('src', 'messages.xmb'))) .then(() => expectFileToMatch(join('src', 'messages.xmb'), /Hello world/)); } From 319518383e1decf69a9305319abd34291d09ffc5 Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Tue, 31 Jan 2017 09:42:38 +0100 Subject: [PATCH 7/8] Assert an AotPlugin instance is present in webpack compilation. Revisit e2e tests. --- packages/@ngtools/webpack/src/extract_i18n_plugin.ts | 4 ++++ tests/e2e/tests/i18n/extract-default.ts | 6 ++---- tests/e2e/tests/i18n/extract-output.ts | 6 ++---- tests/e2e/tests/i18n/extract-xmb.ts | 6 ++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts index 3c4188f00792..7b4d9ed0a778 100644 --- a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts +++ b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts @@ -138,6 +138,10 @@ export class ExtractI18nPlugin implements Tapable { return cb(new Error('An @ngtools/webpack xi18n plugin already exist for ' + 'this compilation.')); } + if (!this._compilation._ngToolsWebpackPluginInstance) { + return cb(new Error('An @ngtools/webpack aot plugin does not exists ' + + 'for this compilation')); + } this._compilation._ngToolsWebpackXi18nPluginInstance = this; diff --git a/tests/e2e/tests/i18n/extract-default.ts b/tests/e2e/tests/i18n/extract-default.ts index 9b5fd0d2f683..066711600685 100644 --- a/tests/e2e/tests/i18n/extract-default.ts +++ b/tests/e2e/tests/i18n/extract-default.ts @@ -1,17 +1,15 @@ import {join} from 'path'; import {ng} from '../../utils/process'; import { - expectFileToExist, deleteFile, writeFile, + expectFileToExist, writeFile, expectFileToMatch } from '../../utils/fs'; export default function() { - const testComponentDir = join('src/app', 'i18n-test'); return ng('generate', 'component', 'i18n-test') - .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) .then(() => writeFile( - join(testComponentDir, 'i18n-test.component.html'), + join('src/app/i18n-test', 'i18n-test.component.html'), '

Hello world

')) .then(() => ng('xi18n', '--no-progress')) .then(() => expectFileToExist(join('src', 'messages.xlf'))) diff --git a/tests/e2e/tests/i18n/extract-output.ts b/tests/e2e/tests/i18n/extract-output.ts index c1a13ccb3e31..87677b5a6864 100644 --- a/tests/e2e/tests/i18n/extract-output.ts +++ b/tests/e2e/tests/i18n/extract-output.ts @@ -1,17 +1,15 @@ import {join} from 'path'; import {ng} from '../../utils/process'; import { - expectFileToExist, deleteFile, writeFile, + expectFileToExist, writeFile, expectFileToMatch } from '../../utils/fs'; export default function() { - const testComponentDir = join('src/app', 'i18n-test'); return ng('generate', 'component', 'i18n-test') - .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) .then(() => writeFile( - join(testComponentDir, 'i18n-test.component.html'), + join('src/app/i18n-test', 'i18n-test.component.html'), '

Hello world

')) .then(() => ng('xi18n', '--no-progress', '--output-path', 'src/locale')) .then(() => expectFileToExist(join('src', 'locale', 'messages.xlf'))) diff --git a/tests/e2e/tests/i18n/extract-xmb.ts b/tests/e2e/tests/i18n/extract-xmb.ts index f01324642f9b..04adaab6549a 100644 --- a/tests/e2e/tests/i18n/extract-xmb.ts +++ b/tests/e2e/tests/i18n/extract-xmb.ts @@ -1,17 +1,15 @@ import {join} from 'path'; import {ng} from '../../utils/process'; import { - expectFileToExist, deleteFile, writeFile, + expectFileToExist, writeFile, expectFileToMatch } from '../../utils/fs'; export default function() { - const testComponentDir = join('src/app', 'i18n-test'); return ng('generate', 'component', 'i18n-test') - .then(() => deleteFile(join(testComponentDir, 'i18n-test.component.html'))) .then(() => writeFile( - join(testComponentDir, 'i18n-test.component.html'), + join('src/app/i18n-test', 'i18n-test.component.html'), '

Hello world

')) .then(() => ng('xi18n', '--no-progress', '--i18n-format', 'xmb')) .then(() => expectFileToExist(join('src', 'messages.xmb'))) From 18adc307cd63b1923a91fb2551d8a7c66035b1d7 Mon Sep 17 00:00:00 2001 From: Carles Galan Cladera Date: Tue, 7 Feb 2017 10:18:32 +0100 Subject: [PATCH 8/8] Rebase master --- packages/{angular-cli => @angular/cli}/commands/xi18n.ts | 0 .../{angular-cli => @angular/cli}/models/webpack-configs/xi18n.ts | 0 .../{angular-cli => @angular/cli}/models/webpack-xi18n-config.ts | 0 packages/{angular-cli => @angular/cli}/tasks/extract-i18n.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/{angular-cli => @angular/cli}/commands/xi18n.ts (100%) rename packages/{angular-cli => @angular/cli}/models/webpack-configs/xi18n.ts (100%) rename packages/{angular-cli => @angular/cli}/models/webpack-xi18n-config.ts (100%) rename packages/{angular-cli => @angular/cli}/tasks/extract-i18n.ts (100%) diff --git a/packages/angular-cli/commands/xi18n.ts b/packages/@angular/cli/commands/xi18n.ts similarity index 100% rename from packages/angular-cli/commands/xi18n.ts rename to packages/@angular/cli/commands/xi18n.ts diff --git a/packages/angular-cli/models/webpack-configs/xi18n.ts b/packages/@angular/cli/models/webpack-configs/xi18n.ts similarity index 100% rename from packages/angular-cli/models/webpack-configs/xi18n.ts rename to packages/@angular/cli/models/webpack-configs/xi18n.ts diff --git a/packages/angular-cli/models/webpack-xi18n-config.ts b/packages/@angular/cli/models/webpack-xi18n-config.ts similarity index 100% rename from packages/angular-cli/models/webpack-xi18n-config.ts rename to packages/@angular/cli/models/webpack-xi18n-config.ts diff --git a/packages/angular-cli/tasks/extract-i18n.ts b/packages/@angular/cli/tasks/extract-i18n.ts similarity index 100% rename from packages/angular-cli/tasks/extract-i18n.ts rename to packages/@angular/cli/tasks/extract-i18n.ts