From 47b11b96f8d61896b01eac592f2e2cb1ba9f3681 Mon Sep 17 00:00:00 2001 From: Wojciech Holysz Date: Fri, 24 Nov 2017 11:04:54 +0100 Subject: [PATCH] feat(@angular/cli): add support for tsx syntax Add support for tsx syntax in component files Closes #8046 --- packages/@angular/cli/commands/completion.ts | 2 +- .../@angular/cli/models/webpack-configs/common.ts | 2 +- .../@angular/cli/models/webpack-configs/test.ts | 4 ++-- .../cli/models/webpack-configs/typescript.ts | 12 ++++++------ .../plugins/named-lazy-chunks-webpack-plugin.ts | 2 +- packages/@angular/cli/tasks/schematic-run.ts | 2 +- packages/@ngtools/webpack/README.md | 9 +++++++-- .../webpack/src/angular_compiler_plugin.ts | 13 +++++++------ packages/@ngtools/webpack/src/entry_resolver.ts | 14 +++++++++++--- .../@ngtools/webpack/src/extract_i18n_plugin.ts | 2 +- packages/@ngtools/webpack/src/loader.ts | 2 +- packages/@ngtools/webpack/src/paths-plugin.ts | 4 +++- packages/@ngtools/webpack/src/plugin.ts | 15 +++++++++------ packages/@ngtools/webpack/src/refactor.ts | 5 +++-- packages/@ngtools/webpack/src/resource_loader.ts | 2 +- .../webpack/src/transformers/ast_helpers.ts | 2 +- 16 files changed, 56 insertions(+), 36 deletions(-) diff --git a/packages/@angular/cli/commands/completion.ts b/packages/@angular/cli/commands/completion.ts index 369128c5b7af..e5fb61101f77 100644 --- a/packages/@angular/cli/commands/completion.ts +++ b/packages/@angular/cli/commands/completion.ts @@ -72,7 +72,7 @@ const CompletionCommand = Command.extend({ commandOptions.all = !commandOptions.bash && !commandOptions.zsh; const commandFiles = fs.readdirSync(__dirname) - .filter(file => file.match(/\.(j|t)s$/) && !file.match(/\.d.ts$/)) + .filter(file => file.match(/\.(js|tsx?)$/) && !file.match(/\.d.ts$/)) .map(file => path.parse(file).name) .map(file => file.toLowerCase()); diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index ecb60283a19c..59a0d89454bb 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -186,7 +186,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { return { resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.tsx', '.js'], modules: ['node_modules', nodeModules], symlinks: !buildOptions.preserveSymlinks, alias diff --git a/packages/@angular/cli/models/webpack-configs/test.ts b/packages/@angular/cli/models/webpack-configs/test.ts index 00d3942d201d..681dfe37a8da 100644 --- a/packages/@angular/cli/models/webpack-configs/test.ts +++ b/packages/@angular/cli/models/webpack-configs/test.ts @@ -26,7 +26,7 @@ export function getTestConfig(wco: WebpackConfigOptions) { if (buildOptions.codeCoverage && CliConfig.fromProject()) { const codeCoverageExclude = CliConfig.fromProject().get('test.codeCoverage.exclude'); let exclude: (string | RegExp)[] = [ - /\.(e2e|spec)\.ts$/, + /\.(e2e|spec)\.tsx?$/, /node_modules/ ]; @@ -40,7 +40,7 @@ export function getTestConfig(wco: WebpackConfigOptions) { } extraRules.push({ - test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader', + test: /\.(js|tsx?)$/, loader: 'istanbul-instrumenter-loader', options: { esModules: true }, enforce: 'post', exclude diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 86b6819deeb0..0989026447b7 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -116,7 +116,7 @@ export function getNonAotConfig(wco: WebpackConfigOptions) { const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); return { - module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, + module: { rules: [{ test: /\.tsx?$/, loader: webpackLoader }] }, plugins: [ _createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }) ] }; } @@ -130,7 +130,7 @@ export function getAotConfig(wco: WebpackConfigOptions) { // Fallback to exclude spec files from AoT compilation on projects using a shared tsconfig. if (testTsConfigPath === tsConfigPath) { - let exclude = [ '**/*.spec.ts' ]; + let exclude = ['**/*.spec.ts', '**/*.spec.tsx' ]; if (appConfig.test) { exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); } @@ -146,8 +146,8 @@ export function getAotConfig(wco: WebpackConfigOptions) { } const test = AngularCompilerPlugin.isSupported() - ? /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/ - : /\.ts$/; + ? /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/ + : /\.tsx?$/; return { module: { rules: [{ test, use: [...boLoader, webpackLoader] }] }, @@ -174,7 +174,7 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions) { // Force include main and polyfills. // This is needed for AngularCompilerPlugin compatibility with existing projects, // since TS compilation there is stricter and tsconfig.spec.ts doesn't include them. - const include = [appConfig.main, appConfig.polyfills, '**/*.spec.ts']; + const include = [appConfig.main, appConfig.polyfills, '**/*.spec.ts', '**/*.spec.tsx']; if (appConfig.test) { include.push(appConfig.test); } @@ -188,7 +188,7 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions) { } return { - module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, + module: { rules: [{ test: /\.tsx?$/, loader: webpackLoader }] }, plugins: [ _createAotPlugin(wco, pluginOptions, false) ] }; } diff --git a/packages/@angular/cli/plugins/named-lazy-chunks-webpack-plugin.ts b/packages/@angular/cli/plugins/named-lazy-chunks-webpack-plugin.ts index 13cb9e4b0c1c..6bd6ef120550 100644 --- a/packages/@angular/cli/plugins/named-lazy-chunks-webpack-plugin.ts +++ b/packages/@angular/cli/plugins/named-lazy-chunks-webpack-plugin.ts @@ -35,7 +35,7 @@ export class NamedLazyChunksWebpackPlugin extends webpack.NamedChunksPlugin { ) { // Create chunkname from file request, stripping ngfactory and extension. const request = chunk.blocks[0].dependencies[0].request; - const chunkName = basename(request).replace(/(\.ngfactory)?\.(js|ts)$/, ''); + const chunkName = basename(request).replace(/(\.ngfactory)?\.(js|tsx?)$/, ''); if (!chunkName || chunkName === '') { // Bail out if something went wrong with the name. return null; diff --git a/packages/@angular/cli/tasks/schematic-run.ts b/packages/@angular/cli/tasks/schematic-run.ts index f3cddcc7c49b..e48505aff8d1 100644 --- a/packages/@angular/cli/tasks/schematic-run.ts +++ b/packages/@angular/cli/tasks/schematic-run.ts @@ -158,7 +158,7 @@ export default Task.extend({ silent: true, configs: [{ files: modifiedFiles - .filter((file: string) => /.ts$/.test(file)) + .filter((file: string) => /.tsx?$/.test(file)) .map((file: string) => path.join(projectRoot, file)) }] }); diff --git a/packages/@ngtools/webpack/README.md b/packages/@ngtools/webpack/README.md index fc3fc2529462..2eadf0c14381 100644 --- a/packages/@ngtools/webpack/README.md +++ b/packages/@ngtools/webpack/README.md @@ -6,6 +6,9 @@ Webpack plugin that AoT compiles your Angular components and modules. In your webpack config, add the following plugin and loader. +**Note**: If you are not using `.tsx` syntax, you can simplify `module.rules[0].test` RegExp to +`/(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/` + Angular version 5 and up, use `AngularCompilerPlugin`: ```typescript @@ -15,7 +18,7 @@ exports = { /* ... */ module: { rules: [ { - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/, loader: '@ngtools/webpack' } ] @@ -33,6 +36,8 @@ exports = { /* ... */ Angular version 2 and 4, use `AotPlugin`: +**Note**: If you are not using `.tsx` syntax, you can simplify `module.rules[0].test` RegExp to `/\.ts$/` + ```typescript import {AotPlugin} from '@ngtools/webpack' @@ -40,7 +45,7 @@ exports = { /* ... */ module: { rules: [ { - test: /\.ts$/, + test: /\.tsx?$/, loader: '@ngtools/webpack' } ] diff --git a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts index ed87b72a272f..becdf493b597 100644 --- a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts @@ -289,13 +289,13 @@ export class AngularCompilerPlugin implements Tapable { private _getChangedTsFiles() { return this._compilerHost.getChangedFilePaths() - .filter(k => k.endsWith('.ts') && !k.endsWith('.d.ts')) + .filter(k => k.endsWith('.tsx') || (k.endsWith('.ts') && !k.endsWith('.d.ts'))) .filter(k => this._compilerHost.fileExists(k)); } private _getChangedCompilationFiles() { return this._compilerHost.getChangedFilePaths() - .filter(k => /\.(?:ts|html|css|scss|sass|less|styl)$/.test(k)); + .filter(k => /\.(?:tsx?|html|css|scss|sass|less|styl)$/.test(k)); } private _createOrUpdateProgram() { @@ -440,7 +440,7 @@ export class AngularCompilerPlugin implements Tapable { modulePath = lazyRouteTSFile; moduleKey = lazyRouteKey; } else { - modulePath = lazyRouteTSFile.replace(/(\.d)?\.ts$/, `.ngfactory.js`); + modulePath = lazyRouteTSFile.replace(/(\.d)?\.tsx?$/, `.ngfactory.js`); moduleKey = `${lazyRouteModule}.ngfactory#${moduleName}NgFactory`; } @@ -583,8 +583,8 @@ export class AngularCompilerPlugin implements Tapable { // Wait for the plugin to be done when requesting `.ts` files directly (entry points), or // when the issuer is a `.ts` or `.ngfactory.js` file. compiler.resolvers.normal.plugin('before-resolve', (request: any, cb: () => void) => { - if (request.request.endsWith('.ts') - || (request.context.issuer && /\.ts|ngfactory\.js$/.test(request.context.issuer))) { + if ((request.request.endsWith('.ts') || request.request.endsWith('.tsx')) + || (request.context.issuer && /\.tsx?|ngfactory\.js$/.test(request.context.issuer))) { this.done!.then(() => cb(), () => cb()); } else { cb(); @@ -791,7 +791,8 @@ export class AngularCompilerPlugin implements Tapable { } } else { // Check if the TS file exists. - if (fileName.endsWith('.ts') && !this._compilerHost.fileExists(fileName, false)) { + if ((fileName.endsWith('.ts') || fileName.endsWith('.tsx')) + && !this._compilerHost.fileExists(fileName, false)) { throw new Error(`${fileName} is not part of the compilation. ` + `Please make sure it is in your tsconfig via the 'files' or 'include' property.`); } diff --git a/packages/@ngtools/webpack/src/entry_resolver.ts b/packages/@ngtools/webpack/src/entry_resolver.ts index 51dbf2358037..c646b8d4e54f 100644 --- a/packages/@ngtools/webpack/src/entry_resolver.ts +++ b/packages/@ngtools/webpack/src/entry_resolver.ts @@ -50,8 +50,16 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, if (specifier.name.text == symbolName) { // If it's a directory, load its index and recursively lookup. if (fs.statSync(module).isDirectory()) { - const indexModule = join(module, 'index.ts'); - if (fs.existsSync(indexModule)) { + let indexModule; + const indexTsModulePath = join(module, 'index.ts'); + const indexTsxModulePath = indexTsModulePath + 'x'; + if (fs.existsSync(indexTsModulePath)) { + indexModule = indexTsModulePath; + } else if (fs.existsSync(indexTsxModulePath)) { + indexModule = indexTsxModulePath; + } + + if (indexModule) { const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program); const maybeModule = _recursiveSymbolExportLookup( indexRefactor, symbolName, host, program); @@ -153,7 +161,7 @@ export function resolveEntryModuleFromMain(mainPath: string, const bootstrapSymbolName = bootstrap[0].text; const module = _symbolImportLookup(source, bootstrapSymbolName, host, program); if (module) { - return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`; + return `${module.replace(/\.tsx?$/, '')}#${bootstrapSymbolName}`; } // shrug... something bad happened and we couldn't find the import statement. diff --git a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts index 2131a0836133..b2aef51e307e 100644 --- a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts +++ b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts @@ -91,7 +91,7 @@ export class ExtractI18nPlugin implements Tapable { fileNames = fileNames.filter(x => !x.replace(/\\/g, '/').match(re)); }); } else { - fileNames = fileNames.filter(fileName => !/\.spec\.ts$/.test(fileName)); + fileNames = fileNames.filter(fileName => !/\.spec\.tsx?$/.test(fileName)); } this._rootFilePath = fileNames; diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index 0bfc273c3fed..83bb5181a256 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -586,7 +586,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s // as dependencies. // Component resources files (html and css templates) also need to be added manually for // AOT, so that this file is reloaded when they change. - if (sourceFileName.endsWith('.ts')) { + if (sourceFileName.endsWith('.ts') || sourceFileName.endsWith('.tsx')) { result.errorDependencies.forEach(dep => this.addDependency(dep)); const dependencies = plugin.getDependencies(sourceFileName); dependencies.forEach(dep => this.addDependency(dep)); diff --git a/packages/@ngtools/webpack/src/paths-plugin.ts b/packages/@ngtools/webpack/src/paths-plugin.ts index 8f05721669a9..f508ce332dfc 100644 --- a/packages/@ngtools/webpack/src/paths-plugin.ts +++ b/packages/@ngtools/webpack/src/paths-plugin.ts @@ -124,7 +124,9 @@ export class PathsPlugin implements Tapable { this._nmf.plugin('before-resolve', (request: NormalModuleFactoryRequest, callback: Callback) => { // Only work on TypeScript issuers. - if (!request.contextInfo.issuer || !request.contextInfo.issuer.endsWith('.ts')) { + if (!request.contextInfo.issuer + || !request.contextInfo.issuer.endsWith('.ts') + || !request.contextInfo.issuer.endsWith('.tsx')) { return callback(null, request); } diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 8c256a366794..177f9ea556ba 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -152,9 +152,9 @@ export class AotPlugin implements Tapable { ); } - // Default exclude to **/*.spec.ts files. + // Default exclude to **/*.spec.ts & **/*.spec.tsx files. if (!options.hasOwnProperty('exclude')) { - options['exclude'] = ['**/*.spec.ts']; + options['exclude'] = ['**/*.spec.ts', '**/*.spec.tsx']; } // Add custom excludes to default TypeScript excludes. @@ -441,11 +441,14 @@ export class AotPlugin implements Tapable { compiler.plugin('after-resolvers', (compiler: any) => { // Virtual file system. - // Wait for the plugin to be done when requesting `.ts` files directly (entry points), or - // when the issuer is a `.ts` file. + // Wait for the plugin to be done when requesting both `.ts` and `.tsx` + // files directly (entry points), or when the issuer is a `.ts` or `.tsx` file. compiler.resolvers.normal.plugin('before-resolve', (request: any, cb: () => void) => { - if (this.done && (request.request.endsWith('.ts') - || (request.context.issuer && request.context.issuer.endsWith('.ts')))) { + if (this.done && ((request.request.endsWith('.ts') || request.request.endsWith('.tsx')) + || (request.context.issuer + && (request.context.issuer.endsWith('.ts') + || request.context.issuer.endsWith('.tsx')) + ))) { this.done.then(() => cb(), () => cb()); } else { cb(); diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor.ts index 91faa5e37464..383024b8f54d 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor.ts @@ -246,7 +246,7 @@ export class TypeScriptFileRefactor { const map = SourceMapGenerator.fromSourceMap(consumer); if (this._changed) { const sourceMap = this._sourceString.generateMap({ - file: path.basename(this._fileName.replace(/\.ts$/, '.js')), + file: path.basename(this._fileName.replace(/\.tsx?$/, '.js')), source: this._fileName, hires: true, }); @@ -258,7 +258,8 @@ export class TypeScriptFileRefactor { ? this._fileName.replace(/\//g, '\\') : this._fileName; sourceMap.sources = [ fileName ]; - sourceMap.file = path.basename(fileName, '.ts') + '.js'; + const fileNameExtension = fileName.endsWith('.tsx') ? '.tsx' : '.ts'; + sourceMap.file = path.basename(fileName, fileNameExtension) + '.js'; sourceMap.sourcesContent = [ this._sourceText ]; return { outputText: result.outputText, sourceMap }; diff --git a/packages/@ngtools/webpack/src/resource_loader.ts b/packages/@ngtools/webpack/src/resource_loader.ts index e914c654e6c9..f00504800688 100644 --- a/packages/@ngtools/webpack/src/resource_loader.ts +++ b/packages/@ngtools/webpack/src/resource_loader.ts @@ -37,7 +37,7 @@ export class WebpackResourceLoader { } // Simple sanity check. - if (filePath.match(/\.[jt]s$/)) { + if (filePath.match(/\.(js|tsx?)$/)) { return Promise.reject('Cannot use a JavaScript or TypeScript file for styleUrl.'); } diff --git a/packages/@ngtools/webpack/src/transformers/ast_helpers.ts b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts index a55cd835ac7f..fc5c5b1e2394 100644 --- a/packages/@ngtools/webpack/src/transformers/ast_helpers.ts +++ b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts @@ -87,5 +87,5 @@ export function transformTypescript( } // Return the transpiled js. - return compilerHost.readFile(fileName.replace(/\.ts$/, '.js')); + return compilerHost.readFile(fileName.replace(/\.tsx?$/, '.js')); }