Skip to content

Commit be59a64

Browse files
committed
feat:@angular/cli add support for tsx syntax
Add support for tsx syntax in component files Closes #8046
1 parent c60e8af commit be59a64

16 files changed

+56
-36
lines changed

packages/@angular/cli/commands/completion.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const CompletionCommand = Command.extend({
7272
commandOptions.all = !commandOptions.bash && !commandOptions.zsh;
7373

7474
const commandFiles = fs.readdirSync(__dirname)
75-
.filter(file => file.match(/\.(j|t)s$/) && !file.match(/\.d.ts$/))
75+
.filter(file => file.match(/\.(js|tsx?)$/) && !file.match(/\.d.ts$/))
7676
.map(file => path.parse(file).name)
7777
.map(file => file.toLowerCase());
7878

packages/@angular/cli/models/webpack-configs/common.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) {
186186

187187
return {
188188
resolve: {
189-
extensions: ['.ts', '.js'],
189+
extensions: ['.ts', '.tsx', '.js'],
190190
modules: ['node_modules', nodeModules],
191191
symlinks: !buildOptions.preserveSymlinks,
192192
alias

packages/@angular/cli/models/webpack-configs/test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function getTestConfig(wco: WebpackConfigOptions<WebpackTestOptions>) {
2626
if (buildOptions.codeCoverage && CliConfig.fromProject()) {
2727
const codeCoverageExclude = CliConfig.fromProject().get('test.codeCoverage.exclude');
2828
let exclude: (string | RegExp)[] = [
29-
/\.(e2e|spec)\.ts$/,
29+
/\.(e2e|spec)\.tsx?$/,
3030
/node_modules/
3131
];
3232

@@ -40,7 +40,7 @@ export function getTestConfig(wco: WebpackConfigOptions<WebpackTestOptions>) {
4040
}
4141

4242
extraRules.push({
43-
test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader',
43+
test: /\.(js|tsx?)$/, loader: 'istanbul-instrumenter-loader',
4444
options: { esModules: true },
4545
enforce: 'post',
4646
exclude

packages/@angular/cli/models/webpack-configs/typescript.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function getNonAotConfig(wco: WebpackConfigOptions) {
116116
const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig);
117117

118118
return {
119-
module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] },
119+
module: { rules: [{ test: /\.tsx?$/, loader: webpackLoader }] },
120120
plugins: [ _createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }) ]
121121
};
122122
}
@@ -130,7 +130,7 @@ export function getAotConfig(wco: WebpackConfigOptions) {
130130

131131
// Fallback to exclude spec files from AoT compilation on projects using a shared tsconfig.
132132
if (testTsConfigPath === tsConfigPath) {
133-
let exclude = [ '**/*.spec.ts' ];
133+
let exclude = ['**/*.spec.ts', '**/*.spec.tsx' ];
134134
if (appConfig.test) {
135135
exclude.push(path.join(projectRoot, appConfig.root, appConfig.test));
136136
}
@@ -146,8 +146,8 @@ export function getAotConfig(wco: WebpackConfigOptions) {
146146
}
147147

148148
const test = AngularCompilerPlugin.isSupported()
149-
? /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/
150-
: /\.ts$/;
149+
? /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/
150+
: /\.tsx?$/;
151151

152152
return {
153153
module: { rules: [{ test, use: [...boLoader, webpackLoader] }] },
@@ -174,7 +174,7 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions) {
174174
// Force include main and polyfills.
175175
// This is needed for AngularCompilerPlugin compatibility with existing projects,
176176
// since TS compilation there is stricter and tsconfig.spec.ts doesn't include them.
177-
const include = [appConfig.main, appConfig.polyfills, '**/*.spec.ts'];
177+
const include = [appConfig.main, appConfig.polyfills, '**/*.spec.ts', '**/*.spec.tsx'];
178178
if (appConfig.test) {
179179
include.push(appConfig.test);
180180
}
@@ -188,7 +188,7 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions) {
188188
}
189189

190190
return {
191-
module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] },
191+
module: { rules: [{ test: /\.tsx?$/, loader: webpackLoader }] },
192192
plugins: [ _createAotPlugin(wco, pluginOptions, false) ]
193193
};
194194
}

packages/@angular/cli/plugins/named-lazy-chunks-webpack-plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class NamedLazyChunksWebpackPlugin extends webpack.NamedChunksPlugin {
3535
) {
3636
// Create chunkname from file request, stripping ngfactory and extension.
3737
const request = chunk.blocks[0].dependencies[0].request;
38-
const chunkName = basename(request).replace(/(\.ngfactory)?\.(js|ts)$/, '');
38+
const chunkName = basename(request).replace(/(\.ngfactory)?\.(js|tsx?)$/, '');
3939
if (!chunkName || chunkName === '') {
4040
// Bail out if something went wrong with the name.
4141
return null;

packages/@angular/cli/tasks/schematic-run.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default Task.extend({
158158
silent: true,
159159
configs: [{
160160
files: modifiedFiles
161-
.filter((file: string) => /.ts$/.test(file))
161+
.filter((file: string) => /.tsx?$/.test(file))
162162
.map((file: string) => path.join(projectRoot, file))
163163
}]
164164
});

packages/@ngtools/webpack/README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Webpack plugin that AoT compiles your Angular components and modules.
66

77
In your webpack config, add the following plugin and loader.
88

9+
**Note**: If you are not using `.tsx` syntax, you can simplify `module.rules[0].test` RegExp to
10+
`/(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/`
11+
912
Angular version 5 and up, use `AngularCompilerPlugin`:
1013

1114
```typescript
@@ -15,7 +18,7 @@ exports = { /* ... */
1518
module: {
1619
rules: [
1720
{
18-
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
21+
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/,
1922
loader: '@ngtools/webpack'
2023
}
2124
]
@@ -33,14 +36,16 @@ exports = { /* ... */
3336

3437
Angular version 2 and 4, use `AotPlugin`:
3538

39+
**Note**: If you are not using `.tsx` syntax, you can simplify `module.rules[0].test` RegExp to `/\.ts$/`
40+
3641
```typescript
3742
import {AotPlugin} from '@ngtools/webpack'
3843

3944
exports = { /* ... */
4045
module: {
4146
rules: [
4247
{
43-
test: /\.ts$/,
48+
test: /\.tsx?$/,
4449
loader: '@ngtools/webpack'
4550
}
4651
]

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,13 @@ export class AngularCompilerPlugin implements Tapable {
289289

290290
private _getChangedTsFiles() {
291291
return this._compilerHost.getChangedFilePaths()
292-
.filter(k => k.endsWith('.ts') && !k.endsWith('.d.ts'))
292+
.filter(k => k.endsWith('.tsx') || (k.endsWith('.ts') && !k.endsWith('.d.ts')))
293293
.filter(k => this._compilerHost.fileExists(k));
294294
}
295295

296296
private _getChangedCompilationFiles() {
297297
return this._compilerHost.getChangedFilePaths()
298-
.filter(k => /\.(?:ts|html|css|scss|sass|less|styl)$/.test(k));
298+
.filter(k => /\.(?:tsx?|html|css|scss|sass|less|styl)$/.test(k));
299299
}
300300

301301
private _createOrUpdateProgram() {
@@ -440,7 +440,7 @@ export class AngularCompilerPlugin implements Tapable {
440440
modulePath = lazyRouteTSFile;
441441
moduleKey = lazyRouteKey;
442442
} else {
443-
modulePath = lazyRouteTSFile.replace(/(\.d)?\.ts$/, `.ngfactory.js`);
443+
modulePath = lazyRouteTSFile.replace(/(\.d)?\.tsx?$/, `.ngfactory.js`);
444444
moduleKey = `${lazyRouteModule}.ngfactory#${moduleName}NgFactory`;
445445
}
446446

@@ -583,8 +583,8 @@ export class AngularCompilerPlugin implements Tapable {
583583
// Wait for the plugin to be done when requesting `.ts` files directly (entry points), or
584584
// when the issuer is a `.ts` or `.ngfactory.js` file.
585585
compiler.resolvers.normal.plugin('before-resolve', (request: any, cb: () => void) => {
586-
if (request.request.endsWith('.ts')
587-
|| (request.context.issuer && /\.ts|ngfactory\.js$/.test(request.context.issuer))) {
586+
if ((request.request.endsWith('.ts') || request.request.endsWith('.tsx'))
587+
|| (request.context.issuer && /\.tsx?|ngfactory\.js$/.test(request.context.issuer))) {
588588
this.done!.then(() => cb(), () => cb());
589589
} else {
590590
cb();
@@ -791,7 +791,8 @@ export class AngularCompilerPlugin implements Tapable {
791791
}
792792
} else {
793793
// Check if the TS file exists.
794-
if (fileName.endsWith('.ts') && !this._compilerHost.fileExists(fileName, false)) {
794+
if ((fileName.endsWith('.ts') || fileName.endsWith('.tsx'))
795+
&& !this._compilerHost.fileExists(fileName, false)) {
795796
throw new Error(`${fileName} is not part of the compilation. `
796797
+ `Please make sure it is in your tsconfig via the 'files' or 'include' property.`);
797798
}

packages/@ngtools/webpack/src/entry_resolver.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,16 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
5050
if (specifier.name.text == symbolName) {
5151
// If it's a directory, load its index and recursively lookup.
5252
if (fs.statSync(module).isDirectory()) {
53-
const indexModule = join(module, 'index.ts');
54-
if (fs.existsSync(indexModule)) {
53+
let indexModule;
54+
const indexTsModulePath = join(module, 'index.ts');
55+
const indexTsxModulePath = indexTsModulePath + 'x';
56+
if (fs.existsSync(indexTsModulePath)) {
57+
indexModule = indexTsModulePath;
58+
} else if (fs.existsSync(indexTsxModulePath)) {
59+
indexModule = indexTsxModulePath;
60+
}
61+
62+
if (indexModule) {
5563
const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program);
5664
const maybeModule = _recursiveSymbolExportLookup(
5765
indexRefactor, symbolName, host, program);
@@ -153,7 +161,7 @@ export function resolveEntryModuleFromMain(mainPath: string,
153161
const bootstrapSymbolName = bootstrap[0].text;
154162
const module = _symbolImportLookup(source, bootstrapSymbolName, host, program);
155163
if (module) {
156-
return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`;
164+
return `${module.replace(/\.tsx?$/, '')}#${bootstrapSymbolName}`;
157165
}
158166

159167
// shrug... something bad happened and we couldn't find the import statement.

packages/@ngtools/webpack/src/extract_i18n_plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class ExtractI18nPlugin implements Tapable {
9191
fileNames = fileNames.filter(x => !x.replace(/\\/g, '/').match(re));
9292
});
9393
} else {
94-
fileNames = fileNames.filter(fileName => !/\.spec\.ts$/.test(fileName));
94+
fileNames = fileNames.filter(fileName => !/\.spec\.tsx?$/.test(fileName));
9595
}
9696
this._rootFilePath = fileNames;
9797

packages/@ngtools/webpack/src/loader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
586586
// as dependencies.
587587
// Component resources files (html and css templates) also need to be added manually for
588588
// AOT, so that this file is reloaded when they change.
589-
if (sourceFileName.endsWith('.ts')) {
589+
if (sourceFileName.endsWith('.ts') || sourceFileName.endsWith('.tsx')) {
590590
result.errorDependencies.forEach(dep => this.addDependency(dep));
591591
const dependencies = plugin.getDependencies(sourceFileName);
592592
dependencies.forEach(dep => this.addDependency(dep));

packages/@ngtools/webpack/src/paths-plugin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export class PathsPlugin implements Tapable {
124124
this._nmf.plugin('before-resolve', (request: NormalModuleFactoryRequest,
125125
callback: Callback<any>) => {
126126
// Only work on TypeScript issuers.
127-
if (!request.contextInfo.issuer || !request.contextInfo.issuer.endsWith('.ts')) {
127+
if (!request.contextInfo.issuer
128+
|| !request.contextInfo.issuer.endsWith('.ts')
129+
|| !request.contextInfo.issuer.endsWith('.tsx')) {
128130
return callback(null, request);
129131
}
130132

packages/@ngtools/webpack/src/plugin.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ export class AotPlugin implements Tapable {
152152
);
153153
}
154154

155-
// Default exclude to **/*.spec.ts files.
155+
// Default exclude to **/*.spec.ts & **/*.spec.tsx files.
156156
if (!options.hasOwnProperty('exclude')) {
157-
options['exclude'] = ['**/*.spec.ts'];
157+
options['exclude'] = ['**/*.spec.ts', '**/*.spec.tsx'];
158158
}
159159

160160
// Add custom excludes to default TypeScript excludes.
@@ -441,11 +441,14 @@ export class AotPlugin implements Tapable {
441441

442442
compiler.plugin('after-resolvers', (compiler: any) => {
443443
// Virtual file system.
444-
// Wait for the plugin to be done when requesting `.ts` files directly (entry points), or
445-
// when the issuer is a `.ts` file.
444+
// Wait for the plugin to be done when requesting both `.ts` and `.tsx`
445+
// files directly (entry points), or when the issuer is a `.ts` or `.tsx` file.
446446
compiler.resolvers.normal.plugin('before-resolve', (request: any, cb: () => void) => {
447-
if (this.done && (request.request.endsWith('.ts')
448-
|| (request.context.issuer && request.context.issuer.endsWith('.ts')))) {
447+
if (this.done && ((request.request.endsWith('.ts') || request.request.endsWith('.tsx'))
448+
|| (request.context.issuer
449+
&& (request.context.issuer.endsWith('.ts')
450+
|| request.context.issuer.endsWith('.tsx'))
451+
))) {
449452
this.done.then(() => cb(), () => cb());
450453
} else {
451454
cb();

packages/@ngtools/webpack/src/refactor.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export class TypeScriptFileRefactor {
246246
const map = SourceMapGenerator.fromSourceMap(consumer);
247247
if (this._changed) {
248248
const sourceMap = this._sourceString.generateMap({
249-
file: path.basename(this._fileName.replace(/\.ts$/, '.js')),
249+
file: path.basename(this._fileName.replace(/\.tsx?$/, '.js')),
250250
source: this._fileName,
251251
hires: true,
252252
});
@@ -258,7 +258,8 @@ export class TypeScriptFileRefactor {
258258
? this._fileName.replace(/\//g, '\\')
259259
: this._fileName;
260260
sourceMap.sources = [ fileName ];
261-
sourceMap.file = path.basename(fileName, '.ts') + '.js';
261+
const fileNameExtension = fileName.endsWith('.tsx') ? '.tsx' : '.ts';
262+
sourceMap.file = path.basename(fileName, fileNameExtension) + '.js';
262263
sourceMap.sourcesContent = [ this._sourceText ];
263264

264265
return { outputText: result.outputText, sourceMap };

packages/@ngtools/webpack/src/resource_loader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class WebpackResourceLoader {
3737
}
3838

3939
// Simple sanity check.
40-
if (filePath.match(/\.[jt]s$/)) {
40+
if (filePath.match(/\.(js|tsx?)$/)) {
4141
return Promise.reject('Cannot use a JavaScript or TypeScript file for styleUrl.');
4242
}
4343

packages/@ngtools/webpack/src/transformers/ast_helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,5 @@ export function transformTypescript(
8787
}
8888

8989
// Return the transpiled js.
90-
return compilerHost.readFile(fileName.replace(/\.ts$/, '.js'));
90+
return compilerHost.readFile(fileName.replace(/\.tsx?$/, '.js'));
9191
}

0 commit comments

Comments
 (0)