diff --git a/.gitignore b/.gitignore index f19abba4284..0c7740148be 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build *.tgz my-app* template/src/__tests__/__snapshots__/ +.awcache \ No newline at end of file diff --git a/config/jest/transform.js b/config/jest/transform.js index 3a085e93e65..889446bb8b9 100644 --- a/config/jest/transform.js +++ b/config/jest/transform.js @@ -10,5 +10,23 @@ const babelDev = require('../babel.dev'); const babelJest = require('babel-jest'); +const tsc = require('typescript'); +const babelTransformer = babelJest.createTransformer(babelDev); -module.exports = babelJest.createTransformer(babelDev); +// transpile the source with TypeScript, if needed, and then with Babel +module.exports = { + process(src, path) { + if (path.endsWith('.ts') || path.endsWith('.tsx')) { + src = tsc.transpile( + src, + { + module: tsc.ModuleKind.CommonJS, + jsx: tsc.JsxEmit.React, + }, + path, + [] + ); + } + return babelTransformer.process(src, path); + }, +}; diff --git a/config/typescript.dev.js b/config/typescript.dev.js new file mode 100644 index 00000000000..dd889e9d3fc --- /dev/null +++ b/config/typescript.dev.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +var path = require('path'); +var paths = require('./paths'); + +// strip cacheDirectory since it's a babel-loader specific option +var babelOptions = Object.assign({}, require('./babel.dev')) +delete babelOptions['cacheDirectory'] + +module.exports = { + // uses the cache to improve dev performance + useCache: true, + // when TypeScript emits a file, pass it to Babel to provide backwards compatibility + useBabel: true, + // these are the options to use + babelOptions: babelOptions, + // tell the loader where the path is + babelCore: path.join(paths.appNodeModules, 'babel-core') +}; \ No newline at end of file diff --git a/config/typescript.prod.js b/config/typescript.prod.js new file mode 100644 index 00000000000..e61f7d16a93 --- /dev/null +++ b/config/typescript.prod.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +var path = require('path'); +var paths = require('./paths'); + +module.exports = { + // when TypeScript emits a file, pass it to Babel to provide backwards compatibility + useBabel: true, + // these are the options to use + babelOptions: require('./babel.prod'), + // tell the loader where the path is + babelCore: path.join(paths.appNodeModules, 'babel-core') +}; \ No newline at end of file diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index e9d5a1f4946..549b1008bc9 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -77,7 +77,8 @@ module.exports = { // We also include JSX as a common component filename extension to support // some tools, although we do not recommend using it, see: // https://github.com/facebookincubator/create-react-app/issues/290 - extensions: ['.js', '.json', '.jsx', ''], + // We support also TypeScript ts and tsx which will be compiled later + extensions: ['.ts', '.tsx', '.js', '.json', '.jsx', ''], alias: { // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ @@ -93,15 +94,27 @@ module.exports = { }, module: { // First, run the linter. - // It's important to do this before Babel processes the JS. + // It's important to do this before Babel or TypeScript processes the JS/TS. preLoaders: [ { test: /\.(js|jsx)$/, loader: 'eslint', include: paths.appSrc, + }, + { + test: /\.tsx?$/, + loader: 'tslint', + include: paths.appSrc } ], loaders: [ + // Process TS with TypeScript and then with Babel + { + test: /\.tsx?$/, + include: paths.appSrc, + loader: 'awesome-typescript', + query: require('./typescript.dev') + }, // Process JS with Babel. { test: /\.(js|jsx)$/, diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js index be27a29e4bf..3484dfe8258 100644 --- a/config/webpack.config.prod.js +++ b/config/webpack.config.prod.js @@ -72,7 +72,8 @@ module.exports = { // We also include JSX as a common component filename extension to support // some tools, although we do not recommend using it, see: // https://github.com/facebookincubator/create-react-app/issues/290 - extensions: ['.js', '.json', '.jsx', ''], + // We support also TypeScript ts and tsx which will be compiled later + extensions: ['.ts', '.tsx', '.js', '.json', '.jsx', ''], alias: { // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ @@ -88,15 +89,27 @@ module.exports = { }, module: { // First, run the linter. - // It's important to do this before Babel processes the JS. + // It's important to do this before Babel or TypeScript processes the JS/TS. preLoaders: [ { test: /\.(js|jsx)$/, loader: 'eslint', include: paths.appSrc + }, + { + test: /\.tsx?$/, + loader: 'tslint', + include: paths.appSrc } ], loaders: [ + // Process TS with TypeScript and then with Babel + { + test: /\.tsx?$/, + include: paths.appSrc, + loader: 'awesome-typescript', + query: require('./typescript.prod') + }, // Process JS with Babel. { test: /\.(js|jsx)$/, diff --git a/package.json b/package.json index a6bf6f384ab..28cf5985b44 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "autoprefixer": "6.4.0", + "awesome-typescript-loader": "^2.2.1", "babel-core": "6.14.0", "babel-eslint": "6.1.2", "babel-jest": "15.0.0", @@ -72,6 +73,9 @@ "rimraf": "2.5.4", "strip-ansi": "3.0.1", "style-loader": "0.13.1", + "tslint": "^3.15.1", + "tslint-loader": "^2.1.5", + "typescript": "^2.0.0", "url-loader": "0.5.7", "webpack": "1.13.2", "webpack-dev-server": "1.15.1", diff --git a/scripts/eject.js b/scripts/eject.js index 3e5e18af387..0771322897c 100644 --- a/scripts/eject.js +++ b/scripts/eject.js @@ -37,6 +37,8 @@ prompt( path.join('config', 'paths.js'), path.join('config', 'env.js'), path.join('config', 'polyfills.js'), + path.join('config', 'typescript.dev.js'), + path.join('config', 'typescript.prod.js'), path.join('config', 'webpack.config.dev.js'), path.join('config', 'webpack.config.prod.js'), path.join('config', 'jest', 'CSSStub.js'), diff --git a/scripts/utils/createJestConfig.js b/scripts/utils/createJestConfig.js index 95a550be533..c6811597ea4 100644 --- a/scripts/utils/createJestConfig.js +++ b/scripts/utils/createJestConfig.js @@ -19,7 +19,9 @@ module.exports = (resolve, rootDir) => { } const config = { - moduleFileExtensions: ['jsx', 'js', 'json'], + // allow Jest to load TypeScript modules + moduleFileExtensions: ['jsx', 'js', 'json', 'ts', 'tsx'], + automock: false, moduleNameMapper: { '^[./a-zA-Z0-9$_-]+\\.(jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$': resolve('config/jest/FileStub.js'), '^[./a-zA-Z0-9$_-]+\\.css$': resolve('config/jest/CSSStub.js') diff --git a/template/src/App.js b/template/src/App.js index d7d52a7f38a..df18fdcf84a 100644 --- a/template/src/App.js +++ b/template/src/App.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import {HelloTypeScript} from './HelloTypeScript' import logo from './logo.svg'; import './App.css'; @@ -13,6 +14,7 @@ class App extends Component {

To get started, edit src/App.js and save to reload.

+ ); } diff --git a/template/src/HelloTypeScript.tsx b/template/src/HelloTypeScript.tsx new file mode 100644 index 00000000000..ee86104a884 --- /dev/null +++ b/template/src/HelloTypeScript.tsx @@ -0,0 +1,8 @@ +import * as React from "react"; +const {Component} = React; + +export class HelloTypeScript extends Component { + render() { + return

If you'd like to, you can also use TypeScript!

; + } +} \ No newline at end of file diff --git a/template/tsconfig.json b/template/tsconfig.json new file mode 100644 index 00000000000..fead9aee27c --- /dev/null +++ b/template/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2015", + "jsx": "react" + }, + "include": [ + "src" + ], + "files": [ + "./typings/index.d.ts" + ] +} \ No newline at end of file diff --git a/template/typings.json b/template/typings.json new file mode 100644 index 00000000000..692ed2d9e70 --- /dev/null +++ b/template/typings.json @@ -0,0 +1,7 @@ +{ + "name": "create-react-app", + "dependencies": { + "react": "registry:npm/react#15.0.1+20160601175240", + "react-dom": "registry:npm/react-dom#15.0.1+20160826174104" + } +} diff --git a/template/typings/index.d.ts b/template/typings/index.d.ts new file mode 100644 index 00000000000..218b1301bcb --- /dev/null +++ b/template/typings/index.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/template/typings/modules/react-dom/index.d.ts b/template/typings/modules/react-dom/index.d.ts new file mode 100644 index 00000000000..89b86ab410c --- /dev/null +++ b/template/typings/modules/react-dom/index.d.ts @@ -0,0 +1,2418 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/cantide5ga/typed-react-dom/d498c5a1056d26babef04b56139628be1f59a357/server.d.ts +declare module '~react-dom/server' { +import { ReactElement } from '~react-dom~react'; + +namespace ReactDomServer { + function renderToString(element: ReactElement): string; + function renderToStaticMarkup(element: ReactElement): string; + var version: string; +} + +export = ReactDomServer; +} +declare module 'react-dom/server' { +import main = require('~react-dom/server'); +export = main; +} + +// Generated by typings +// Source: https://raw.githubusercontent.com/cantide5ga/typed-react/42692d400db3c333394ec75bce9f6d09b5a0a769/react.d.ts +declare module '~react-dom~react' { +// Type definitions for React v0.14 +// Project: http://facebook.github.io/react/ +// Definitions by: Asana , AssureSign , Microsoft +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// Typings: Michael N. Payne + +namespace React { + + // + // React Elements + // ---------------------------------------------------------------------- + + type ReactType = string | ComponentClass | StatelessComponent; + + type Key = string | number; + type Ref = string | ((instance: T) => any); + type ComponentState = {} | void; + + interface Attributes { + key?: Key; + } + interface ClassAttributes extends Attributes { + ref?: Ref; + } + + interface ReactElement

{ + type: string | ComponentClass

| SFC

; + props: P; + key?: Key; + } + + interface SFCElement

extends ReactElement

{ + type: SFC

; + } + + type CElement> = ComponentElement; + interface ComponentElement> extends ReactElement

{ + type: ComponentClass

; + ref?: Ref; + } + + type ClassicElement

= CElement>; + + interface DOMElement

extends ReactElement

{ + type: string; + ref: Ref; + } + + interface ReactHTMLElement extends DOMElement { + } + + interface ReactSVGElement extends DOMElement { + } + + // + // Factories + // ---------------------------------------------------------------------- + + interface Factory

{ + (props?: P & Attributes, ...children: ReactNode[]): ReactElement

; + } + + interface SFCFactory

{ + (props?: P & Attributes, ...children: ReactNode[]): SFCElement

; + } + + interface ComponentFactory> { + (props?: P & ClassAttributes, ...children: ReactNode[]): CElement; + } + + type CFactory> = ComponentFactory; + type ClassicFactory

= CFactory>; + + interface DOMFactory

{ + (props?: P & ClassAttributes, ...children: ReactNode[]): DOMElement; + } + + interface HTMLFactory extends DOMFactory { + } + + interface SVGFactory extends DOMFactory { + } + + // + // React Nodes + // http://facebook.github.io/react/docs/glossary.html + // ---------------------------------------------------------------------- + + type ReactText = string | number; + type ReactChild = ReactElement | ReactText; + + // Should be Array but type aliases cannot be recursive + type ReactFragment = {} | Array; + type ReactNode = ReactChild | ReactFragment | boolean; + + // + // Top Level API + // ---------------------------------------------------------------------- + + function createClass(spec: ComponentSpec): ClassicComponentClass

; + + function createFactory

( + type: string): DOMFactory; + function createFactory

(type: SFC

): SFCFactory

; + function createFactory

( + type: ClassType, ClassicComponentClass

>): CFactory>; + function createFactory, C extends ComponentClass

>( + type: ClassType): CFactory; + function createFactory

(type: ComponentClass

| SFC

): Factory

; + + function createElement

( + type: string, + props?: P & ClassAttributes, + ...children: ReactNode[]): DOMElement; + function createElement

( + type: SFC

, + props?: P & Attributes, + ...children: ReactNode[]): SFCElement

; + function createElement

( + type: ClassType, ClassicComponentClass

>, + props?: P & ClassAttributes>, + ...children: ReactNode[]): CElement>; + function createElement, C extends ComponentClass

>( + type: ClassType, + props?: P & ClassAttributes, + ...children: ReactNode[]): CElement; + function createElement

( + type: ComponentClass

| SFC

, + props?: P & Attributes, + ...children: ReactNode[]): ReactElement

; + + function cloneElement

( + element: DOMElement, + props?: P & ClassAttributes, + ...children: ReactNode[]): DOMElement; + function cloneElement

( + element: SFCElement

, + props?: Q, // should be Q & Attributes, but then Q is inferred as {} + ...children: ReactNode[]): SFCElement

; + function cloneElement

>( + element: CElement, + props?: Q, // should be Q & ClassAttributes + ...children: ReactNode[]): CElement; + function cloneElement

( + element: ReactElement

, + props?: Q, // should be Q & Attributes + ...children: ReactNode[]): ReactElement

; + + function isValidElement

(object: {}): object is ReactElement

; + + var DOM: ReactDOM; + var PropTypes: ReactPropTypes; + var Children: ReactChildren; + + // + // Component API + // ---------------------------------------------------------------------- + + type ReactInstance = Component | Element; + + // Base component for plain JS classes + class Component implements ComponentLifecycle { + constructor(props?: P, context?: any); + setState(f: (prevState: S, props: P) => S, callback?: () => any): void; + setState(state: S, callback?: () => any): void; + forceUpdate(callBack?: () => any): void; + render(): ReactElement; + + // React.Props is now deprecated, which means that the `children` + // property is not available on `P` by default, even though you can + // always pass children as variadic arguments to `createElement`. + // In the future, if we can define its call signature conditionally + // on the existence of `children` in `P`, then we should remove this. + props: P & { children?: ReactNode }; + state: S; + context: {}; + refs: { + [key: string]: ReactInstance + }; + } + + interface ClassicComponent extends Component { + replaceState(nextState: S, callback?: () => any): void; + isMounted(): boolean; + getInitialState?(): S; + } + + interface ChildContextProvider { + getChildContext(): CC; + } + + // + // Class Interfaces + // ---------------------------------------------------------------------- + + type SFC

= StatelessComponent

; + interface StatelessComponent

{ + (props?: P, context?: any): ReactElement; + propTypes?: ValidationMap

; + contextTypes?: ValidationMap; + defaultProps?: P; + displayName?: string; + } + + interface ComponentClass

{ + new(props?: P, context?: any): Component; + propTypes?: ValidationMap

; + contextTypes?: ValidationMap; + childContextTypes?: ValidationMap; + defaultProps?: P; + displayName?: string; + } + + interface ClassicComponentClass

extends ComponentClass

{ + new(props?: P, context?: any): ClassicComponent; + getDefaultProps?(): P; + } + + /** + * We use an intersection type to infer multiple type parameters from + * a single argument, which is useful for many top-level API defs. + * See https://github.com/Microsoft/TypeScript/issues/7234 for more info. + */ + type ClassType, C extends ComponentClass

> = + C & + (new() => T) & + (new() => { props: P }); + + // + // Component Specs and Lifecycle + // ---------------------------------------------------------------------- + + interface ComponentLifecycle { + componentWillMount?(): void; + componentDidMount?(): void; + componentWillReceiveProps?(nextProps: P, nextContext: any): void; + shouldComponentUpdate?(nextProps: P, nextState: S, nextContext: any): boolean; + componentWillUpdate?(nextProps: P, nextState: S, nextContext: any): void; + componentDidUpdate?(prevProps: P, prevState: S, prevContext: any): void; + componentWillUnmount?(): void; + } + + interface Mixin extends ComponentLifecycle { + mixins?: Mixin; + statics?: { + [key: string]: any; + }; + + displayName?: string; + propTypes?: ValidationMap; + contextTypes?: ValidationMap; + childContextTypes?: ValidationMap; + + getDefaultProps?(): P; + getInitialState?(): S; + } + + interface ComponentSpec extends Mixin { + render(): ReactElement; + + [propertyName: string]: any; + } + + // + // Event System + // ---------------------------------------------------------------------- + + interface SyntheticEvent { + bubbles: boolean; + cancelable: boolean; + currentTarget: EventTarget; + defaultPrevented: boolean; + eventPhase: number; + isTrusted: boolean; + nativeEvent: Event; + preventDefault(): void; + stopPropagation(): void; + target: EventTarget; + timeStamp: Date; + type: string; + } + + interface ClipboardEvent extends SyntheticEvent { + clipboardData: DataTransfer; + } + + interface CompositionEvent extends SyntheticEvent { + data: string; + } + + interface DragEvent extends SyntheticEvent { + dataTransfer: DataTransfer; + } + + interface FocusEvent extends SyntheticEvent { + relatedTarget: EventTarget; + } + + interface FormEvent extends SyntheticEvent { + } + + interface KeyboardEvent extends SyntheticEvent { + altKey: boolean; + charCode: number; + ctrlKey: boolean; + getModifierState(key: string): boolean; + key: string; + keyCode: number; + locale: string; + location: number; + metaKey: boolean; + repeat: boolean; + shiftKey: boolean; + which: number; + } + + interface MouseEvent extends SyntheticEvent { + altKey: boolean; + button: number; + buttons: number; + clientX: number; + clientY: number; + ctrlKey: boolean; + getModifierState(key: string): boolean; + metaKey: boolean; + pageX: number; + pageY: number; + relatedTarget: EventTarget; + screenX: number; + screenY: number; + shiftKey: boolean; + } + + interface TouchEvent extends SyntheticEvent { + altKey: boolean; + changedTouches: TouchList; + ctrlKey: boolean; + getModifierState(key: string): boolean; + metaKey: boolean; + shiftKey: boolean; + targetTouches: TouchList; + touches: TouchList; + } + + interface UIEvent extends SyntheticEvent { + detail: number; + view: AbstractView; + } + + interface WheelEvent extends SyntheticEvent { + deltaMode: number; + deltaX: number; + deltaY: number; + deltaZ: number; + } + + // + // Event Handler Types + // ---------------------------------------------------------------------- + + interface EventHandler { + (event: E): void; + } + + type ReactEventHandler = EventHandler; + + type ClipboardEventHandler = EventHandler; + type CompositionEventHandler = EventHandler; + type DragEventHandler = EventHandler; + type FocusEventHandler = EventHandler; + type FormEventHandler = EventHandler; + type KeyboardEventHandler = EventHandler; + type MouseEventHandler = EventHandler; + type TouchEventHandler = EventHandler; + type UIEventHandler = EventHandler; + type WheelEventHandler = EventHandler; + + // + // Props / DOM Attributes + // ---------------------------------------------------------------------- + + /** + * @deprecated. This was used to allow clients to pass `ref` and `key` + * to `createElement`, which is no longer necessary due to intersection + * types. If you need to declare a props object before passing it to + * `createElement` or a factory, use `ClassAttributes`: + * + * ```ts + * var b: Button; + * var props: ButtonProps & ClassAttributes