From 506d1734e02823ad8094ede96a9aa4110381e586 Mon Sep 17 00:00:00 2001 From: Bruno Lemos Date: Sat, 20 Oct 2018 08:33:49 -0300 Subject: [PATCH] TypeScript support --- README.md | 4 +- docusaurus/docs/adding-typescript.md | 45 ++++++++++++++++ docusaurus/docs/running-tests.md | 4 +- docusaurus/docs/setting-up-your-editor.md | 4 +- .../docs/supported-browsers-features.md | 2 +- docusaurus/website/i18n/en.json | 3 ++ docusaurus/website/sidebars.json | 1 + packages/babel-preset-react-app/README.md | 21 ++++---- packages/babel-preset-react-app/create.js | 6 +++ packages/babel-preset-react-app/package.json | 1 + packages/react-scripts/config/paths.js | 44 ++++++++++++--- .../config/webpack.config.dev.js | 33 ++++++++++-- .../config/webpack.config.prod.js | 36 +++++++++++-- packages/react-scripts/scripts/test.js | 2 +- .../scripts/utils/createJestConfig.js | 23 +++++--- .../react-scripts/template/src/loaders.d.ts | 53 +++++++++++++++++++ .../template/src/serviceWorker.d.ts | 8 +++ test/fixtures/typescript/.disable-pnp | 0 test/fixtures/typescript/index.test.js | 8 +++ test/fixtures/typescript/package.json | 1 + test/fixtures/typescript/src/App.test.ts | 8 +++ test/fixtures/typescript/src/App.ts | 14 +++++ 22 files changed, 281 insertions(+), 40 deletions(-) create mode 100644 docusaurus/docs/adding-typescript.md create mode 100644 packages/react-scripts/template/src/loaders.d.ts create mode 100644 packages/react-scripts/template/src/serviceWorker.d.ts create mode 100644 test/fixtures/typescript/.disable-pnp create mode 100644 test/fixtures/typescript/index.test.js create mode 100644 test/fixtures/typescript/package.json create mode 100644 test/fixtures/typescript/src/App.test.ts create mode 100644 test/fixtures/typescript/src/App.ts diff --git a/README.md b/README.md index 65ed8421ddb..6928c02878d 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Please refer to the [User Guide](https://facebook.github.io/create-react-app/doc Your environment will have everything you need to build a modern single-page React app: -- React, JSX, ES6, and Flow syntax support. +- React, JSX, ES6, TypeScript and Flow syntax support. - Language extras beyond ES6 like the object spread operator. - Autoprefixed CSS, so you don’t need `-webkit-` or other prefixes. - A fast interactive unit test runner with built-in support for coverage reporting. @@ -175,8 +175,6 @@ Here’s a few common cases where you might want to try something else: - If your website is **mostly static** (for example, a portfolio or a blog), consider using [Gatsby](https://www.gatsbyjs.org/) instead. Unlike Create React App, it pre-renders the website into HTML at the build time. -- If you want to use **TypeScript**, consider using [create-react-app-typescript](https://github.com/wmonk/create-react-app-typescript). - - Finally, if you need **more customization**, check out [Neutrino](https://neutrino.js.org/) and its [React preset](https://neutrino.js.org/packages/react/). All of the above tools can work with little to no configuration. diff --git a/docusaurus/docs/adding-typescript.md b/docusaurus/docs/adding-typescript.md new file mode 100644 index 00000000000..15f75a20d4a --- /dev/null +++ b/docusaurus/docs/adding-typescript.md @@ -0,0 +1,45 @@ +--- +id: adding-typescript +title: Adding TypeScript +--- + +TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. + +Recent versions of [TypeScript](https://www.typescriptlang.org/) work with Create React App projects out of the box thanks to Babel 7. Beware that Babel 7 TypeScript does not allow some features of TypeScript such as constant enum and namespaces. + +To add TypeScript to a Create React App project, follow these steps: + +1. Run `npm install --save typescript fork-ts-checker-webpack-plugin @types/react @types/react-dom @types/jest` (or `yarn add typescript fork-ts-checker-webpack-plugin @types/react @types/react-dom @types/jest`). +2. Rename the `.js` files you want to convert. Use `.tsx` if they use JSX or `.ts` if not (e.g. `git mv src/index.js src/index.tsx`). + +3. Create a [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) at the root directory with the following content: + +```json +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "moduleResolution": "node", + "lib": ["esnext", "dom", "dom.iterable"], + "allowJs": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "jsx": "preserve", + "noEmit": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src"] +} +``` + +4. Copy [loaders.d.ts](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/src/loaders.d.ts) from the template to your `src` directory. + +Type errors will show up in the same console as the build one. + +> Note: If you prefer to run type checking separately from the build process, you can run `npm uninstall fork-ts-checker-webpack-plugin` (or `yarn remove fork-ts-checker-webpack-plugin`) to remove the `fork-ts-checker-webpack-plugin` dependency and then `npx tsc -w` on a new terminal tab. + +We recommend using [VSCode](https://code.visualstudio.com/) for a better integrated experience. + +To learn more about TypeScript, check out [its documentation](https://www.typescriptlang.org/). diff --git a/docusaurus/docs/running-tests.md b/docusaurus/docs/running-tests.md index 4d889a8ab9a..ea1b32579f0 100644 --- a/docusaurus/docs/running-tests.md +++ b/docusaurus/docs/running-tests.md @@ -109,6 +109,8 @@ import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() }); ``` +> Note: When using TypeScript with Babel, all your files need to have at least one export, otherwise you will get the error `Cannot compile namespaces when the '--isolatedModules' flag is provided.`. To fix this, you can add `export default undefined` to `src/setupTests.ts`. + > Note: Keep in mind that if you decide to "eject" before creating `src/setupTests.js`, the resulting `package.json` file won't contain any reference to it. [Read here](#initializing-test-environment) to learn how to add this after ejecting. Now you can write a smoke test with it: @@ -283,7 +285,7 @@ Example package.json: "name": "your-package", "jest": { "collectCoverageFrom": [ - "src/**/*.{js,jsx}", + "src/**/*.{js,jsx,ts,tsx}", "!/node_modules/", "!/path/to/dir/" ], diff --git a/docusaurus/docs/setting-up-your-editor.md b/docusaurus/docs/setting-up-your-editor.md index 937ed1d77bd..16f61153f3e 100644 --- a/docusaurus/docs/setting-up-your-editor.md +++ b/docusaurus/docs/setting-up-your-editor.md @@ -120,7 +120,7 @@ Next we add a 'lint-staged' field to the `package.json`, for example: // ... }, + "lint-staged": { -+ "src/**/*.{js,jsx,json,css}": [ ++ "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "prettier --single-quote --write", + "git add" + ] @@ -128,6 +128,6 @@ Next we add a 'lint-staged' field to the `package.json`, for example: "scripts": { ``` -Now, whenever you make a commit, Prettier will format the changed files automatically. You can also run `./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx}"` to format your entire project for the first time. +Now, whenever you make a commit, Prettier will format the changed files automatically. You can also run `./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"` to format your entire project for the first time. Next you might want to integrate Prettier in your favorite editor. Read the section on [Editor Integration](https://prettier.io/docs/en/editors.html) on the Prettier GitHub page. diff --git a/docusaurus/docs/supported-browsers-features.md b/docusaurus/docs/supported-browsers-features.md index 6156734e1d7..f38042f2515 100644 --- a/docusaurus/docs/supported-browsers-features.md +++ b/docusaurus/docs/supported-browsers-features.md @@ -17,7 +17,7 @@ This project supports a superset of the latest JavaScript standard. In addition - [Object Rest/Spread Properties](https://github.com/tc39/proposal-object-rest-spread) (ES2018). - [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) - [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (part of stage 3 proposal). -- [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flow.org/) syntax. +- [JSX](https://facebook.github.io/react/docs/introducing-jsx.html), [Flow](./adding-flow) and [TypeScript](./adding-typescript). Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). diff --git a/docusaurus/website/i18n/en.json b/docusaurus/website/i18n/en.json index a6cadf283d3..5df3cc56344 100644 --- a/docusaurus/website/i18n/en.json +++ b/docusaurus/website/i18n/en.json @@ -36,6 +36,9 @@ "adding-relay": { "title": "Adding Relay" }, + "adding-typescript": { + "title": "Adding TypeScript" + }, "advanced-configuration": { "title": "Advanced Configuration" }, diff --git a/docusaurus/website/sidebars.json b/docusaurus/website/sidebars.json index e774b26e8d4..219c14e82ab 100644 --- a/docusaurus/website/sidebars.json +++ b/docusaurus/website/sidebars.json @@ -31,6 +31,7 @@ "using-global-variables", "adding-bootstrap", "adding-flow", + "adding-typescript", "adding-relay", "adding-a-router", "adding-custom-environment-variables", diff --git a/packages/babel-preset-react-app/README.md b/packages/babel-preset-react-app/README.md index 1c48d8f7c95..52ad2eba4c5 100644 --- a/packages/babel-preset-react-app/README.md +++ b/packages/babel-preset-react-app/README.md @@ -32,19 +32,22 @@ Then create a file named `.babelrc` with following contents in the root folder o This preset uses the `useBuiltIns` option with [transform-object-rest-spread](http://babeljs.io/docs/plugins/transform-object-rest-spread/) and [transform-react-jsx](http://babeljs.io/docs/plugins/transform-react-jsx/), which assumes that `Object.assign` is available or polyfilled. -## Usage with TypeScript +## Usage with Flow + +Flow is enabled by default. Make sure you have a `.flowconfig` file at the root directory. You can also use the `flow` option on `.babelrc`: + +``` +{ + "presets": [["react-app", { "flow": true, "typescript": false }]] +} +``` -To use this package with [`@babel/preset-typescript`](https://www.npmjs.com/package/@babel/preset-typescript), you need to disable `@babel/preset-flow` first. +## Usage with TypeScript -You can achieve this by doing: +TypeScript is enabled by default. Make sure you have a `tsconfig.json` file at the root directory. You can also use the `typescript` option on `.babelrc`: ``` { - "presets": [ - ["react-app", { - "flow": false - }], - "@babel/typescript" - ] + "presets": [["react-app", { "flow": false, "typescript": true }]] } ``` diff --git a/packages/babel-preset-react-app/create.js b/packages/babel-preset-react-app/create.js index cdf94a63243..e7fc0c8c409 100644 --- a/packages/babel-preset-react-app/create.js +++ b/packages/babel-preset-react-app/create.js @@ -30,6 +30,11 @@ module.exports = function(api, opts, env) { var isEnvTest = env === 'test'; var isFlowEnabled = validateBoolOption('flow', opts.flow, true); + var isTypeScriptEnabled = validateBoolOption( + 'typescript', + opts.typescript, + true + ); var areHelpersEnabled = validateBoolOption('helpers', opts.helpers, true); var useAbsoluteRuntime = validateBoolOption( 'absoluteRuntime', @@ -97,6 +102,7 @@ module.exports = function(api, opts, env) { useBuiltIns: true, }, ], + isTypeScriptEnabled && [require('@babel/preset-typescript').default], ].filter(Boolean), plugins: [ // Strip flow types before any other transform, emulating the behavior diff --git a/packages/babel-preset-react-app/package.json b/packages/babel-preset-react-app/package.json index f6fa2ea4466..f783a1b411d 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -29,6 +29,7 @@ "@babel/plugin-transform-runtime": "7.1.0", "@babel/preset-env": "7.1.0", "@babel/preset-react": "7.0.0", + "@babel/preset-typescript": "7.1.0", "@babel/runtime": "7.0.0", "babel-loader": "8.0.4", "babel-plugin-dynamic-import-node": "2.2.0", diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 61bcecfa3af..f556b4b196f 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -46,6 +46,33 @@ function getServedPath(appPackageJson) { return ensureSlash(servedUrl, true); } +const moduleFileExtensions = [ + 'web.mjs', + 'mjs', + 'web.js', + 'js', + 'web.ts', + 'ts', + 'web.tsx', + 'tsx', + 'json', + 'web.jsx', + 'jsx', +]; + +// Resolve file paths in the same order as webpack +const resolveModule = (resolveFn, filePath) => { + const extension = moduleFileExtensions.find(extension => + fs.existsSync(resolveFn(`${filePath}.${extension}`)) + ); + + if (extension) { + return resolveFn(`${filePath}.${extension}`); + } + + return resolveFn(`${filePath}.js`); +}; + // config after eject: we're in ./config/ module.exports = { dotenv: resolveApp('.env'), @@ -53,11 +80,12 @@ module.exports = { appBuild: resolveApp('build'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), - appIndexJs: resolveApp('src/index.js'), + appIndexJs: resolveModule(resolveApp, 'src/index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), + appTsConfig: resolveApp('tsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), - testsSetup: resolveApp('src/setupTests.js'), + testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), publicUrl: getPublicUrl(resolveApp('package.json')), @@ -74,11 +102,12 @@ module.exports = { appBuild: resolveApp('build'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), - appIndexJs: resolveApp('src/index.js'), + appIndexJs: resolveModule(resolveApp, 'src/index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), + appTsConfig: resolveApp('tsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), - testsSetup: resolveApp('src/setupTests.js'), + testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), publicUrl: getPublicUrl(resolveApp('package.json')), @@ -105,11 +134,12 @@ if ( appBuild: resolveOwn('../../build'), appPublic: resolveOwn('template/public'), appHtml: resolveOwn('template/public/index.html'), - appIndexJs: resolveOwn('template/src/index.js'), + appIndexJs: resolveModule(resolveOwn, 'template/src/index'), appPackageJson: resolveOwn('package.json'), appSrc: resolveOwn('template/src'), + appTsConfig: resolveOwn('template/tsconfig.json'), yarnLockFile: resolveOwn('template/yarn.lock'), - testsSetup: resolveOwn('template/src/setupTests.js'), + testsSetup: resolveModule(resolveOwn, 'template/src/setupTests'), proxySetup: resolveOwn('template/src/setupProxy.js'), appNodeModules: resolveOwn('node_modules'), publicUrl: getPublicUrl(resolveOwn('package.json')), @@ -120,3 +150,5 @@ if ( }; } // @remove-on-eject-end + +module.exports.moduleFileExtensions = moduleFileExtensions; diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 29c7a8621f2..21016b99c69 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -8,7 +8,9 @@ // @remove-on-eject-end 'use strict'; +const fs = require('fs'); const path = require('path'); +const resolve = require('resolve'); const webpack = require('webpack'); const PnpWebpackPlugin = require('pnp-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -146,7 +148,7 @@ module.exports = { // https://github.com/facebook/create-react-app/issues/290 // `web` extension prefixes have been added for better support // for React Native Web. - extensions: ['.mjs', '.web.js', '.js', '.json', '.web.jsx', '.jsx'], + extensions: paths.moduleFileExtensions.map(ext => `.${ext}`), alias: { // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ @@ -220,7 +222,7 @@ module.exports = { // Process application JS with Babel. // The preset includes JSX, Flow, and some ESnext features. { - test: /\.(js|mjs|jsx)$/, + test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { @@ -353,7 +355,7 @@ module.exports = { // its runtime that would otherwise be processed through "file" loader. // Also exclude `html` and `json` extensions so they get processed // by webpacks internal loaders. - exclude: [/\.(js|mjs|jsx)$/, /\.html$/, /\.json$/], + exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], loader: require.resolve('file-loader'), options: { name: 'static/media/[name].[hash:8].[ext]', @@ -406,7 +408,30 @@ module.exports = { fileName: 'asset-manifest.json', publicPath: publicPath, }), - ], + // TypeScript type checking + fs.existsSync(paths.appTsConfig) && + (() => { + let ForkTsCheckerWebpackPlugin; + try { + ForkTsCheckerWebpackPlugin = require(resolve.sync( + 'fork-ts-checker-webpack-plugin', + { basedir: paths.appNodeModules } + )); + } catch (e) { + // Fail silently. + // Type checking using this plugin is optional. + // The user may decide to install `fork-ts-checker-webpack-plugin` or use `tsc -w`. + return null; + } + + return new ForkTsCheckerWebpackPlugin({ + async: false, + checkSyntacticErrors: true, + tsconfig: paths.appTsConfig, + watch: paths.appSrc, + }); + })(), + ].filter(Boolean), // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index ec3c19c36c1..a68ae8da048 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -8,8 +8,10 @@ // @remove-on-eject-end 'use strict'; +const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); +const resolve = require('resolve'); const PnpWebpackPlugin = require('pnp-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin'); @@ -220,7 +222,7 @@ module.exports = { // https://github.com/facebook/create-react-app/issues/290 // `web` extension prefixes have been added for better support // for React Native Web. - extensions: ['.mjs', '.web.js', '.js', '.json', '.web.jsx', '.jsx'], + extensions: paths.moduleFileExtensions.map(ext => `.${ext}`), alias: { // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ @@ -293,9 +295,9 @@ module.exports = { }, }, // Process application JS with Babel. - // The preset includes JSX, Flow, and some ESnext features. + // The preset includes JSX, Flow, TypeScript and some ESnext features. { - test: /\.(js|mjs|jsx)$/, + test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), @@ -445,7 +447,7 @@ module.exports = { // it's runtime that would otherwise be processed through "file" loader. // Also exclude `html` and `json` extensions so they get processed // by webpacks internal loaders. - exclude: [/\.(js|mjs|jsx)$/, /\.html$/, /\.json$/], + exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], options: { name: 'static/media/[name].[hash:8].[ext]', }, @@ -476,7 +478,8 @@ module.exports = { }), // Inlines the webpack runtime script. This script is too small to warrant // a network request. - shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]), + shouldInlineRuntimeChunk && + new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]), // Makes some environment variables available in index.html. // The public URL is available as %PUBLIC_URL% in index.html, e.g.: // @@ -525,6 +528,29 @@ module.exports = { new RegExp('/[^/]+\\.[^/]+$'), ], }), + // TypeScript type checking + fs.existsSync(paths.appTsConfig) && + (() => { + let ForkTsCheckerWebpackPlugin; + try { + ForkTsCheckerWebpackPlugin = require(resolve.sync( + 'fork-ts-checker-webpack-plugin', + { basedir: paths.appNodeModules } + )); + } catch (e) { + // Fail silently. + // Type checking using this plugin is optional. + // The user may decide to install `fork-ts-checker-webpack-plugin` or use `tsc -w`. + return null; + } + + return new ForkTsCheckerWebpackPlugin({ + async: false, + checkSyntacticErrors: true, + tsconfig: paths.appTsConfig, + watch: paths.appSrc, + }); + })(), ].filter(Boolean), // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. diff --git a/packages/react-scripts/scripts/test.js b/packages/react-scripts/scripts/test.js index 731924f4a3d..9102960f2cd 100644 --- a/packages/react-scripts/scripts/test.js +++ b/packages/react-scripts/scripts/test.js @@ -81,7 +81,7 @@ argv.push( // This is a very dirty workaround for https://github.com/facebook/jest/issues/5913. // We're trying to resolve the environment ourselves because Jest does it incorrectly. -// TODO: remove this (and the `resolve` dependency) as soon as it's fixed in Jest. +// TODO: remove this as soon as it's fixed in Jest. const resolve = require('resolve'); function resolveJestDefaultEnvironment(name) { const jestDir = path.dirname( diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index 6a6b84958dd..abfd92c8d3c 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -14,14 +14,17 @@ const paths = require('../../config/paths'); module.exports = (resolve, rootDir, isEjecting) => { // Use this instead of `paths.testsSetup` to avoid putting // an absolute filename into configuration after ejecting. + const setupTestsMatches = paths.testsSetup.match(/src\/setupTests\.(.+)/); + const setupTestsFileExtension = + (setupTestsMatches && setupTestsMatches[1]) || 'js'; const setupTestsFile = fs.existsSync(paths.testsSetup) - ? '/src/setupTests.js' + ? `/src/setupTests.${setupTestsFileExtension}` : undefined; // TODO: I don't know if it's safe or not to just use / as path separator // in Jest configs. We need help from somebody with Windows to determine this. const config = { - collectCoverageFrom: ['src/**/*.{js,jsx}'], + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], // TODO: this breaks Yarn PnP on eject. // But we can't simply emit this because it'll be an absolute path. @@ -38,27 +41,31 @@ module.exports = (resolve, rootDir, isEjecting) => { setupTestFrameworkScriptFile: setupTestsFile, testMatch: [ - '/src/**/__tests__/**/*.{js,jsx}', - '/src/**/?(*.)(spec|test).{js,jsx}', + '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', + '/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}', ], testEnvironment: 'jsdom', testURL: 'http://localhost', transform: { - '^.+\\.(js|jsx)$': isEjecting + '^.+\\.(js|jsx|ts|tsx)$': isEjecting ? '/node_modules/babel-jest' : resolve('config/jest/babelTransform.js'), '^.+\\.css$': resolve('config/jest/cssTransform.js'), - '^(?!.*\\.(js|jsx|css|json)$)': resolve('config/jest/fileTransform.js'), + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': resolve( + 'config/jest/fileTransform.js' + ), }, transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$', + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$', '^.+\\.module\\.(css|sass|scss)$', ], moduleNameMapper: { '^react-native$': 'react-native-web', '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', }, - moduleFileExtensions: ['web.js', 'js', 'json', 'web.jsx', 'jsx', 'node'], + moduleFileExtensions: [...paths.moduleFileExtensions, 'node'].filter( + ext => !ext.includes('mjs') + ), }; if (rootDir) { config.rootDir = rootDir; diff --git a/packages/react-scripts/template/src/loaders.d.ts b/packages/react-scripts/template/src/loaders.d.ts new file mode 100644 index 00000000000..a683144ed66 --- /dev/null +++ b/packages/react-scripts/template/src/loaders.d.ts @@ -0,0 +1,53 @@ +declare module '*.json' { + const value: any; + export default value; +} + +declare module '*.bmp' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.svg' { + import React = require('react'); + + export const ReactComponent: React.SFC>; + + const src: string; + export default src; +} + +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} + +declare module '*.module.scss' { + const classes: { [key: string]: string }; + export default classes; +} + +declare module '*.module.sass' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/packages/react-scripts/template/src/serviceWorker.d.ts b/packages/react-scripts/template/src/serviceWorker.d.ts new file mode 100644 index 00000000000..e30541a5450 --- /dev/null +++ b/packages/react-scripts/template/src/serviceWorker.d.ts @@ -0,0 +1,8 @@ +export type Config = { + onSuccess?: (registration: ServiceWorkerRegistration) => void + onUpdate?: (registration: ServiceWorkerRegistration) => void +} + +export function register(config: Config): void + +export function unregister(): void diff --git a/test/fixtures/typescript/.disable-pnp b/test/fixtures/typescript/.disable-pnp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/typescript/index.test.js b/test/fixtures/typescript/index.test.js new file mode 100644 index 00000000000..fa09a2222af --- /dev/null +++ b/test/fixtures/typescript/index.test.js @@ -0,0 +1,8 @@ +const testSetup = require('../__shared__/test-setup'); + +test('passes tests', async () => { + const { fulfilled } = await testSetup.scripts.test({ + jestEnvironment: 'node', + }); + expect(fulfilled).toBe(true); +}); diff --git a/test/fixtures/typescript/package.json b/test/fixtures/typescript/package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/test/fixtures/typescript/package.json @@ -0,0 +1 @@ +{} diff --git a/test/fixtures/typescript/src/App.test.ts b/test/fixtures/typescript/src/App.test.ts new file mode 100644 index 00000000000..cef45f281d3 --- /dev/null +++ b/test/fixtures/typescript/src/App.test.ts @@ -0,0 +1,8 @@ +import App from './App'; + +it('reads a typescript file with no syntax error', () => { + const app = new App(); + expect(App.foo.bar).toBe(true); + expect(App.foo.baz!.n).toBe(123); + expect(app.n).toBe(123); +}); diff --git a/test/fixtures/typescript/src/App.ts b/test/fixtures/typescript/src/App.ts new file mode 100644 index 00000000000..92dc792a3ea --- /dev/null +++ b/test/fixtures/typescript/src/App.ts @@ -0,0 +1,14 @@ +interface MyType { + foo: number; + bar: boolean; + baz?: { n: number }; +} + +type MyObject = Pick; + +class App { + static foo: MyObject = { bar: true, baz: { n: 123 } }; + n = App.foo.baz!.n; +} + +export default App;