Skip to content

feat:@angular/cli add support for tsx syntax #8619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@angular/cli/commands/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
2 changes: 1 addition & 1 deletion packages/@angular/cli/models/webpack-configs/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/@angular/cli/models/webpack-configs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function getTestConfig(wco: WebpackConfigOptions<WebpackTestOptions>) {
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/
];

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

extraRules.push({
test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader',
test: /\.(js|tsx?)$/, loader: 'istanbul-instrumenter-loader',
options: { esModules: true },
enforce: 'post',
exclude
Expand Down
12 changes: 6 additions & 6 deletions packages/@angular/cli/models/webpack-configs/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) ]
};
}
Expand All @@ -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));
}
Expand All @@ -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] }] },
Expand All @@ -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);
}
Expand All @@ -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) ]
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/@angular/cli/tasks/schematic-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}]
});
Expand Down
9 changes: 7 additions & 2 deletions packages/@ngtools/webpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,7 +18,7 @@ exports = { /* ... */
module: {
rules: [
{
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/,
loader: '@ngtools/webpack'
}
]
Expand All @@ -33,14 +36,16 @@ 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'

exports = { /* ... */
module: {
rules: [
{
test: /\.ts$/,
test: /\.tsx?$/,
loader: '@ngtools/webpack'
}
]
Expand Down
13 changes: 7 additions & 6 deletions packages/@ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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`;
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.`);
}
Expand Down
14 changes: 11 additions & 3 deletions packages/@ngtools/webpack/src/entry_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/@ngtools/webpack/src/extract_i18n_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/@ngtools/webpack/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
4 changes: 3 additions & 1 deletion packages/@ngtools/webpack/src/paths-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export class PathsPlugin implements Tapable {
this._nmf.plugin('before-resolve', (request: NormalModuleFactoryRequest,
callback: Callback<any>) => {
// 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);
}

Expand Down
15 changes: 9 additions & 6 deletions packages/@ngtools/webpack/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions packages/@ngtools/webpack/src/refactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -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 };
Expand Down
2 changes: 1 addition & 1 deletion packages/@ngtools/webpack/src/resource_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@ngtools/webpack/src/transformers/ast_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}