Skip to content

Commit 009d277

Browse files
committed
Feature redone to use ngtools api
1 parent 331ecbf commit 009d277

File tree

7 files changed

+315
-58
lines changed

7 files changed

+315
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import * as ts from 'typescript';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
5+
import {__NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli';
6+
7+
import {Tapable} from './webpack';
8+
import {WebpackResourceLoader} from './resource_loader';
9+
10+
export interface ExtractI18nPluginOptions {
11+
tsConfigPath: string;
12+
basePath?: string;
13+
genDir?: string;
14+
i18nFormat?: string;
15+
exclude?: string[];
16+
}
17+
18+
export class ExtractI18nPlugin implements Tapable {
19+
private _resourceLoader: WebpackResourceLoader;
20+
21+
private _donePromise: Promise<void>;
22+
private _compiler: any = null;
23+
private _compilation: any = null;
24+
25+
private _tsConfigPath: string;
26+
private _basePath: string;
27+
private _genDir: string;
28+
private _rootFilePath: string[];
29+
private _compilerOptions: any = null;
30+
private _angularCompilerOptions: any = null;
31+
//private _compilerHost: WebpackCompilerHost;
32+
private _compilerHost: ts.CompilerHost;
33+
private _program: ts.Program;
34+
35+
private _i18nFormat: string;
36+
37+
constructor(options: ExtractI18nPluginOptions) {
38+
this._setupOptions(options);
39+
}
40+
41+
private _setupOptions(options: ExtractI18nPluginOptions) {
42+
if (!options.hasOwnProperty('tsConfigPath')) {
43+
throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.');
44+
}
45+
this._tsConfigPath = options.tsConfigPath;
46+
47+
// Check the base path.
48+
const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath);
49+
let basePath = maybeBasePath;
50+
if (fs.statSync(maybeBasePath).isFile()) {
51+
basePath = path.dirname(basePath);
52+
}
53+
if (options.hasOwnProperty('basePath')) {
54+
basePath = path.resolve(process.cwd(), options.basePath);
55+
}
56+
57+
let tsConfigJson: any = null;
58+
try {
59+
tsConfigJson = JSON.parse(fs.readFileSync(this._tsConfigPath, 'utf8'));
60+
} catch (err) {
61+
throw new Error(`An error happened while parsing ${this._tsConfigPath} JSON: ${err}.`);
62+
}
63+
const tsConfig = ts.parseJsonConfigFileContent(
64+
tsConfigJson, ts.sys, basePath, null, this._tsConfigPath);
65+
66+
let fileNames = tsConfig.fileNames;
67+
if (options.hasOwnProperty('exclude')) {
68+
let exclude: string[] = typeof options.exclude == 'string'
69+
? [options.exclude as string] : (options.exclude as string[]);
70+
71+
exclude.forEach((pattern: string) => {
72+
const basePathPattern = '(' + basePath.replace(/\\/g, '/')
73+
.replace(/[\-\[\]\/{}()+?.\\^$|*]/g, '\\$&') + ')?';
74+
pattern = pattern
75+
// Replace windows path separators with forward slashes.
76+
.replace(/\\/g, '/')
77+
// Escape characters that are used normally in regexes, except stars.
78+
.replace(/[\-\[\]{}()+?.\\^$|]/g, '\\$&')
79+
// Two stars replacement.
80+
.replace(/\*\*/g, '(?:.*)')
81+
// One star replacement.
82+
.replace(/\*/g, '(?:[^/]*)')
83+
// Escape characters from the basePath and make sure it's forward slashes.
84+
.replace(/^/, basePathPattern);
85+
86+
const re = new RegExp('^' + pattern + '$');
87+
fileNames = fileNames.filter(x => !x.replace(/\\/g, '/').match(re));
88+
});
89+
} else {
90+
fileNames = fileNames.filter(fileName => !/\.spec\.ts$/.test(fileName));
91+
}
92+
this._rootFilePath = fileNames;
93+
94+
// By default messages will be generated in basePath
95+
let genDir = basePath;
96+
97+
if(options.hasOwnProperty('genDir')) {
98+
genDir = path.resolve(process.cwd(), options.genDir);
99+
}
100+
101+
console.log(genDir);
102+
103+
this._compilerOptions = tsConfig.options;
104+
this._angularCompilerOptions = Object.assign(
105+
{ genDir },
106+
this._compilerOptions,
107+
tsConfig.raw['angularCompilerOptions'],
108+
{ basePath }
109+
);
110+
111+
this._basePath = basePath;
112+
this._genDir = genDir;
113+
114+
//this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath);
115+
this._compilerHost = ts.createCompilerHost(this._compilerOptions, true);
116+
this._program = ts.createProgram(
117+
this._rootFilePath, this._compilerOptions, this._compilerHost);
118+
119+
if (options.hasOwnProperty('i18nFormat')) {
120+
this._i18nFormat = options.i18nFormat;
121+
}
122+
}
123+
124+
apply(compiler: any) {
125+
this._compiler = compiler;
126+
127+
compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb));
128+
129+
compiler.plugin('after-emit', (compilation: any, cb: any) => {
130+
this._donePromise = null;
131+
this._compilation = null;
132+
compilation._ngToolsWebpackXi18nPluginInstance = null;
133+
cb();
134+
});
135+
}
136+
137+
private _make(compilation: any, cb: (err?: any, request?: any) => void) {
138+
this._compilation = compilation;
139+
if(this._compilation._ngToolsWebpackXi18nPluginInstance) {
140+
return cb(new Error('An @ngtools/webpack xi18n plugin already exist for this compilation.'));
141+
}
142+
143+
this._compilation._ngToolsWebpackXi18nPluginInstance = this;
144+
145+
this._resourceLoader = new WebpackResourceLoader(compilation);
146+
147+
this._donePromise = Promise.resolve()
148+
.then(() => {
149+
return __NGTOOLS_PRIVATE_API_2.extractI18n({
150+
basePath: this._basePath,
151+
compilerOptions: this._compilerOptions,
152+
program: this._program,
153+
host: this._compilerHost,
154+
angularCompilerOptions: this._angularCompilerOptions,
155+
i18nFormat: this._i18nFormat,
156+
157+
readResource: (path: string) => this._resourceLoader.get(path)
158+
});
159+
})
160+
.then(() => cb(), (err: any) => {
161+
compilation.errors.push(err);
162+
cb();
163+
});
164+
165+
}
166+
}

packages/angular-cli/commands/xi18n.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@ const Command = require('../ember-cli/lib/models/command');
22

33
import {Extracti18nTask} from '../tasks/extract-i18n';
44

5+
export interface Xi18nOptions {
6+
outputPath?: string;
7+
verbose?: boolean;
8+
i18nFormat?: string;
9+
}
10+
511
const Xi18nCommand = Command.extend({
612
name: 'xi18n',
713
description: 'Extracts i18n messages from source code.',
814
works: 'insideProject',
915
availableOptions: [
1016
{
11-
name: 'format',
17+
name: 'i18n-format',
1218
type: String,
13-
default: 'xliff',
14-
aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xliff'}]}
19+
default: 'xlf',
20+
aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xlf'}]
21+
},
22+
{ name: 'output-path', type: 'Path', default: null, aliases: ['o']},
23+
{ name: 'verbose', type: Boolean, default: false},
24+
{ name: 'progress', type: Boolean, default: true }
25+
1526
],
1627
run: function (commandOptions: any) {
1728

1829
const xi18nTask = new Extracti18nTask({
1930
ui: this.ui,
20-
project: this.project,
21-
i18nFormat: commandOptions.format
31+
project: this.project
2232
});
2333

24-
return xi18nTask.run();
34+
return xi18nTask.run(commandOptions);
2535
}
2636
});
2737

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as path from 'path';
2+
import {ExtractI18nPlugin} from '../../@ngtools/webpack/src/extract_i18n_plugin';
3+
4+
export const getWebpackExtractI18nConfig = function(
5+
projectRoot: string,
6+
appConfig: any,
7+
genDir: string,
8+
i18nFormat: string):any {
9+
10+
let exclude: string[] = [];
11+
if (appConfig.test) {
12+
exclude.push(path.join(projectRoot, appConfig.root, appConfig.test));
13+
}
14+
15+
return {
16+
plugins: [
17+
new ExtractI18nPlugin({
18+
tsConfigPath: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
19+
exclude: exclude,
20+
genDir: genDir,
21+
i18nFormat: i18nFormat
22+
})
23+
]
24+
}
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {CliConfig} from './config';
2+
import {NgCliWebpackConfig} from './webpack-config';
3+
const webpackMerge = require('webpack-merge');
4+
import {getWebpackExtractI18nConfig} from './webpack-extract-i18n';
5+
6+
export class XI18nWebpackConfig extends NgCliWebpackConfig {
7+
8+
public config: any;
9+
10+
constructor(
11+
ngCliProject: any,
12+
genDir: string,
13+
buildDir: string,
14+
i18nFormat: string,
15+
verbose: boolean = false, progress: boolean = true) {
16+
super(
17+
ngCliProject,
18+
'development',
19+
'dev',
20+
buildDir,
21+
null,
22+
null,
23+
null,
24+
null,
25+
false,
26+
true,
27+
true,
28+
verbose,
29+
progress,
30+
null,
31+
'none',
32+
true);
33+
34+
const appConfig = CliConfig.fromProject().config.apps[0];
35+
36+
let config = this.config;
37+
const extractI18nConfig =
38+
getWebpackExtractI18nConfig(this.ngCliProject.root, appConfig, genDir, i18nFormat);
39+
config = webpackMerge(config, extractI18nConfig);
40+
41+
this.config = config;
42+
}
43+
}
+45-51
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,59 @@
1+
import * as webpack from 'webpack';
2+
import * as path from 'path';
3+
import * as rimraf from 'rimraf';
4+
15
const Task = require('../ember-cli/lib/models/task');
26

3-
import * as compiler from '@angular/compiler';
4-
import {Extractor} from '@angular/compiler-cli';
5-
import * as tsc from '@angular/tsc-wrapped';
6-
import * as ts from 'typescript';
7-
import * as path from 'path';
8-
import * as chalk from 'chalk';
7+
import {XI18nWebpackConfig} from '../models/webpack-xi18n-config';
8+
import {CliConfig} from '../models/config';
9+
910

1011
export const Extracti18nTask = Task.extend({
11-
run: function () {
12-
const ui = this.ui;
13-
const project = path.resolve(this.project.root, 'src');
14-
const cliOptions = new tsc.I18nExtractionCliOptions({
15-
i18nFormat: this.i18nFormat
16-
});
12+
run: function (runTaskOptions: any) {
1713

18-
function extract (
19-
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions,
20-
program: ts.Program, host: ts.CompilerHost) {
14+
const project = this.project;
2115

22-
const resourceLoader: compiler.ResourceLoader = {
23-
get: (s: string) => {
24-
if (!host.fileExists(s)) {
25-
// Return empty string to avoid extractor stop processing
26-
return Promise.resolve('');
27-
}
28-
return Promise.resolve(host.readFile(s));
29-
}
30-
};
31-
const extractor =
32-
Extractor.create(ngOptions, cliOptions.i18nFormat, program, host, resourceLoader);
16+
const appConfig = CliConfig.fromProject().config.apps[0];
3317

34-
const bundlePromise: Promise<compiler.MessageBundle> = extractor.extract();
18+
const buildDir = '.tmp';
19+
const genDir = runTaskOptions.outputPath || appConfig.root;
3520

36-
return (bundlePromise).then(messageBundle => {
37-
let ext: string;
38-
let serializer: compiler.Serializer;
39-
const format = (cliOptions.i18nFormat || 'xlf').toLowerCase();
40-
switch (format) {
41-
case 'xmb':
42-
ext = 'xmb';
43-
serializer = new compiler.Xmb();
44-
break;
45-
case 'xliff':
46-
case 'xlf':
47-
const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser());
48-
ext = 'xlf';
49-
serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG);
50-
break;
51-
default:
52-
throw new Error('Unknown i18n output format. For available formats, see \`ng help\`.');
21+
const config = new XI18nWebpackConfig(
22+
project,
23+
genDir,
24+
buildDir,
25+
runTaskOptions.i18nFormat,
26+
runTaskOptions.verbose,
27+
runTaskOptions.progress
28+
).config;
29+
30+
const webpackCompiler = webpack(config);
31+
//const statsConfig = getWebpackStatsConfig(runTaskOptions.verbose);
32+
33+
return new Promise((resolve, reject) => {
34+
const callback: webpack.compiler.CompilerCallback = (err, stats) => {
35+
if (err) {
36+
return reject(err);
5337
}
5438

55-
const dstPath = path.join(ngOptions.genDir, `messages.${ext}`);
56-
host.writeFile(dstPath, messageBundle.write(serializer), false);
57-
});
58-
}
39+
if (stats.hasErrors()) {
40+
reject();
41+
} else {
42+
resolve();
43+
}
44+
};
5945

60-
return tsc.main(project, cliOptions, extract)
61-
.catch((e) => {
62-
ui.writeLine(chalk.red(e.message));
46+
webpackCompiler.run(callback);
47+
})
48+
.then(() => {
49+
// Deletes temporary build folder
50+
rimraf.sync(path.resolve(project.root, buildDir));
51+
})
52+
.catch((err: Error) => {
53+
if (err) {
54+
this.ui.writeError('\nAn error occured during the i18n extraction:\n' + ((err && err.stack) || err));
55+
}
56+
throw err;
6357
});
6458
}
6559
});

0 commit comments

Comments
 (0)