├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __specs__ ├── __fixtures__ │ ├── tsconfigExtendsFile.json │ ├── tsconfigExtendsFileRecursive.json │ ├── tsconfigExtendsModule.json │ ├── tsconfigExtendsModuleRecursive.json │ └── tsconfigNoExtends.json ├── __snapshots__ │ └── transformer.test.js.snap ├── extends.test.js └── transformer.test.js ├── index.js ├── package-lock.json ├── package.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-native" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | plugins: ['standard', 'promise', 'jest'], 4 | rules: { 5 | 'comma-dangle': [2, 'always-multiline'], 6 | 'space-before-function-paren': [2, 'never'], 7 | }, 8 | env: { 9 | 'jest/globals': true, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '9' 5 | script: 6 | - yarn test --runInBand 7 | - yarn lint 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest All", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": [ 10 | "--runInBand" 11 | ], 12 | "console": "integratedTerminal", 13 | "internalConsoleOptions": "neverOpen", 14 | "disableOptimisticBPs": true, 15 | "windows": { 16 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 17 | } 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Jest Current File", 23 | "program": "${workspaceFolder}/node_modules/.bin/jest", 24 | "args": [ 25 | "${fileBasenameNoExtension}" 26 | ], 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen", 29 | "disableOptimisticBPs": true, 30 | "windows": { 31 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.formatOnSave": true, 4 | "prettier.semi": false, 5 | "prettier.trailingComma": "es5", 6 | "prettier.singleQuote": true, 7 | "editor.formatOnPaste": true 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.13 2 | 3 | - Add support for "extends" tsconfig option. Contribution by @alechill 4 | 5 | ## 1.2.12 6 | 7 | - Add support for RN >= 59 8 | 9 | ## 1.2.11 10 | 11 | - David Sheldrick update lockfile 12 | - David Sheldrick update readme for babel 7 13 | - Koen Punt remove resolver.sourceExts 14 | - Koen Punt update readme for latest metro bundler 15 | - aleclarson fix: add missing dependency 16 | - Andrew Goodale Simplify lib to just `es2017` 17 | - Andrew Goodale Do not recommend "dom" TS library 18 | 19 | ## 1.2.10 20 | 21 | Add support for RN >= 56 22 | 23 | Contribution by timwangdev 24 | 25 | ## 1.2.9 26 | 27 | Revert to old (sync, non-wasm) version of source-map. Not worth the trouble right now? 28 | 29 | ## 1.2.8 30 | 31 | Remove engingeStrict field in package.json, since some people claim that the version range 32 | `>=8.0.0` is being mishandled by npm (???) 33 | 34 | ## 1.2.7 35 | 36 | Add enginges field in package.json, since WebAssembly is only available in node 8+ 37 | 38 | ## 1.2.6 39 | 40 | Update source-map, which now uses WASM and is apparently a lot faster. 41 | 42 | ## 1.2.5 43 | 44 | Improve error message when failure to find tsconfig.json file 45 | 46 | Contribution by @vyshkant in #58 47 | 48 | ## 1.2.4 49 | 50 | Fix tsconfig.json resolution for Monorepos. 51 | 52 | Contribution by @ali-hk in #54 53 | 54 | ## 1.2.3 55 | 56 | Fix react native version sniffing. 57 | 58 | Contribution by @wsxyeah 59 | 60 | ## 1.2.2 61 | 62 | Remove `crypto` dependency. It's bundled with node. 63 | 64 | ## 1.2.1 65 | 66 | Fix minor bug with ast source map transformation 67 | 68 | ## 1.2.0 69 | 70 | Add implementation for getCacheKey 71 | 72 | ## 1.1.7 73 | 74 | Add source map support for RN >= 0.52 75 | 76 | ## 1.1.6 77 | 78 | Add basic support for RN >= 0.52 79 | 80 | Contribution by @olofd 81 | 82 | ## 1.1.5 83 | 84 | Borked publish, do not use. 85 | 86 | ## 1.1.4 87 | 88 | Improve README.md 89 | 90 | ## 1.1.3 91 | 92 | Add support for RN => 0.47 93 | 94 | ## 1.1.2 95 | 96 | Make typescript a peer dependency of react-native-typescript-transformer 97 | 98 | ## 1.1.1 99 | 100 | Amend support for react-native >= 0.46 in light of metro-bundler changes 101 | before the final 0.46.0 was released. 102 | 103 | Contribution by @petejkim 104 | 105 | ## 1.1.0 106 | 107 | Add tentative support for react-native >= 0.46, which uses the external 108 | `metro-bundler` package instead of an internal bundler 109 | 110 | Contribution by @Igor1201 111 | 112 | ## 1.0.13 113 | 114 | Fix readme 115 | 116 | ## 1.0.12 117 | 118 | Add support for raw mappings 119 | 120 | Contribution by @stackia 121 | 122 | ## <= 1.0.11 123 | 124 | Consult git log. Sorry. 125 | 126 | Contributions by @cliffkoh (json5) and @orta (typo fix) 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Send me the codes. Make it neat. Make it good. Y'all've been great so far. :heart: 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 David Sheldrick 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-typescript-transformer 2 | 3 | Seamlessly use TypeScript with react-native >= 0.45 4 | 5 | ## Stop! You probably don't need this package. 6 | 7 | If you are starting a new React Native project, you can follow the instructions in the React Native docs: https://facebook.github.io/react-native/docs/typescript 8 | 9 | This will set up the project to transpile your TypeScript files using Babel. 10 | 11 | Otherwise, if you're using React Native 0.57+ and you are converting an existing RN app to TS, then you can follow the configuration in this gist: https://gist.github.com/DimitryDushkin/bcf5a7f5df71113c67dbe2e890008308 12 | 13 | ### Babel Caveats 14 | 15 | Babel will not type-check your files. You'll still want to use the TypeScript compiler as a kind of linter (with the `noEmit` compiler option set to true). 16 | 17 | Also there are four rarely-used langauge features that can't be compiled with Babel. 18 | 19 | From [this blog post](https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/): 20 | 21 | - namespaces 22 | - bracket style type-assertion/cast syntax regardless of when JSX is enabled (i.e. writing `x` won’t work even in `.ts` files if JSX support is turned on, but you can instead write `x as Foo`). 23 | - enums that span multiple declarations (i.e. enum merging) 24 | - legacy-style import/export syntax (i.e. `import foo = require(...)` and `export = foo`) 25 | 26 | Don't expect this list to grow. 27 | 28 | ## I'm on RN < 0.57 or I definitely want to compile my TypeScript files using TypeScript and not Babel 29 | 30 | ### Step 1: Install 31 | 32 | yarn add --dev react-native-typescript-transformer typescript 33 | 34 | ### Step 2: Configure TypeScript 35 | 36 | Make sure your tsconfig.json has these compiler options: 37 | 38 | ```json 39 | { 40 | "compilerOptions": { 41 | "target": "es2015", 42 | "jsx": "react", 43 | "noEmit": true, 44 | "moduleResolution": "node", 45 | }, 46 | "exclude": [ 47 | "node_modules", 48 | ], 49 | } 50 | ``` 51 | 52 | See [tsconfig.json Notes](#tsconfigjson-notes) for more advanced configuration details. 53 | 54 | ### Step 3: Configure the react native packager 55 | 56 | #### RN >= 0.59 57 | 58 | In your projects root, extend `metro.config.js` so it contains the `transformer.babelTransformerPath` property: 59 | 60 | ```js 61 | module.exports = { 62 | transformer: { 63 | babelTransformerPath: require.resolve('react-native-typescript-transformer') 64 | } 65 | }; 66 | ``` 67 | 68 | #### RN >= 0.57, < 0.59 69 | 70 | Add this to your `rn-cli.config.js` (make one if you don't have one already): 71 | 72 | ```js 73 | module.exports = { 74 | transformer: { 75 | babelTransformerPath: require.resolve('react-native-typescript-transformer') 76 | } 77 | } 78 | ``` 79 | 80 | #### RN < 0.57 81 | 82 | Add this to your `rn-cli.config.js` (make one if you don't have one already): 83 | 84 | ```js 85 | module.exports = { 86 | getTransformModulePath() { 87 | return require.resolve('react-native-typescript-transformer'); 88 | }, 89 | getSourceExts() { 90 | return ['ts', 'tsx']; 91 | } 92 | } 93 | ``` 94 | 95 | If you need to run the packager directly from the command line, run the following 96 | 97 | react-native start --config /absolute/path/to/rn-cli.config.js 98 | 99 | 100 | ### Step 4: Write TypeScript code! 101 | 102 | Note that the platform-specific index files (index.ios.js, index.android.js, etc) 103 | still need to be .js files, but everything else can be TypeScript. 104 | 105 | You probably want typings for react and react-native 106 | 107 | yarn add --dev @types/react @types/react-native 108 | 109 | Note that if you run `yarn tsc` it will act as a type checker rather than a compiler. Run it with `--watch` to catch dev-time errors in all files, not just the one you're editing. 110 | 111 | ### Use tslib (Optional) 112 | 113 | yarn add tslib 114 | 115 | in tsconfig.json 116 | 117 | ```patch 118 | { 119 | "compilerOptions": { 120 | + "importHelpers": true, 121 | } 122 | } 123 | ``` 124 | 125 | Doing this should reduce your bundle size. See [this blog post](https://blog.mariusschulz.com/2016/12/16/typescript-2-1-external-helpers-library) for more details. 126 | 127 | ### Use absolute paths (Optional) 128 | 129 | Absolute paths needs to have support from both the TypeScript compiler and the react-native packager. 130 | 131 | This section will show you how to work with project structures like this: 132 | 133 | ``` 134 | 135 | ├── src 136 | │   ├── package.json 137 | │   ├── App.tsx 138 | │   ├── components 139 | │   │   ├── Banana.tsx 140 | │   ├── index.tsx 141 | ├── index.ios.js 142 | ├── package.json 143 | ├── tsconfig.json 144 | ``` 145 | 146 | Where you want to be able to `import Banana from 'src/components/Banana'` from any .ts(x) file, regardless of its place in the directory tree. 147 | 148 | #### TypeScript 149 | 150 | In `tsconfig.json`: 151 | 152 | ```patch 153 | { 154 | "compilerOptions": { 155 | + "baseUrl": "." 156 | } 157 | } 158 | ``` 159 | 160 | #### react-native 161 | 162 | For react-native you need to add one or more `package.json` files. These only need to contain the `"name"` field, and should be placed into any folders in the root of your project that you want to reference with an absolute path. The `"name"` field's value should be the name of the folder. So for me, I just added one file at `src/package.json` with the contents `{"name": "src"}`. 163 | 164 | #### Jest (Optional) 165 | 166 | If you use Jest as a test runner, add the following in your root package.json: 167 | 168 | ```patch 169 | { 170 | "jest" { 171 | + "modulePaths": [""] 172 | } 173 | } 174 | ``` 175 | 176 | ## tsconfig.json Notes 177 | 178 | - If you enable synthetic default imports with the `"allowSyntheticDefaultImports"` flag, be sure to set `"module"` to something like "es2015" to allow the es6 import/export syntax to pass through the TypeScript compiler untouched. Then Babel can compile those statements while emitting the necessary extra code to make synthetic default imports work properly. 179 | 180 | This is neccessary until TypeScript implements suport for synthetic default imports in emitted code as well as in the type checker. See [Microsoft/TypeScript#9562](https://github.com/Microsoft/TypeScript/issues/9562). 181 | 182 | - `"target"` can be anything supported by your project's Babel configuration. 183 | 184 | - `"jsx"` can also be `"react-native"` or `"preserve"`, which are functionally identical in the context of a react-native-typescript-transformer project. In this case, the JSX syntax is compiled by Babel instead of TypeScript 185 | 186 | - The source map options are not useful 187 | 188 | - You probably want to specify some base typings with the `"lib"` option. I've had success with the following: 189 | 190 | ```patch 191 | { 192 | "compilerOptions": { 193 | + "lib": [ "es2017" ], 194 | } 195 | } 196 | ``` 197 | Including the `"dom"` lib is not recommended. The React Native JavaScript runtime does not include any DOM-related APIs. See [JavaScript Environment](https://facebook.github.io/react-native/docs/javascript-environment) for more details on what web APIs React Native supports. 198 | 199 | ## Jest notes 200 | 201 | Follow the react-native setup guide for [ts-jest](https://github.com/kulshekhar/ts-jest). 202 | 203 | Alternatively, if you want to use exactly the same transformation code for both Jest and react-native check out [this comment](https://github.com/ds300/react-native-typescript-transformer/issues/21#issuecomment-330148700). 204 | 205 | Note that there have been no reports of problems arising from differences between code compiled by the `ts-jest` transformer and code compiled by `react-native-typescript-transformer`. Additionally, `ts-jest` takes care of a lot of edge cases and is more configurable. 206 | 207 | ## Avoid cyclical dependencies 208 | 209 | If you're transitioning an app from `tsc` to `react-native-typescript-transformer`, you might see runtime errors which involve imported modules being `undefined`. You almost certainly have cyclical inter-module dependencies which manifest during your app's initialization. e.g. if ModuleA is `undefined` in ModuleB it means that ModuleA (in)directly imports ModuleB. 210 | 211 | `tsc` seems to be able to mitigate some instances of these cyclical dependencies when used as a whole-app compiler. Unfortunately the module-at-a-time compilation approach that react-native's bundler supports does not permit the same optimizations. 212 | 213 | Be especially careful of "umbrella export" files which can easily introduce these cycles. 214 | 215 | ## License 216 | 217 | MIT 218 | 219 | [![Empowered by Futurice's open source sponsorship program](https://img.shields.io/badge/sponsor-futurice-ff69b4.svg)](http://futurice.com/blog/sponsoring-free-time-open-source-activities?utm_source=github&utm_medium=spice&utm_campaign=react-native-typescript-transformer) 220 | -------------------------------------------------------------------------------- /__specs__/__fixtures__/tsconfigExtendsFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfigNoExtends.json", 3 | "compilerOptions": { 4 | "tsconfigExtendsFile": true, 5 | "overrideMe": "tsconfigExtendsFile" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__specs__/__fixtures__/tsconfigExtendsFileRecursive.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfigExtendsFile.json", 3 | "compilerOptions": { 4 | "tsconfigExtendsFileRecursive": true, 5 | "overrideMe": "tsconfigExtendsFileRecursive" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__specs__/__fixtures__/tsconfigExtendsModule.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@instacart/tsconfig", 3 | "compilerOptions": { 4 | "tsconfigExtendsModule": true, 5 | "moduleResolution": "ES6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__specs__/__fixtures__/tsconfigExtendsModuleRecursive.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@instacart/tsconfig/babel", 3 | "compilerOptions": { 4 | "tsconfigExtendsModuleRecursive": true, 5 | "moduleResolution": "ES6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__specs__/__fixtures__/tsconfigNoExtends.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsconfigNoExtends": true, 4 | "overrideMe": "tsconfigNoExtends" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__specs__/__snapshots__/transformer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`the transformer does not throw errors for bad types 1`] = ` 4 | "\\"use strict\\"; 5 | 6 | var x = 5;" 7 | `; 8 | 9 | exports[`the transformer throws errors for bad syntax 1`] = `"badSyntax.tsx (2,9): '=' expected."`; 10 | 11 | exports[`the transformer works for dev mode 1`] = ` 12 | "\\"use strict\\"; 13 | 14 | Object.defineProperty(exports, \\"__esModule\\", { value: true }); 15 | function Cheese() { 16 | return { cheese: 'stilton' }; 17 | } 18 | exports.default = Cheese;" 19 | `; 20 | 21 | exports[`the transformer works for dev mode 2`] = ` 22 | Array [ 23 | Array [ 24 | 4, 25 | 0, 26 | 6, 27 | 0, 28 | ], 29 | Array [ 30 | 4, 31 | 9, 32 | 6, 33 | 0, 34 | "Cheese", 35 | ], 36 | Array [ 37 | 4, 38 | 15, 39 | 6, 40 | 0, 41 | ], 42 | Array [ 43 | 4, 44 | 18, 45 | 6, 46 | 0, 47 | ], 48 | Array [ 49 | 5, 50 | 0, 51 | 7, 52 | 2, 53 | ], 54 | Array [ 55 | 5, 56 | 11, 57 | 7, 58 | 9, 59 | ], 60 | Array [ 61 | 5, 62 | 13, 63 | 7, 64 | 10, 65 | "cheese", 66 | ], 67 | Array [ 68 | 5, 69 | 21, 70 | 7, 71 | 18, 72 | ], 73 | Array [ 74 | 5, 75 | 30, 76 | 7, 77 | 9, 78 | ], 79 | Array [ 80 | 5, 81 | 32, 82 | 7, 83 | 2, 84 | ], 85 | Array [ 86 | 6, 87 | 0, 88 | 8, 89 | 1, 90 | ], 91 | Array [ 92 | 7, 93 | 0, 94 | 6, 95 | 0, 96 | "exports", 97 | ], 98 | Array [ 99 | 7, 100 | 8, 101 | 6, 102 | 0, 103 | "default", 104 | ], 105 | Array [ 106 | 7, 107 | 15, 108 | 6, 109 | 0, 110 | ], 111 | Array [ 112 | 7, 113 | 18, 114 | 6, 115 | 0, 116 | "Cheese", 117 | ], 118 | Array [ 119 | 7, 120 | 24, 121 | 6, 122 | 0, 123 | ], 124 | ] 125 | `; 126 | 127 | exports[`the transformer works for production mode 1`] = ` 128 | "\\"use strict\\"; 129 | 130 | Object.defineProperty(exports, \\"__esModule\\", { value: true }); 131 | function Cheese() { 132 | return { cheese: 'stilton' }; 133 | } 134 | exports.default = Cheese;" 135 | `; 136 | 137 | exports[`the transformer works for production mode 2`] = ` 138 | Object { 139 | "mappings": ";;;AAKA,SAAAA,MAAA,GAAA;AACE,WAAO,EAACC,QAAQ,SAAT,EAAP;AACD;AAFDC,QAAAC,OAAA,GAAAH,MAAA", 140 | "names": Array [ 141 | "Cheese", 142 | "cheese", 143 | "exports", 144 | "default", 145 | ], 146 | "sources": Array [ 147 | "blah.tsx", 148 | ], 149 | "sourcesContent": Array [ 150 | " 151 | type Cheese = { 152 | readonly cheese: string 153 | } 154 | 155 | export default function Cheese(): Cheese { 156 | return {cheese: 'stilton'}; 157 | } 158 | ", 159 | ], 160 | "version": 3, 161 | } 162 | `; 163 | -------------------------------------------------------------------------------- /__specs__/extends.test.js: -------------------------------------------------------------------------------- 1 | const transformer = require('../') 2 | const path = require('path') 3 | 4 | describe('Config extension', () => { 5 | describe('Given a tsconfig that extends a relative config file', () => { 6 | describe('When tsconfig is loaded', () => { 7 | let config 8 | 9 | beforeAll(() => { 10 | process.env.TSCONFIG_PATH = path.resolve( 11 | __dirname, 12 | './__fixtures__/tsconfigExtendsFile.json' 13 | ) 14 | config = transformer.loadTSConfig() 15 | }) 16 | 17 | it('Should contain compiler options that are only defined in top level config', () => { 18 | expect(config.compilerOptions.tsconfigExtendsFile).toBe(true) 19 | }) 20 | 21 | it('Should retain compiler options defined in base config that are not overriden', () => { 22 | expect(config.compilerOptions.tsconfigNoExtends).toBe(true) 23 | }) 24 | 25 | it('Should override duplicate compiler options with values from highest level config', () => { 26 | expect(config.compilerOptions.overrideMe).toBe('tsconfigExtendsFile') 27 | }) 28 | 29 | afterAll(() => { 30 | delete process.env.TSCONFIG_PATH 31 | }) 32 | }) 33 | }) 34 | 35 | describe('Given a tsconfig that extends a multiple config files', () => { 36 | describe('When tsconfig is loaded', () => { 37 | let config 38 | 39 | beforeAll(() => { 40 | process.env.TSCONFIG_PATH = path.resolve( 41 | __dirname, 42 | './__fixtures__/tsconfigExtendsFileRecursive.json' 43 | ) 44 | config = transformer.loadTSConfig() 45 | }) 46 | 47 | it('Should contain compiler options that are only defined in top level config', () => { 48 | expect(config.compilerOptions.tsconfigExtendsFileRecursive).toBe(true) 49 | }) 50 | 51 | it('Should retain compiler options in middle config that are not overriden', () => { 52 | expect(config.compilerOptions.tsconfigExtendsFile).toBe(true) 53 | }) 54 | 55 | it('Should retain compiler options defined in base config that are not overriden', () => { 56 | expect(config.compilerOptions.tsconfigNoExtends).toBe(true) 57 | }) 58 | 59 | it('Should override duplicate compiler options with values from highest level config', () => { 60 | expect(config.compilerOptions.overrideMe).toBe( 61 | 'tsconfigExtendsFileRecursive' 62 | ) 63 | }) 64 | 65 | afterAll(() => { 66 | delete process.env.TSCONFIG_PATH 67 | }) 68 | }) 69 | }) 70 | 71 | describe('Given a tsconfig that extends a module config', () => { 72 | describe('When tsconfig is loaded', () => { 73 | let config 74 | 75 | beforeAll(() => { 76 | process.env.TSCONFIG_PATH = path.resolve( 77 | __dirname, 78 | './__fixtures__/tsconfigExtendsModule.json' 79 | ) 80 | config = transformer.loadTSConfig() 81 | }) 82 | 83 | afterAll(() => { 84 | delete process.env.TSCONFIG_PATH 85 | }) 86 | 87 | it('Should contain compiler options that are only defined in top level config', () => { 88 | expect(config.compilerOptions.tsconfigExtendsModule).toBe(true) 89 | }) 90 | 91 | it('Should retain compiler options defined in base config that are not overriden', () => { 92 | expect(config.compilerOptions.strict).toBe(true) 93 | }) 94 | 95 | it('Should override duplicate compiler options with values from highest level config', () => { 96 | expect(config.compilerOptions.moduleResolution).toBe('ES6') 97 | }) 98 | }) 99 | }) 100 | 101 | describe('Given a tsconfig that extends multiple module configs', () => { 102 | describe('When tsconfig is loaded', () => { 103 | let config 104 | 105 | beforeAll(() => { 106 | process.env.TSCONFIG_PATH = path.resolve( 107 | __dirname, 108 | './__fixtures__/tsconfigExtendsModuleRecursive.json' 109 | ) 110 | config = transformer.loadTSConfig() 111 | }) 112 | 113 | afterAll(() => { 114 | delete process.env.TSCONFIG_PATH 115 | }) 116 | 117 | it('Should contain compiler options that are only defined in top level config', () => { 118 | expect(config.compilerOptions.tsconfigExtendsModuleRecursive).toBe(true) 119 | }) 120 | 121 | it('Should retain compiler options in middle config that are not overriden', () => { 122 | expect(config.compilerOptions.strict).toBe(true) 123 | }) 124 | 125 | it('Should retain compiler options defined in base config that are not overriden', () => { 126 | expect(config.compilerOptions.jsx).toBe('preserve') 127 | }) 128 | 129 | it('Should override duplicate compiler options with values from highest level config', () => { 130 | expect(config.compilerOptions.moduleResolution).toBe('ES6') 131 | }) 132 | }) 133 | }) 134 | 135 | describe('Given a tsconfig that does not extend', () => { 136 | describe('When tsconfig is loaded', () => { 137 | let config 138 | 139 | beforeAll(() => { 140 | process.env.TSCONFIG_PATH = path.resolve( 141 | __dirname, 142 | './__fixtures__/tsconfigNoExtends.json' 143 | ) 144 | config = transformer.loadTSConfig() 145 | }) 146 | 147 | afterAll(() => { 148 | delete process.env.TSCONFIG_PATH 149 | }) 150 | 151 | it('Should contain compiler options that are only defined in top level config', () => { 152 | expect(config.compilerOptions.tsconfigNoExtends).toBe(true) 153 | }) 154 | 155 | it('Should override duplicate compiler options with values from highest level config', () => { 156 | expect(config.compilerOptions.overrideMe).toBe('tsconfigNoExtends') 157 | }) 158 | }) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /__specs__/transformer.test.js: -------------------------------------------------------------------------------- 1 | const file = ` 2 | type Cheese = { 3 | readonly cheese: string 4 | } 5 | 6 | export default function Cheese(): Cheese { 7 | return {cheese: 'stilton'}; 8 | } 9 | ` 10 | 11 | const transformer = require('../') 12 | 13 | describe('the transformer', () => { 14 | it('works for production mode', async () => { 15 | const result = await transformer.transform(file, 'blah.tsx', { 16 | generateSourceMaps: true, 17 | }) 18 | expect(result.code).toMatchSnapshot() 19 | expect(result.map).toMatchSnapshot() 20 | }) 21 | 22 | it('works for dev mode', async () => { 23 | const result = await transformer.transform(file, 'blah.tsx') 24 | expect(result.code).toMatchSnapshot() 25 | expect(result.map).toMatchSnapshot() 26 | }) 27 | 28 | it('throws errors for bad syntax', async () => { 29 | try { 30 | await transformer.transform(badSyntaxFile, 'badSyntax.tsx', {}) 31 | } catch (e) { 32 | expect(e.message).toMatchSnapshot() 33 | } 34 | }) 35 | 36 | it('does not throw errors for bad types', async () => { 37 | const result = await transformer.transform(badTypeFile, 'badType.tsx', {}) 38 | expect(result.code).toMatchSnapshot() 39 | }) 40 | }) 41 | 42 | const badSyntaxFile = ` 43 | const x == 7; 44 | ` 45 | 46 | const badTypeFile = ` 47 | const x: boolean = 5; 48 | ` 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const ts = require('typescript') 3 | const fs = require('fs') 4 | const findRoot = require('find-root') 5 | const os = require('os') 6 | const path = require('path') 7 | const process = require('process') 8 | const semver = require('semver') 9 | const traverse = require('babel-traverse') 10 | const crypto = require('crypto') 11 | const chalk = require('chalk') 12 | const deepmerge = require('deepmerge') 13 | 14 | let upstreamTransformer = null 15 | 16 | const reactNativeVersionString = require('react-native/package.json').version 17 | const reactNativeMinorVersion = semver(reactNativeVersionString).minor 18 | 19 | if (reactNativeMinorVersion >= 59) { 20 | upstreamTransformer = require('metro-react-native-babel-transformer/src/index') 21 | } else if (reactNativeMinorVersion >= 56) { 22 | upstreamTransformer = require('metro/src/reactNativeTransformer') 23 | } else if (reactNativeMinorVersion >= 52) { 24 | upstreamTransformer = require('metro/src/transformer') 25 | } else if (reactNativeMinorVersion >= 47) { 26 | upstreamTransformer = require('metro-bundler/src/transformer') 27 | } else if (reactNativeMinorVersion === 46) { 28 | upstreamTransformer = require('metro-bundler/build/transformer') 29 | } else { 30 | // handle RN <= 0.45 31 | const oldUpstreamTransformer = require('react-native/packager/transformer') 32 | upstreamTransformer = { 33 | transform({ src, filename, options }) { 34 | return oldUpstreamTransformer.transform(src, filename, options) 35 | }, 36 | } 37 | } 38 | 39 | const { SourceMapConsumer, SourceMapGenerator } = require('source-map') 40 | 41 | function loadJsonFile(jsonFileName) { 42 | try { 43 | const buffer = fs.readFileSync(jsonFileName) 44 | const jju = require('jju') 45 | return jju.parse(buffer.toString()) 46 | } catch (error) { 47 | throw new Error( 48 | `Error reading "${jsonFileName}":${os.EOL} ${error.message}` 49 | ) 50 | } 51 | } 52 | 53 | function getFileOrModulePath(location) { 54 | try { 55 | return require.resolve(location) 56 | } catch (e) {} 57 | } 58 | 59 | function isFile(location) { 60 | return fs.existsSync(location) 61 | } 62 | 63 | // loads config file supporting recursive extendsion from files or node modules 64 | function loadConfig(location) { 65 | let json 66 | const configPath = getFileOrModulePath(location) 67 | if (configPath) { 68 | try { 69 | json = loadJsonFile(configPath) 70 | } catch (error) { 71 | throw new Error( 72 | `Error loading config ${location}:${os.EOL} ${error.message}` 73 | ) 74 | } 75 | } else { 76 | throw new Error(`Could not load config from ${location}`) 77 | } 78 | 79 | if (typeof json.extends === 'string') { 80 | const relativeCandidate = path.join( 81 | path.dirname(configPath), 82 | `${json.extends}${json.extends.endsWith('.json') ? '' : '.json'}` 83 | ) 84 | const extendedLocation = isFile(relativeCandidate) 85 | ? relativeCandidate 86 | : json.extends 87 | const extendedJson = loadConfig(extendedLocation) 88 | json = deepmerge(extendedJson, json, { arrayMerge: (_, source) => source }) 89 | delete json.extends 90 | } 91 | return json 92 | } 93 | 94 | // only used with RN >= 52 95 | function sourceMapAstInPlace(tsMap, babelAst) { 96 | const tsConsumer = new SourceMapConsumer(tsMap) 97 | traverse.default.cheap(babelAst, node => { 98 | if (node.loc) { 99 | const originalStart = tsConsumer.originalPositionFor(node.loc.start) 100 | if (originalStart.line) { 101 | node.loc.start.line = originalStart.line 102 | node.loc.start.column = originalStart.column 103 | } 104 | const originalEnd = tsConsumer.originalPositionFor(node.loc.end) 105 | if (originalEnd.line) { 106 | node.loc.end.line = originalEnd.line 107 | node.loc.end.column = originalEnd.column 108 | } 109 | } 110 | }) 111 | } 112 | 113 | function composeRawSourceMap(tsMap, babelMap) { 114 | const tsConsumer = new SourceMapConsumer(tsMap) 115 | const composedMap = [] 116 | babelMap.forEach( 117 | ([generatedLine, generatedColumn, originalLine, originalColumn, name]) => { 118 | if (originalLine) { 119 | const tsOriginal = tsConsumer.originalPositionFor({ 120 | line: originalLine, 121 | column: originalColumn, 122 | }) 123 | if (tsOriginal.line) { 124 | if (typeof name === 'string') { 125 | composedMap.push([ 126 | generatedLine, 127 | generatedColumn, 128 | tsOriginal.line, 129 | tsOriginal.column, 130 | name, 131 | ]) 132 | } else { 133 | composedMap.push([ 134 | generatedLine, 135 | generatedColumn, 136 | tsOriginal.line, 137 | tsOriginal.column, 138 | ]) 139 | } 140 | } 141 | } 142 | } 143 | ) 144 | return composedMap 145 | } 146 | 147 | function composeSourceMaps(tsMap, babelMap, tsFileName, tsContent, babelCode) { 148 | const tsConsumer = new SourceMapConsumer(tsMap) 149 | const babelConsumer = new SourceMapConsumer(babelMap) 150 | const map = new SourceMapGenerator() 151 | map.setSourceContent(tsFileName, tsContent) 152 | babelConsumer.eachMapping( 153 | ({ 154 | source, 155 | generatedLine, 156 | generatedColumn, 157 | originalLine, 158 | originalColumn, 159 | name, 160 | }) => { 161 | if (originalLine) { 162 | const original = tsConsumer.originalPositionFor({ 163 | line: originalLine, 164 | column: originalColumn, 165 | }) 166 | if (original.line) { 167 | map.addMapping({ 168 | generated: { 169 | line: generatedLine, 170 | column: generatedColumn, 171 | }, 172 | original: { 173 | line: original.line, 174 | column: original.column, 175 | }, 176 | source: tsFileName, 177 | name: name, 178 | }) 179 | } 180 | } 181 | } 182 | ) 183 | return map.toJSON() 184 | } 185 | 186 | function loadTSConfig() { 187 | const TSCONFIG_PATH = process.env.TSCONFIG_PATH 188 | 189 | if (TSCONFIG_PATH) { 190 | const resolvedTsconfigPath = path.resolve(process.cwd(), TSCONFIG_PATH) 191 | if (isFile(resolvedTsconfigPath)) { 192 | return loadConfig(resolvedTsconfigPath) 193 | } 194 | console.warn( 195 | 'tsconfig file specified by TSCONFIG_PATH environment variable was not found' 196 | ) 197 | console.warn(`TSCONFIG_PATH = ${TSCONFIG_PATH}`) 198 | console.warn(`resolved = ${resolvedTsconfigPath}`) 199 | console.warn('looking in app root directory') 200 | } 201 | 202 | const expectedTsConfigFileName = 'tsconfig.json' 203 | 204 | let root 205 | try { 206 | root = findRoot(process.cwd(), dir => { 207 | return isFile(path.join(dir, expectedTsConfigFileName)) 208 | }) 209 | } catch (error) { 210 | console.error(`${chalk.bold(`***ERROR***`)} in react-native-typescript-transformer 211 | 212 | ${chalk.red(` Unable to find a "${expectedTsConfigFileName}" file.`)} 213 | 214 | It should be placed at the root of your project. 215 | Otherwise, you can specify another location using the TSCONFIG_PATH environment variable. 216 | 217 | `) 218 | process.exit(1) 219 | } 220 | 221 | const tsConfigPath = path.join(root, expectedTsConfigFileName) 222 | 223 | // the error message thrown by this is good enough on it's own 224 | return loadConfig(tsConfigPath) 225 | } 226 | 227 | const tsConfig = loadTSConfig() 228 | 229 | const compilerOptions = Object.assign(tsConfig.compilerOptions, { 230 | sourceMap: true, 231 | inlineSources: true, 232 | }) 233 | 234 | function getCacheKey() { 235 | const upstreamCacheKey = upstreamTransformer.getCacheKey 236 | ? upstreamTransformer.getCacheKey() 237 | : '' 238 | var key = crypto.createHash('md5') 239 | key.update(upstreamCacheKey) 240 | key.update(fs.readFileSync(__filename)) 241 | key.update(JSON.stringify(tsConfig)) 242 | return key.digest('hex') 243 | } 244 | 245 | function transform(src, filename, options) { 246 | if (typeof src === 'object') { 247 | // handle RN >= 0.46 248 | ;({ src, filename, options } = src) 249 | } 250 | 251 | if (filename.endsWith('.ts') || filename.endsWith('.tsx')) { 252 | const tsCompileResult = ts.transpileModule(src, { 253 | compilerOptions, 254 | fileName: filename, 255 | reportDiagnostics: true, 256 | }) 257 | 258 | const errors = tsCompileResult.diagnostics.filter( 259 | ({ category }) => category === ts.DiagnosticCategory.Error 260 | ) 261 | 262 | if (errors.length) { 263 | // report first error 264 | const error = errors[0] 265 | const message = ts.flattenDiagnosticMessageText(error.messageText, '\n') 266 | if (error.file) { 267 | let { line, character } = error.file.getLineAndCharacterOfPosition( 268 | error.start 269 | ) 270 | if (error.file.fileName === 'module.ts') { 271 | console.error({ 272 | error, 273 | filename, 274 | options, 275 | }) 276 | } 277 | throw new Error( 278 | `${error.file.fileName} (${line + 1},${character + 1}): ${message}` 279 | ) 280 | } else { 281 | throw new Error(message) 282 | } 283 | } 284 | 285 | const babelCompileResult = upstreamTransformer.transform({ 286 | src: tsCompileResult.outputText, 287 | filename, 288 | options, 289 | }) 290 | 291 | if (reactNativeMinorVersion >= 52) { 292 | sourceMapAstInPlace(tsCompileResult.sourceMapText, babelCompileResult.ast) 293 | return babelCompileResult 294 | } 295 | 296 | const composedMap = Array.isArray(babelCompileResult.map) 297 | ? composeRawSourceMap( 298 | tsCompileResult.sourceMapText, 299 | babelCompileResult.map 300 | ) 301 | : composeSourceMaps( 302 | tsCompileResult.sourceMapText, 303 | babelCompileResult.map, 304 | filename, 305 | src, 306 | babelCompileResult.code 307 | ) 308 | 309 | return Object.assign({}, babelCompileResult, { 310 | map: composedMap, 311 | }) 312 | } else { 313 | return upstreamTransformer.transform({ 314 | src, 315 | filename, 316 | options, 317 | }) 318 | } 319 | } 320 | 321 | module.exports = { 322 | getCacheKey, 323 | loadTSConfig, 324 | transform, 325 | } 326 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-typescript-transformer", 3 | "version": "1.2.13", 4 | "description": "TypeScript transformer for react-native", 5 | "main": "index.js", 6 | "repository": "https://github.com/ds300/react-native-typescript-transformer", 7 | "author": "David Sheldrick", 8 | "license": "MIT", 9 | "dependencies": { 10 | "babel-traverse": "^6.26.0", 11 | "chalk": "^2.4.0", 12 | "deepmerge": "^4.0.0", 13 | "find-root": "^1.1.0", 14 | "jju": "^1.3.0", 15 | "semver": "^5.4.1", 16 | "source-map": "^0.5.6" 17 | }, 18 | "peerDependencies": { 19 | "react-native": ">=0.45.0", 20 | "typescript": ">=2" 21 | }, 22 | "scripts": { 23 | "precommit": "lint-staged", 24 | "lint": "eslint index.js __specs__/**/*.js", 25 | "test": "jest" 26 | }, 27 | "lint-staged": { 28 | "*.js": [ 29 | "prettier --write --no-semi --single-quote --trailing-comma es5", 30 | "git add" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@instacart/tsconfig": "0.1.1", 35 | "babel-jest": "^20.0.0", 36 | "babel-preset-react-native": "^1.9.1", 37 | "eslint": "^3.19.0", 38 | "eslint-config-standard": "^10.2.1", 39 | "eslint-plugin-import": "^2.2.0", 40 | "eslint-plugin-jest": "^20.0.0", 41 | "eslint-plugin-node": "^4.2.2", 42 | "eslint-plugin-promise": "^3.5.0", 43 | "eslint-plugin-standard": "^3.0.1", 44 | "husky": "^0.13.3", 45 | "jest": "^20.0.0", 46 | "lint-staged": "^3.4.1", 47 | "metro-bundler": "^0.9.0", 48 | "np": "^2.15.0", 49 | "prettier": "^1.3.1", 50 | "react-native": "^0.45.1", 51 | "typescript": "^2.3.2" 52 | }, 53 | "jest": { 54 | "preset": "react-native", 55 | "transform": { 56 | "^.+\\.jsx?$": "babel-jest" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", /* here is a comment */ 5 | "strict": true 6 | } 7 | } 8 | --------------------------------------------------------------------------------