├── .editorconfig ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── source └── index.ts ├── tests ├── source │ ├── .baz │ │ └── index.ts │ ├── bar.ts │ ├── foo.ts │ ├── index.ts │ └── multiple-types.ts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /output/ 3 | /tests/output/ 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/node_modules/ttypescript/bin/tsc", 12 | "cwd": "${workspaceFolder}", 13 | "args": ["-p", "tests/tsconfig.json"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **DEPRECATED** 2 | I wrote this in hopes that I could continue writing reasonable looking TypeScript until the TypeScript devs came around to seeing reason and added support for import rewriting. Unfortunately, it appears they are standing their ground firmly and are unlikely to ever change their position. Along with that, changes to tsc have caused this plugin to stop working and updating it is a non-trivial task. 3 | 4 | For these reasons, I have personally stopped using this project and resorted to just adding an (incorrect) `.js` extension to all of my `.ts` imports. I won't be updating this project anymore, but it may be worthwhile to look at the forks of this project to see if someone is maintaining a fork. 5 | 6 | # Abstract 7 | Teach the TypeScript compiler to emit JavaScript files that can be run natively in the browser using es2015 module syntax. 8 | 9 | # Motivation 10 | Browsers now support loading modules natively, without needing to rely on bundlers. However, unlike in NodeJS, the browser cannot try a bunch of different paths to find a file, it must fetch the correct file in a single standard HTTP request on the first try. This means that when you do `import { Foo } from './foo'`, the browser will try to fetch a file at the path `http://my-domain/path/foo`. Unless you configure your HTTP server to serve up JS files when no extension is provided, the web server will likely not find any file at that path because the actual file lives at `http://my-domain/path/foo.js`. One potential solution to this problem is to write your TS like `import { Foo } from './foo.js'`. TypeScript is clever enough that it will realize that you _really_ meant `foo.ts` during compilation, and it will successfully find type information. However, ts-node is [not clever enough](https://github.com/TypeStrong/ts-node/issues/783) to handle these faux paths so if you want a library that works in either ts-node or browser you are out of luck. 11 | 12 | The hope is that eventually TypeScript will [add support for appending the `.js` extension as a compiler option](https://github.com/microsoft/TypeScript/issues/16577), but for the time being we'll have to do it ourself. 13 | 14 | ## Note 15 | If your project includes files with `.` in the filename (e.g., `my.modules.tests.ts`), you probably want to use https://github.com/nvandamme/typescript-transformer-append-js-extension instead, which has a bit more complex logic for deciding when to append the extension and when not to. Instructions/usage are the same, just change `@zoltu/typescript-transformer-append-js-extension` to `@nvandamme/typescript-transformer-append-js-extension` in the two places below. 16 | 17 | # Usage 18 | 1. Install `typescript`, `ttypescript`, and this transformer into your project if you don't already have them. 19 | ``` 20 | npm install --save-dev typescript 21 | npm install --save-dev ttypescript 22 | npm install --save-dev @zoltu/typescript-transformer-append-js-extension 23 | ``` 24 | 1. Add the transformer to your es2015 module `tsconfig-es.json` (or whatever `tsconfig.json` you are using to build es2015 modules) 25 | ```json 26 | // tsconfig-es.json 27 | { 28 | "compilerOptions": { 29 | "module": "es2015", 30 | "plugins": [ 31 | { 32 | "transform": "@zoltu/typescript-transformer-append-js-extension/output/index.js", 33 | "after": true, 34 | } 35 | ] 36 | }, 37 | } 38 | ``` 39 | 1. Write some typescript with normal imports 40 | ```typescript 41 | // foo.ts 42 | export function foo() { console.log('foo') } 43 | ``` 44 | ```typescript 45 | // index.ts 46 | import { foo } from './foo' 47 | foo() 48 | ``` 49 | 1. Compile using `ttsc` 50 | ``` 51 | npx ttsc --project tsconfig-es.json 52 | ``` 53 | 54 | ## Alternative Solutions 55 | 56 | The recommendation from the TypeScript team is to have all of your TS imports be of the form `import ... from './whatever.js'`. I think this is bad because `whatever.js` is what is being imported at runtime, but it doesn't exist at compile time (the compiler generates it) and it requires specialized editor tooling to navigate to the referenced file plus requires the developer to understand the intricacies of the compiler. This would be similar to C requiring that you import `foo.o` instead of `foo.h` (or something akin to that). 57 | 58 | However, if you have an existing project that has no extensions and you want to follow Microsoft's recommendation then you can use a tool like https://github.com/milahu/random/blob/master/javascript/typescript-autofix-js-import-extension.js to do a one-time update of everything in your project in a way that ensures you only hit the imports that TypeScript cares about. 59 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zoltu/typescript-transformer-append-js-extension", 3 | "version": "1.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "12.7.1", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.1.tgz", 10 | "integrity": "sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw==", 11 | "dev": true 12 | }, 13 | "arg": { 14 | "version": "4.1.0", 15 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", 16 | "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", 17 | "dev": true 18 | }, 19 | "buffer-from": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 22 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 23 | "dev": true 24 | }, 25 | "diff": { 26 | "version": "4.0.1", 27 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", 28 | "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", 29 | "dev": true 30 | }, 31 | "make-error": { 32 | "version": "1.3.5", 33 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 34 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 35 | "dev": true 36 | }, 37 | "path-parse": { 38 | "version": "1.0.6", 39 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 40 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 41 | "dev": true 42 | }, 43 | "resolve": { 44 | "version": "1.12.0", 45 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", 46 | "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", 47 | "dev": true, 48 | "requires": { 49 | "path-parse": "^1.0.6" 50 | } 51 | }, 52 | "source-map": { 53 | "version": "0.6.1", 54 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 55 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 56 | "dev": true 57 | }, 58 | "source-map-support": { 59 | "version": "0.5.12", 60 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", 61 | "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", 62 | "dev": true, 63 | "requires": { 64 | "buffer-from": "^1.0.0", 65 | "source-map": "^0.6.0" 66 | } 67 | }, 68 | "ts-node": { 69 | "version": "8.3.0", 70 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", 71 | "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", 72 | "dev": true, 73 | "requires": { 74 | "arg": "^4.1.0", 75 | "diff": "^4.0.1", 76 | "make-error": "^1.1.1", 77 | "source-map-support": "^0.5.6", 78 | "yn": "^3.0.0" 79 | } 80 | }, 81 | "ttypescript": { 82 | "version": "1.5.7", 83 | "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.7.tgz", 84 | "integrity": "sha512-qloW8S60+xWVC2bQWldYQfESNFkIgDL5/M+vAOOsuLJ1pQu0SG2vQx5DNJO2nlwSrHxD8cDuF2sVDXg6v3GG3Q==", 85 | "dev": true, 86 | "requires": { 87 | "resolve": "^1.9.0" 88 | } 89 | }, 90 | "typescript": { 91 | "version": "3.5.3", 92 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", 93 | "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", 94 | "dev": true 95 | }, 96 | "yn": { 97 | "version": "3.1.0", 98 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", 99 | "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", 100 | "dev": true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zoltu/typescript-transformer-append-js-extension", 3 | "description": "A TypeScript transformer for use with ttypescript that will append the JS extension to all relative imports that have no extension.", 4 | "version": "1.0.1", 5 | "repository": {}, 6 | "license": "Unlicense", 7 | "main": "output/index.js", 8 | "devDependencies": { 9 | "@types/node": "12.7.1", 10 | "ts-node": "8.3.0", 11 | "ttypescript": "1.5.7", 12 | "typescript": "3.5.3" 13 | }, 14 | "files": [ 15 | "/output/", 16 | "/source/", 17 | "README.md", 18 | "LICENSE" 19 | ], 20 | "scripts": { 21 | "build": "tsc", 22 | "test": "ttsc -p tests/tsconfig.json && echo Go look at tests/output/index.js to validate transformer did its job, because I'm too lazy to write a real test." 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript' 2 | import * as path from 'path' 3 | 4 | const transformer = (_: typescript.Program) => (transformationContext: typescript.TransformationContext) => (sourceFile: typescript.SourceFile) => { 5 | function visitNode(node: typescript.Node): typescript.VisitResult { 6 | if (shouldMutateModuleSpecifier(node)) { 7 | if (typescript.isImportDeclaration(node)) { 8 | const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`) 9 | return typescript.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier) 10 | } else if (typescript.isExportDeclaration(node)) { 11 | const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`) 12 | return typescript.updateExportDeclaration(node, node.decorators, node.modifiers, node.exportClause, newModuleSpecifier) 13 | } 14 | } 15 | 16 | return typescript.visitEachChild(node, visitNode, transformationContext) 17 | } 18 | 19 | function shouldMutateModuleSpecifier(node: typescript.Node): node is (typescript.ImportDeclaration | typescript.ExportDeclaration) & { moduleSpecifier: typescript.StringLiteral } { 20 | if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false 21 | if (node.moduleSpecifier === undefined) return false 22 | // only when module specifier is valid 23 | if (!typescript.isStringLiteral(node.moduleSpecifier)) return false 24 | // only when path is relative 25 | if (!node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../')) return false 26 | // only when module specifier has no extension 27 | if (path.extname(node.moduleSpecifier.text) !== '') return false 28 | return true 29 | } 30 | 31 | return typescript.visitNode(sourceFile, visitNode) 32 | } 33 | 34 | export default transformer 35 | -------------------------------------------------------------------------------- /tests/source/.baz/index.ts: -------------------------------------------------------------------------------- 1 | export function baz() { console.log('baz') } 2 | -------------------------------------------------------------------------------- /tests/source/bar.ts: -------------------------------------------------------------------------------- 1 | export function bar() { console.log('bar') } 2 | -------------------------------------------------------------------------------- /tests/source/foo.ts: -------------------------------------------------------------------------------- 1 | export function foo() { console.log('foo') } 2 | -------------------------------------------------------------------------------- /tests/source/index.ts: -------------------------------------------------------------------------------- 1 | import { foo } from './foo' 2 | import { bar } from './bar.js' 3 | import { baz } from './.baz/index' 4 | export { foo } from "./foo" 5 | export { bar } from "./bar.js" 6 | export { baz } 7 | export { Apple, Banana, Cherry } from './multiple-types' 8 | 9 | foo() 10 | bar() 11 | baz() 12 | -------------------------------------------------------------------------------- /tests/source/multiple-types.ts: -------------------------------------------------------------------------------- 1 | export interface Apple { } 2 | export class Banana {} 3 | export type Cherry = Apple | Banana 4 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "esnext", 5 | "moduleResolution": "classic", 6 | "rootDir": "source", 7 | "outDir": "output", 8 | "strict": true, 9 | "declaration": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "esModuleInterop": true, 16 | "lib": [ "es2019" ], 17 | "plugins": [ 18 | { "transform": "../source/index.ts", "after": true } 19 | ] 20 | }, 21 | "include": [ 22 | "source/**/*.ts", 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "rootDir": "source", 7 | "outDir": "output", 8 | "strict": true, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "noImplicitAny": true, 12 | "noImplicitThis": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "esModuleInterop": true, 17 | "lib": [ "es2019" ], 18 | }, 19 | "include": [ 20 | "source/**/*.ts", 21 | ], 22 | } 23 | --------------------------------------------------------------------------------