├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── benchmarks └── index.ts ├── bin ├── japaTypes.ts └── test.ts ├── index.ts ├── package.json ├── register.ts ├── src ├── Cache │ └── index.ts ├── Compiler │ └── index.ts ├── Config │ └── index.ts ├── Contracts │ └── index.ts ├── DiagnosticsReporter │ └── index.ts └── utils.ts ├── test-helpers └── index.ts ├── test ├── cache-path.spec.ts ├── cache.spec.ts ├── compiler.spec.ts └── config.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | AdonisJS is a community driven project. You are free to contribute in any of the following ways. 4 | 5 | - [Coding style](coding-style) 6 | - [Fix bugs by creating PR's](fix-bugs-by-creating-prs) 7 | - [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) 8 | - [Report security issues](report-security-issues) 9 | - [Be a part of the community](be-a-part-of-community) 10 | 11 | ## Coding style 12 | 13 | Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. 14 | 15 | ## Fix bugs by creating PR's 16 | 17 | We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. 18 | 19 | Go through the following points, before creating a new PR. 20 | 21 | 1. Create an issue discussing the bug or short-coming in the framework. 22 | 2. Once approved, go ahead and fork the REPO. 23 | 3. Make sure to start from the `develop`, since this is the upto date branch. 24 | 4. Make sure to keep commits small and relevant. 25 | 5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. 26 | 6. Once done with all the changes, create a PR against the `develop` branch. 27 | 28 | ## Share an RFC for new features or big changes 29 | 30 | Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. 31 | 32 | ### What is an RFC? 33 | 34 | RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). 35 | 36 | In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. 37 | 38 | The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. 39 | 40 | ## Report security issues 41 | 42 | All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. 43 | 44 | ## Be a part of community 45 | 46 | We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report identified bugs 4 | --- 5 | 6 | 7 | 8 | ## Prerequisites 9 | 10 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 11 | 12 | - Lots of raised issues are directly not bugs but instead are design decisions taken by us. 13 | - Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. 14 | - Ensure the issue isn't already reported. 15 | - Ensure you are reporting the bug in the correct repo. 16 | 17 | *Delete the above section and the instructions in the sections below before submitting* 18 | 19 | ## Package version 20 | 21 | 22 | ## Node.js and npm version 23 | 24 | 25 | ## Sample Code (to reproduce the issue) 26 | 27 | 28 | ## BONUS (a sample repo to reproduce the issue) 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Propose changes for adding a new feature 4 | --- 5 | 6 | 7 | 8 | ## Prerequisites 9 | 10 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 11 | 12 | ## Consider an RFC 13 | 14 | Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if 15 | 16 | - Feature introduces a breaking change 17 | - Demands lots of time and changes in the current code base. 18 | 19 | *Delete the above section and the instructions in the sections below before submitting* 20 | 21 | ## Why this feature is required (specific use-cases will be appreciated)? 22 | 23 | 24 | ## Have you tried any other work arounds? 25 | 26 | 27 | ## Are you willing to work on it with little guidance? 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/require-ts/blob/master/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - "Type: Security" 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: "Status: Abandoned" 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | linux: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 14.15.4 12 | - 17.x 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install 20 | run: npm install 21 | - name: Run tests 22 | run: npm test 23 | windows: 24 | runs-on: windows-latest 25 | strategy: 26 | matrix: 27 | node-version: 28 | - 14.15.4 29 | - 17.x 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Install 37 | run: npm install 38 | - name: Run tests 39 | run: npm test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | shrinkwrap.yaml 13 | package-lock.json 14 | test/__app 15 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | config.json 4 | .eslintrc.json 5 | package.json 6 | *.html 7 | *.txt 8 | *.md 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Harminder Virk, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 |

Typescript Compiler

10 |

In memory Typescript compiler for Node.js with support for caching and custom transformers

11 |
12 | 13 |
14 | 15 |
16 | 17 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] 18 | 19 |
20 | 21 | ## Introduction 22 | 23 | Require ts is a module similar to [ts-node](https://github.com/TypeStrong/ts-node) with a handful of differences. 24 | 25 | The idea is to hook into the lifecycle of Node.js `require` calls and compile Typescript on the fly (in memory) 26 | 27 | In case, if you are not aware, Node.js has first class support for registering custom [require extensions](https://gist.github.com/jamestalmage/df922691475cff66c7e6) to resolve and compile files with a certain extension. For example: 28 | 29 | ```ts 30 | require.extenstions['.ts'] = function (module, filename) { 31 | var content = fs.readFileSync(filename, 'utf8') 32 | module._compile(content, filename) 33 | } 34 | ``` 35 | 36 | If we replace the function body of the example with the Typescript compiler API, the we basically get in-memory typescript compilation. However, there are many other things to manage. 37 | 38 | - Making source-maps to work, so that the error points to the Typescript code and not the compiled in memory Javascript. 39 | - Support for typescript extensions 40 | - Introducing some sort of caching to avoid re-compiling the unchanged files. Typescript compiler is not one of the fastest compilers, so caching is required. 41 | 42 | ## Goals 43 | 44 | Following are the goals for writing this module 45 | 46 | - Able to work with Typescript without setting up a on-disk compiler 47 | - Keeping the in-memory compilation fast. For this, we do not perform type checking. Your IDE or text editor should do it. 48 | - Cache the compiled output on disk so that we can avoid re-compiling the unchanged files. A decent project has 100s of source files and we usually don't change all of them together. Also compiled cache is not same as the compiled output. 49 | - Expose helper functions for watchers to clear the cache. Most of the Node.js apps use some kind of a watcher to watch for file changes and then restart the process. The helpers exposed by this package, allows the watcher to cleanup cache of the changed file. 50 | - Add support for custom transformers. 51 | 52 | ## Usage 53 | 54 | This module is pre-configured with all the AdonisJS applications and ideally you won't have to dig into the setup process yourself. However, if you are using it outside of AdonisJS, then follow the following setup process. 55 | 56 | ```sh 57 | npm i -D @adonisjs/require-ts 58 | ``` 59 | 60 | And then require it as a Node.js require hook 61 | 62 | ```sh 63 | node -r @adonisjs/require-ts/build/register app.ts 64 | ``` 65 | 66 | I have personally created a bash alias for the above command. 67 | 68 | ```sh 69 | alias tsnode="node -r @adonisjs/require-ts/build/register" 70 | ``` 71 | 72 | and then run it as follows 73 | 74 | ```sh 75 | tsnode app.ts 76 | ``` 77 | 78 | ## Programmatic usage 79 | 80 | The main goal of this package is to expose a programmatic API that others can use to create their own build tools or commands. 81 | 82 | ### `register` 83 | 84 | ```ts 85 | const { register } = require('@adonisjs/require-ts') 86 | 87 | /** 88 | * Require ts will resolve the "tsconfig.json" file from this 89 | * path. tsconfig.json file is required to compile the code as * per the project requirements 90 | */ 91 | const appRoot = __dirname 92 | 93 | const options = { 94 | cache: true, 95 | cachePath: join(require.resolve('node_modules'), '.cache/your-app-name'), 96 | transformers: { 97 | before: [], 98 | after: [], 99 | afterDeclarations: [], 100 | }, 101 | } 102 | 103 | register(appRoot, options) 104 | 105 | /** 106 | * From here on you can import the typescript code 107 | */ 108 | require('./typescript-app-entrypoint.ts') 109 | ``` 110 | 111 | The `register` method accepts an optional object for configuring the cache and executing transformers. 112 | 113 | - `cache`: Whether or not to configure the cache 114 | - `cachePath`: Where to write the cached output 115 | - `transformers`: An object with transformers to be executed at different lifecycles. Read [transformers](#transformers) section. 116 | 117 | The register method adds two global properties to the Node.js global namespace. 118 | 119 | - `compiler`: Reference to the compiler, that is compiling the source code. You can access it as follows: 120 | ```ts 121 | const { symbols } = require('@adonisjs/require-ts') 122 | console.log(global[symbols.compiler]) 123 | ``` 124 | - `config`: Reference to the config parser, that parses the `tsconfig.json` file. You can access it as follows: 125 | ```ts 126 | const { symbols } = require('@adonisjs/require-ts') 127 | console.log(global[symbols.config]) 128 | ``` 129 | 130 | ### `getWatcherHelpers` 131 | 132 | The watcher helpers allows the watchers to cleanup the cache at different events. Here's how you can use it 133 | 134 | ```ts 135 | const { getWatcherHelpers } = require('@adonisjs/require-ts') 136 | 137 | /** 138 | * Require ts will resolve the "tsconfig.json" file from this 139 | * path. tsconfig.json file is required to compile the code as * per the project requirements 140 | */ 141 | const appRoot = __dirname 142 | 143 | /** 144 | * Same as what you passed to the `register` method 145 | */ 146 | const cachePath = join(require.resolve('node_modules'), '.cache/your-app-name') 147 | 148 | const helpers = getWatcherHelpers(appRoot, cachePath) 149 | 150 | helpers.clear('./relative/path/from/app/root') 151 | ``` 152 | 153 | This is how you should set up the flow 154 | 155 | - Clean the entire cache when you start the watcher for the first time. `helpers.clear()`. No arguments means, clear everything 156 | - Clean the cache for the file that just changed. `helpers.clear('./file/path')` 157 | - Check if the config file has changed in a way that will impact the compiled output. If yes, then clear all the cached files. 158 | 159 | ```ts 160 | if (helpers.isConfigStale()) { 161 | helpers.clear() // clear all files from cache 162 | } 163 | ``` 164 | 165 | ## Caching 166 | 167 | Caching is really important for us. Reading the compiled output from the disk is way faster than re-compiling the same file with Typescript. 168 | 169 | This is how we perform caching. 170 | 171 | - Create a `md5 hash` of the file contents using the [rev-hash](https://www.npmjs.com/package/rev-hash) package. 172 | - Checking the cache output with the same name as the hash. 173 | - If the file exists, pass its output to Node.js `module._compile` method. 174 | - Otherwise, compile the file using the Typescript compiler API and cache it on the disk 175 | 176 | The module itself doesn't bother itself with clearing the stale cached files. Meaning, the cache grows like grass. 177 | 178 | However, we expose helper functions to cleanup the cache. Usually, you will be using them with a file watcher like `nodemon` to clear the cache for the changed file. 179 | 180 | ## Differences from ts-node 181 | 182 | `ts-node` and `require-ts` has a few but important differences. 183 | 184 | - `ts-node` also type checks the Typescript code. They do allow configuring ts-node without type checking. But overall, they pay extra setup cost just by even considering type checking. 185 | - `ts-node` has no concept of on-disk caching. This is a deal breaker for us. **Then why not contribute this feature to ts-node?**. Well, we can. But in order for caching to work properly, the module need to expose the helpers for watchers to cleanup the cache and I don't think, ts-node will bother itself with this. 186 | - `ts-node` ships with inbuilt REPL. We don't want to bother ourselves with this. Again, keeping the codebase focused on a single use case. You can use [@adonisjs/repl](https://github.com/adonisjs/repl) for the REPL support. 187 | 188 | These are small differences, but has biggest impact overall. 189 | 190 | ## Transformers 191 | 192 | Typescript compiler API supports transformers to transform/mutate the AST during the compile phase. [Here](https://github.com/madou/typescript-transformer-handbook#writing-your-first-transformer) you can learn about transformers in general. 193 | 194 | With `require-ts`, you can register the transformers with in the `tsconfig.json` file or pass them inline, when using the programmatic API. 195 | 196 | Following is an example of the tsconfig.json file 197 | 198 | ```json 199 | { 200 | "compilerOptions": {}, 201 | "transformers": { 202 | "before": ["./transformer-before"], 203 | "after": ["./transformer-after"], 204 | "afterDeclarations": ["./transformer-after-declarations"] 205 | } 206 | } 207 | ``` 208 | 209 | The transformer array accepts the relative file name from the `appRoot`. The transformer module must export a function as follows: 210 | 211 | ```ts 212 | export default transformerBefore(ts: typescript, appRoot: string) { 213 | return function transformerFactory (context) {} 214 | } 215 | ``` 216 | 217 | [gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/require-ts/test?style=for-the-badge 218 | [gh-workflow-url]: https://github.com/adonisjs/require-ts/actions/workflows/test.yml 'Github action' 219 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 220 | [typescript-url]: "typescript" 221 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/require-ts.svg?style=for-the-badge&logo=npm 222 | [npm-url]: https://npmjs.org/package/@adonisjs/require-ts 'npm' 223 | [license-image]: https://img.shields.io/npm/l/@adonisjs/require-ts?color=blueviolet&style=for-the-badge 224 | [license-url]: LICENSE.md 'license' 225 | [synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/require-ts?label=Synk%20Vulnerabilities&style=for-the-badge 226 | [synk-url]: https://snyk.io/test/github/adonisjs/require-ts?targetFile=package.json 'synk' 227 | -------------------------------------------------------------------------------- /benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | import { Suite } from 'benchmark' 2 | import revisionHash from 'rev-hash' 3 | 4 | const basePath = 'app/Http/Controllers/HomeController.ts' 5 | 6 | function normalize() { 7 | const tokens = basePath.split('/') 8 | const fileName = tokens.pop() 9 | return `${tokens.join('-')}-${fileName!.replace(/\.\w+$/, '')}` 10 | } 11 | 12 | function hashFile() { 13 | return revisionHash(basePath) 14 | } 15 | 16 | new Suite() 17 | .add('md5 hash', () => { 18 | hashFile() 19 | }) 20 | .add('normalize path', () => { 21 | normalize() 22 | }) 23 | .on('cycle', function (event: any) { 24 | console.log(String(event.target)) 25 | }) 26 | .on('complete', function () { 27 | console.log('Fastest is ' + this.filter('fastest').map('name')) 28 | }) 29 | .run({ async: true }) 30 | -------------------------------------------------------------------------------- /bin/japaTypes.ts: -------------------------------------------------------------------------------- 1 | import { Assert } from '@japa/assert' 2 | 3 | declare module '@japa/runner' { 4 | interface TestContext { 5 | assert: Assert 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { specReporter } from '@japa/spec-reporter' 3 | import { runFailedTests } from '@japa/run-failed-tests' 4 | import { processCliArgs, configure, run } from '@japa/runner' 5 | 6 | process.env.DEBUG = 'adonis:require-ts' 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Configure tests 10 | |-------------------------------------------------------------------------- 11 | | 12 | | The configure method accepts the configuration to configure the Japa 13 | | tests runner. 14 | | 15 | | The first method call "processCliArgs" process the command line arguments 16 | | and turns them into a config object. Using this method is not mandatory. 17 | | 18 | | Please consult japa.dev/runner-config for the config docs. 19 | */ 20 | configure({ 21 | ...processCliArgs(process.argv.slice(2)), 22 | ...{ 23 | files: ['test/**/*.spec.ts'], 24 | plugins: [assert(), runFailedTests()], 25 | reporters: [specReporter()], 26 | importer: (filePath: string) => import(filePath), 27 | }, 28 | }) 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Run tests 33 | |-------------------------------------------------------------------------- 34 | | 35 | | The following "run" method is required to execute all the tests. 36 | | 37 | */ 38 | run() 39 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { addHook } from 'pirates' 11 | import tsStatic from 'typescript' 12 | import findCacheDir from 'find-cache-dir' 13 | 14 | import { Cache } from './src/Cache' 15 | import { Config } from './src/Config' 16 | import { Compiler } from './src/Compiler' 17 | import { loadTypescript } from './src/utils' 18 | import { Transformers } from './src/Contracts' 19 | 20 | /** 21 | * Extensions to register require extension for 22 | */ 23 | const EXTS = ['.ts', '.tsx'] 24 | const CACHE_DIR_NAME = 'adonis-require-ts' 25 | 26 | /** 27 | * Symbols that can be used to get the global reference of the compiler 28 | */ 29 | export const symbols = { 30 | compiler: Symbol.for('REQUIRE_TS_COMPILER'), 31 | config: Symbol.for('REQUIRE_TS_CONFIG'), 32 | } 33 | 34 | /** 35 | * Returns helpers to along with cache when using a watcher. 36 | * 37 | * - You can check if the tsconfig file inside the cache is stale or not. 38 | * If it is stale, then clear the entire cache 39 | * 40 | * - Clear cache for a given file path. 41 | * - Clear all cache 42 | */ 43 | export function getWatcherHelpers(appRoot: string, cachePath?: string) { 44 | cachePath = cachePath || findCacheDir({ name: CACHE_DIR_NAME }) 45 | const cache = new Cache(appRoot, cachePath!) 46 | 47 | return { 48 | clear(filePath?: string) { 49 | return filePath ? cache.clearForFile(filePath) : cache.clearAll() 50 | }, 51 | isConfigStale: () => { 52 | const config = new Config(appRoot, cachePath!, undefined, true) 53 | const { cached } = config.getCached() 54 | return !cached || cached.version !== Config.version 55 | }, 56 | } 57 | } 58 | 59 | /** 60 | * Load in-memory typescript compiler 61 | */ 62 | export function loadCompiler( 63 | appRoot: string, 64 | options: { 65 | compilerOptions: tsStatic.CompilerOptions 66 | transformers?: Transformers 67 | } 68 | ) { 69 | const typescript = loadTypescript(appRoot) 70 | return new Compiler(appRoot, appRoot, typescript, options, false) 71 | } 72 | 73 | /** 74 | * Register hook to compile typescript files in-memory. When 75 | * caching is enabled, the compiled files will be written 76 | * on the disk 77 | */ 78 | export function register( 79 | appRoot: string, 80 | opts?: { 81 | cache?: boolean 82 | cachePath?: string 83 | transformers?: Transformers 84 | } 85 | ) { 86 | /** 87 | * Normalize options 88 | */ 89 | opts = Object.assign({ cache: false, cachePath: '' }, opts) 90 | if (opts.cache && !opts.cachePath) { 91 | opts.cachePath = findCacheDir({ name: CACHE_DIR_NAME }) 92 | } 93 | 94 | const typescript = loadTypescript(appRoot) 95 | 96 | /** 97 | * Parse config 98 | */ 99 | const config = new Config(appRoot, opts.cachePath!, typescript, !!opts.cache).parse() 100 | 101 | /** 102 | * Cannot continue when config has errors 103 | */ 104 | if (config.error) { 105 | process.exit(1) 106 | } 107 | 108 | /** 109 | * Merge transformers when defined 110 | */ 111 | if (opts.transformers) { 112 | config.options!.transformers = config.options!.transformers || {} 113 | 114 | if (opts.transformers.before) { 115 | config.options!.transformers.before = (config.options!.transformers.before || []).concat( 116 | opts.transformers.before 117 | ) 118 | } 119 | 120 | if (opts.transformers.after) { 121 | config.options!.transformers.after = (config.options!.transformers.after || []).concat( 122 | opts.transformers.after 123 | ) 124 | } 125 | 126 | if (opts.transformers.afterDeclarations) { 127 | config.options!.transformers.afterDeclarations = ( 128 | config.options!.transformers.afterDeclarations || [] 129 | ).concat(opts.transformers.afterDeclarations) 130 | } 131 | } 132 | 133 | /** 134 | * Instantiate compiler to compile `.ts` files using the typescript compiler. Currently 135 | * we not resolve `.js` files and will never resolve `.tsx` or `.jsx` files. 136 | */ 137 | const compiler = new Compiler(appRoot, opts.cachePath!, typescript, config.options!, !!opts.cache) 138 | global[symbols.compiler] = compiler 139 | global[symbols.config] = config 140 | 141 | addHook( 142 | (code, filename) => { 143 | return compiler.compile(filename, code) 144 | }, 145 | { exts: EXTS, matcher: () => true } 146 | ) 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/require-ts", 3 | "version": "2.0.13", 4 | "description": "In memory typescript compiler", 5 | "scripts": { 6 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 7 | "pretest": "npm run lint", 8 | "test": "cross-env FORCE_COLOR=true node -r ts-node/register/transpile-only bin/test.ts", 9 | "clean": "del-cli build", 10 | "compile": "npm run lint && npm run clean && tsc", 11 | "build": "npm run compile", 12 | "prepublishOnly": "npm run build", 13 | "lint": "eslint . --ext=.ts", 14 | "format": "prettier --write .", 15 | "commit": "git-cz", 16 | "release": "np --message=\"chore(release): %s\"", 17 | "version": "npm run build", 18 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/require-ts" 19 | }, 20 | "keywords": [ 21 | "typescript", 22 | "ts", 23 | "tsc", 24 | "ts-node" 25 | ], 26 | "author": "virk,adonisjs", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@adonisjs/mrm-preset": "^5.0.3", 30 | "@japa/assert": "^1.3.6", 31 | "@japa/run-failed-tests": "^1.1.0", 32 | "@japa/runner": "^2.2.1", 33 | "@japa/spec-reporter": "^1.3.1", 34 | "@poppinss/dev-utils": "^2.0.3", 35 | "@types/node": "^18.7.18", 36 | "@types/source-map-support": "^0.5.6", 37 | "benchmark": "^2.1.4", 38 | "commitizen": "^4.2.5", 39 | "cross-env": "^7.0.3", 40 | "cz-conventional-changelog": "^3.3.0", 41 | "del-cli": "^5.0.0", 42 | "eslint": "^8.23.1", 43 | "eslint-config-prettier": "^8.5.0", 44 | "eslint-plugin-adonis": "^2.1.1", 45 | "eslint-plugin-prettier": "^4.2.1", 46 | "github-label-sync": "^2.2.0", 47 | "husky": "^8.0.1", 48 | "mrm": "^4.1.6", 49 | "np": "^7.6.2", 50 | "prettier": "^2.7.1", 51 | "test-console": "^2.0.0", 52 | "ts-node": "^10.9.1", 53 | "typescript": "^4.8.3" 54 | }, 55 | "nyc": { 56 | "exclude": [ 57 | "test" 58 | ], 59 | "extension": [ 60 | ".ts" 61 | ] 62 | }, 63 | "main": "build/index.js", 64 | "files": [ 65 | "build/src", 66 | "build/index.d.ts", 67 | "build/index.js", 68 | "build/register.js", 69 | "build/register.d.ts" 70 | ], 71 | "config": { 72 | "commitizen": { 73 | "path": "cz-conventional-changelog" 74 | } 75 | }, 76 | "np": { 77 | "contents": ".", 78 | "anyBranch": false 79 | }, 80 | "dependencies": { 81 | "@poppinss/utils": "^5.0.0", 82 | "debug": "^4.3.4", 83 | "find-cache-dir": "^3.3.2", 84 | "fs-extra": "^10.1.0", 85 | "normalize-path": "^3.0.0", 86 | "pirates": "^4.0.5", 87 | "rev-hash": "^3.0.0", 88 | "source-map-support": "^0.5.21" 89 | }, 90 | "directories": { 91 | "test": "test" 92 | }, 93 | "repository": { 94 | "type": "git", 95 | "url": "git+https://github.com/adonisjs/require-ts.git" 96 | }, 97 | "bugs": { 98 | "url": "https://github.com/adonisjs/require-ts/issues" 99 | }, 100 | "homepage": "https://github.com/adonisjs/require-ts#readme", 101 | "mrmConfig": { 102 | "core": true, 103 | "license": "MIT", 104 | "services": [ 105 | "github-actions" 106 | ], 107 | "minNodeVersion": "14.15.4", 108 | "probotApps": [ 109 | "stale", 110 | "lock" 111 | ], 112 | "runGhActionsOnWindows": true 113 | }, 114 | "eslintConfig": { 115 | "extends": [ 116 | "plugin:adonis/typescriptPackage", 117 | "prettier" 118 | ], 119 | "plugins": [ 120 | "prettier" 121 | ], 122 | "rules": { 123 | "prettier/prettier": [ 124 | "error", 125 | { 126 | "endOfLine": "auto" 127 | } 128 | ] 129 | } 130 | }, 131 | "eslintIgnore": [ 132 | "build" 133 | ], 134 | "prettier": { 135 | "trailingComma": "es5", 136 | "semi": false, 137 | "singleQuote": true, 138 | "useTabs": false, 139 | "quoteProps": "consistent", 140 | "bracketSpacing": true, 141 | "arrowParens": "always", 142 | "printWidth": 100 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /register.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { register } from './index' 11 | 12 | const CWD = process.env.REQUIRE_TS_CWD || process.cwd() 13 | register(CWD, { 14 | cache: !!process.env.REQUIRE_TS_CACHE, 15 | }) 16 | -------------------------------------------------------------------------------- /src/Cache/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'path' 11 | import revHash from 'rev-hash' 12 | import { readFileSync, outputFileSync, removeSync } from 'fs-extra' 13 | import { getCachePathForFile, debug } from '../utils' 14 | 15 | /** 16 | * Exposes the API to write file parsed contents to disk as cache. Handles 17 | * all the complexity of generate correct paths and creating contents 18 | * hash 19 | */ 20 | export class Cache { 21 | constructor(private appRoot: string, private cacheRoot: string) {} 22 | 23 | /** 24 | * Generates hash from file contents 25 | */ 26 | public generateHash(contents: string) { 27 | return revHash(contents) 28 | } 29 | 30 | /** 31 | * Makes cache path from a given file path and its contents 32 | */ 33 | public makeCachePath(filePath: string, contents: string, extname: '.js' | '.json') { 34 | const relativeCachePath = getCachePathForFile(this.appRoot, filePath) 35 | const hash = this.generateHash(contents) 36 | return join(this.cacheRoot, relativeCachePath, `${hash}${extname}`) 37 | } 38 | 39 | /** 40 | * Returns the file contents from the cache (if exists), otherwise 41 | * returns null 42 | */ 43 | public get(cachePath: string): string | null { 44 | try { 45 | const contents = readFileSync(cachePath, 'utf8') 46 | debug('reading from cache "%s"', cachePath) 47 | return contents 48 | } catch (error) { 49 | if (error.code === 'ENOENT') { 50 | return null 51 | } 52 | throw error 53 | } 54 | } 55 | 56 | /** 57 | * Writes file contents to the disk 58 | */ 59 | public set(cachePath: string, contents: string) { 60 | debug('writing to cache "%s"', cachePath) 61 | outputFileSync(cachePath, contents) 62 | } 63 | 64 | /** 65 | * Clears all the generate cache for a given file 66 | */ 67 | public clearForFile(filePath: string) { 68 | debug('clear cache for "%s"', filePath) 69 | const relativeCachePath = getCachePathForFile(this.appRoot, filePath) 70 | removeSync(join(this.cacheRoot, relativeCachePath)) 71 | } 72 | 73 | /** 74 | * Clears the cache root folder 75 | */ 76 | public clearAll() { 77 | removeSync(this.cacheRoot) 78 | } 79 | } 80 | 81 | /** 82 | * A parallel fake implementation of cache that results in noop. Used 83 | * when caching is disabled. 84 | */ 85 | export class FakeCache { 86 | constructor() {} 87 | 88 | /** 89 | * Generates hash from file contents 90 | */ 91 | public generateHash(_: string) { 92 | return '' 93 | } 94 | 95 | /** 96 | * Makes cache path from a given file path and its contents 97 | */ 98 | public makeCachePath(_: string, __: string, ___: '.js' | '.json') { 99 | return '' 100 | } 101 | 102 | /** 103 | * Returns the file contents from the cache (if exists), otherwise 104 | * returns null 105 | */ 106 | public get(_: string): string | null { 107 | return null 108 | } 109 | 110 | /** 111 | * Writes file contents to the disk 112 | */ 113 | public set(_: string, __: string) {} 114 | 115 | /** 116 | * Clears all the generate cache for a given file 117 | */ 118 | public clearForFile(_: string) {} 119 | 120 | /** 121 | * Clears the cache root folder 122 | */ 123 | public clearAll() {} 124 | } 125 | -------------------------------------------------------------------------------- /src/Compiler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import tsStatic from 'typescript' 11 | import { esmRequire } from '@poppinss/utils' 12 | import sourceMapSupport from 'source-map-support' 13 | 14 | import { debug } from '../utils' 15 | import { Cache, FakeCache } from '../Cache' 16 | import { Transformers } from '../Contracts' 17 | import { DiagnosticsReporter } from '../DiagnosticsReporter' 18 | 19 | /** 20 | * Exposes the API compile source files using the tsc compiler. No 21 | * type checking takes place. 22 | */ 23 | export class Compiler { 24 | /** 25 | * In-memory compiled files cache for source maps to work. 26 | */ 27 | private memCache: Map = new Map() 28 | 29 | /** 30 | * Disk cache 31 | */ 32 | private cache = this.usesCache ? new Cache(this.appRoot, this.cacheRoot) : new FakeCache() 33 | 34 | /** 35 | * Dignostic reporter to print program errors 36 | */ 37 | private diagnosticsReporter = new DiagnosticsReporter(this.appRoot, this.ts, false) 38 | 39 | private transformers: tsStatic.CustomTransformers = {} 40 | 41 | constructor( 42 | private appRoot: string, 43 | private cacheRoot: string, 44 | private ts: typeof tsStatic, 45 | private options: { 46 | compilerOptions: tsStatic.CompilerOptions 47 | transformers?: Transformers 48 | }, 49 | private usesCache: boolean = true 50 | ) { 51 | this.patchCompilerOptions() 52 | this.setupSourceMaps() 53 | this.resolveTransformers() 54 | } 55 | 56 | /** 57 | * Patch compiler options to make source map work properly 58 | */ 59 | private patchCompilerOptions() { 60 | /** 61 | * Force inline source maps. We need this to avoid manual 62 | * lookups 63 | */ 64 | this.options.compilerOptions.inlineSourceMap = true 65 | 66 | /** 67 | * Inline sources 68 | */ 69 | this.options.compilerOptions.inlineSources = true 70 | 71 | /** 72 | * Remove "outDir" property, so that the source maps paths are generated 73 | * relative from the cwd and not the outDir. 74 | * 75 | * ts-node manually patches the source maps to use absolute paths. We cannot 76 | * do same, since we cache files on the disk and changing the folder name 77 | * of project root will corrupt the absolute path names inside the 78 | * source maps. 79 | */ 80 | delete this.options.compilerOptions.outDir 81 | 82 | /** 83 | * Inline source maps and source map cannot be used together 84 | */ 85 | delete this.options.compilerOptions.sourceMap 86 | } 87 | 88 | /** 89 | * Resolves transformer relative from the app root 90 | */ 91 | private resolverTransformer(transformer: string) { 92 | try { 93 | const value = esmRequire(require.resolve(transformer, { paths: [this.appRoot] })) 94 | if (typeof value !== 'function') { 95 | throw new Error('Transformer module must export a function') 96 | } 97 | return value(this.ts, this.appRoot) 98 | } catch (error) { 99 | if (error.code === 'ENOENT') { 100 | throw new Error( 101 | `Unable to resolve transformer "${transformer}" specified in tsconfig.json file` 102 | ) 103 | } 104 | throw error 105 | } 106 | } 107 | 108 | /** 109 | * Resolve transformers 110 | */ 111 | private resolveTransformers() { 112 | if (!this.options.transformers) { 113 | return 114 | } 115 | 116 | if (this.options.transformers.before) { 117 | this.transformers.before = this.options.transformers.before.map((transformer) => { 118 | return this.resolverTransformer(transformer.transform) 119 | }) 120 | } 121 | 122 | if (this.options.transformers.after) { 123 | this.transformers.after = this.options.transformers.after.map((transformer) => { 124 | return this.resolverTransformer(transformer.transform) 125 | }) 126 | } 127 | 128 | if (this.options.transformers.afterDeclarations) { 129 | this.transformers.afterDeclarations = this.options.transformers.afterDeclarations.map( 130 | (transformer) => { 131 | return this.resolverTransformer(transformer.transform) 132 | } 133 | ) 134 | } 135 | } 136 | 137 | /** 138 | * Setup source maps support to read from in-memory cache 139 | */ 140 | private setupSourceMaps() { 141 | sourceMapSupport.install({ 142 | environment: 'node', 143 | retrieveFile: (pathOrUrl: string) => { 144 | debug('reading source for "%s"', pathOrUrl) 145 | return this.memCache.get(pathOrUrl) || '' 146 | }, 147 | }) 148 | } 149 | 150 | /** 151 | * Compiles the file using the typescript compiler 152 | */ 153 | private compileFile(filePath: string, contents: string, virtualFile: boolean) { 154 | debug('compiling file using typescript "%s"', filePath) 155 | 156 | let { outputText, diagnostics } = this.ts.transpileModule(contents, { 157 | fileName: filePath, 158 | compilerOptions: virtualFile 159 | ? { 160 | ...this.options.compilerOptions, 161 | rootDir: undefined, 162 | } 163 | : this.options.compilerOptions, 164 | reportDiagnostics: !virtualFile, 165 | transformers: this.transformers, 166 | }) 167 | 168 | /** 169 | * Report diagnostics if any 170 | */ 171 | if (diagnostics) { 172 | this.diagnosticsReporter.report(diagnostics) 173 | } 174 | 175 | /** 176 | * Write to in-memory cache for sourcemaps to work 177 | */ 178 | this.memCache.set(filePath, outputText) 179 | return outputText 180 | } 181 | 182 | /** 183 | * Compile typescript source code using the tsc compiler. 184 | */ 185 | public compile(filePath: string, contents: string, virtualFile: boolean = false) { 186 | /** 187 | * Do not cache virtual files 188 | */ 189 | if (virtualFile) { 190 | debug('compiling virtual file "%s" (no cache)', filePath) 191 | return this.compileFile(filePath, contents, true) 192 | } 193 | 194 | debug('compiling file "%s"', filePath) 195 | const cachePath = this.cache.makeCachePath(filePath, contents, '.js') 196 | 197 | /** 198 | * Return the file from cache when it exists 199 | */ 200 | const compiledContent = this.cache.get(cachePath) 201 | if (compiledContent) { 202 | /** 203 | * Write to in-memory cache for sourcemaps to work 204 | */ 205 | this.memCache.set(filePath, compiledContent) 206 | return compiledContent 207 | } 208 | 209 | /** 210 | * Compile file using the compiler 211 | */ 212 | const outputText = this.compileFile(filePath, contents, false) 213 | 214 | /** 215 | * Write to cache on disk 216 | */ 217 | this.cache.set(cachePath, outputText) 218 | 219 | /** 220 | * Return compiled text 221 | */ 222 | return outputText 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'path' 11 | import tsStatic from 'typescript' 12 | import { readFileSync } from 'fs-extra' 13 | 14 | import { debug } from '../utils' 15 | import { Transformers } from '../Contracts' 16 | import { Cache, FakeCache } from '../Cache' 17 | import { DiagnosticsReporter } from '../DiagnosticsReporter' 18 | 19 | /** 20 | * Exposes the API to parse tsconfig file and cache it until the 21 | * contents of the file are changed. 22 | */ 23 | export class Config { 24 | public static version = 'v1' 25 | 26 | /** 27 | * Hard assumption has been made that config file name 28 | * is "tsconfig.json" 29 | */ 30 | private configFilePath = join(this.appRoot, 'tsconfig.json') 31 | 32 | /** 33 | * Reference to the cache 34 | */ 35 | private cache = this.usesCache ? new Cache(this.appRoot, this.cacheRoot) : new FakeCache() 36 | 37 | /** 38 | * Dignostic reporter to print program errors 39 | */ 40 | private diagnosticsReporter = this.ts 41 | ? new DiagnosticsReporter(this.appRoot, this.ts, false) 42 | : undefined 43 | 44 | constructor( 45 | private appRoot: string, 46 | private cacheRoot: string, 47 | private ts?: typeof tsStatic, 48 | private usesCache: boolean = true 49 | ) {} 50 | 51 | /** 52 | * Returns the raw contents of the config file. We need to read this 53 | * always to generate the hash and then look for the cached config 54 | * file. 55 | */ 56 | private getConfigRawContents(): string { 57 | debug('checking for tsconfig "%s"', this.configFilePath) 58 | 59 | try { 60 | return readFileSync(this.configFilePath, 'utf-8') 61 | } catch (error) { 62 | if (error.code === 'ENOENT') { 63 | throw new Error( 64 | '"@adonisjs/require-ts" expects the "tsconfig.json" file to exists in the app root' 65 | ) 66 | } 67 | 68 | throw error 69 | } 70 | } 71 | 72 | /** 73 | * Parses the ts config using the typescript compiler 74 | */ 75 | private parseTsConfig(): { error: null | tsStatic.Diagnostic[]; options: any } { 76 | let exception: any = null 77 | debug('parse tsconfig file') 78 | 79 | if (!this.ts) { 80 | throw new Error( 81 | 'Cannot parse typescript config. Make sure to instantiate Config class with typescript compiler' 82 | ) 83 | } 84 | 85 | /** 86 | * Parse config using typescript compiler 87 | */ 88 | const config = this.ts.getParsedCommandLineOfConfigFile( 89 | this.configFilePath, 90 | {}, 91 | { 92 | ...this.ts.sys, 93 | useCaseSensitiveFileNames: true, 94 | getCurrentDirectory: () => this.appRoot, 95 | onUnRecoverableConfigFileDiagnostic: (error: any) => (exception = error), 96 | } 97 | ) 98 | 99 | /** 100 | * Return exception as it is 101 | */ 102 | if (exception) { 103 | return { 104 | error: exception, 105 | options: null, 106 | } 107 | } 108 | 109 | /** 110 | * Return diagnostic errors if any 111 | */ 112 | if (config!.errors && config!.errors.length) { 113 | return { 114 | error: config!.errors, 115 | options: null, 116 | } 117 | } 118 | 119 | /** 120 | * Return compiler options 121 | */ 122 | return { 123 | error: null, 124 | options: config!.options, 125 | } 126 | } 127 | 128 | /** 129 | * Parses the cached config string as JSON. Errors 130 | * are ignored and hence cache is ignored too 131 | */ 132 | private parseConfigAsJson(config: string | null) { 133 | if (!config) { 134 | return null 135 | } 136 | 137 | try { 138 | return JSON.parse(config) 139 | } catch (error) { 140 | return null 141 | } 142 | } 143 | 144 | /** 145 | * Extracts transformers the tsconfig file contents 146 | */ 147 | private extractTransformers(rawConfig: string): Transformers | undefined { 148 | try { 149 | const transformers = JSON.parse(rawConfig).transformers || {} 150 | return { 151 | before: transformers.before, 152 | after: transformers.after, 153 | afterDeclarations: transformers.afterDeclarations, 154 | } 155 | } catch (error) {} 156 | } 157 | 158 | /** 159 | * Returns the config file from the cache or returns null when there is 160 | * no cache 161 | */ 162 | public getCached(): { 163 | raw: string 164 | cachePath: string 165 | cached: null | { 166 | version: string 167 | options: { compilerOptions: tsStatic.CompilerOptions; transformers?: Transformers } 168 | } 169 | } { 170 | const rawContents = this.getConfigRawContents() 171 | const cachePath = this.cache.makeCachePath(this.configFilePath, rawContents, '.json') 172 | return { 173 | raw: rawContents, 174 | cachePath, 175 | cached: this.parseConfigAsJson(this.cache.get(cachePath)), 176 | } 177 | } 178 | 179 | /** 180 | * Parses config and returns the compiler options 181 | */ 182 | public parse(): { 183 | version: string 184 | options: null | { compilerOptions: tsStatic.CompilerOptions; transformers?: Transformers } 185 | error: null | tsStatic.Diagnostic[] 186 | } { 187 | if (!this.diagnosticsReporter) { 188 | throw new Error( 189 | 'Cannot parse typescript config. Make sure to instantiate Config class with typescript compiler' 190 | ) 191 | } 192 | 193 | /** 194 | * Cache exists and is upto date 195 | */ 196 | const { cached, raw, cachePath } = this.getCached() 197 | if (cached && cached.version === Config.version) { 198 | return { 199 | version: cached.version, 200 | error: null, 201 | options: { 202 | compilerOptions: cached.options.compilerOptions, 203 | transformers: cached.options.transformers, 204 | }, 205 | } 206 | } 207 | 208 | /** 209 | * Parse the config using the compiler 210 | */ 211 | const config = this.parseTsConfig() 212 | if (config.error) { 213 | this.diagnosticsReporter.report(config.error) 214 | return { 215 | version: Config.version, 216 | options: null, 217 | error: config.error, 218 | } 219 | } 220 | 221 | /** 222 | * Write to cache to avoid future parsing 223 | */ 224 | const parsed = { 225 | version: Config.version, 226 | error: null, 227 | options: { 228 | compilerOptions: config.options, 229 | transformers: this.extractTransformers(raw), 230 | }, 231 | } 232 | 233 | this.cache.set(cachePath, JSON.stringify(parsed)) 234 | return parsed 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * Custom transformers extracted from the package.json file 12 | */ 13 | export type Transformers = { 14 | before?: { transform: string }[] 15 | after?: { transform: string }[] 16 | afterDeclarations?: { transform: string }[] 17 | } 18 | -------------------------------------------------------------------------------- /src/DiagnosticsReporter/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import tsStatic from 'typescript' 11 | 12 | /** 13 | * Exposes the API to report/print typescript diagnostic reports 14 | */ 15 | export class DiagnosticsReporter { 16 | /** 17 | * Diagnostics host 18 | */ 19 | private host = { 20 | getNewLine: () => this.ts.sys.newLine, 21 | getCurrentDirectory: () => this.appRoot, 22 | getCanonicalFileName: this.ts.sys.useCaseSensitiveFileNames 23 | ? (fileName: string) => fileName 24 | : (fileName: string) => fileName.toLowerCase(), 25 | } 26 | 27 | constructor(private appRoot: string, private ts: typeof tsStatic, private pretty: boolean) {} 28 | 29 | public report(diagnostics: tsStatic.Diagnostic[]) { 30 | if (!diagnostics.length) { 31 | return 32 | } 33 | 34 | if (this.pretty) { 35 | console.log(this.ts.formatDiagnosticsWithColorAndContext(diagnostics, this.host)) 36 | } else { 37 | console.log(this.ts.formatDiagnostics(diagnostics, this.host)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import Debug from 'debug' 11 | import normalizePath from 'normalize-path' 12 | 13 | export const debug = Debug('adonis:require-ts') 14 | 15 | /** 16 | * Returns the cache directory path for a given file. The idea is to 17 | * use the filename as a directory and then drop files with their 18 | * hashes inside that directory. In case the file gets changed, 19 | * we just need to drop the directory 20 | */ 21 | export function getCachePathForFile(cwd: string, location: string) { 22 | const tokens = normalizePath(location.replace(cwd, '')).split('/') 23 | const fileName = tokens.pop() 24 | tokens.shift() 25 | 26 | if (!tokens.length) { 27 | return fileName.replace(/\.\w+$/, '') 28 | } 29 | 30 | return `${tokens.join('-')}-${fileName.replace(/\.\w+$/, '')}` 31 | } 32 | 33 | /** 34 | * Loads typescript from the user project dependencies 35 | */ 36 | export function loadTypescript(cwd: string) { 37 | try { 38 | return require(require.resolve('typescript', { paths: [cwd] })) 39 | } catch (error) { 40 | if (error.code === 'ENOENT') { 41 | throw new Error('"@adonisjs/require-ts" expects the "typescript" to be installed') 42 | } 43 | throw error 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import testConsole from 'test-console' 11 | 12 | export function stringToArray(value: string) { 13 | return value.split('\n').map((line) => line.trim()) 14 | } 15 | 16 | /** 17 | * Strip ANSI escape codes 18 | */ 19 | function stripAnsi(value: string) { 20 | const pattern = [ 21 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', 22 | '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 23 | ].join('|') 24 | return value.replace(RegExp(pattern, 'g'), '') 25 | } 26 | 27 | export function inspectConsole() { 28 | const stdoutOutput = testConsole.stdout.inspect() 29 | const stderrOutput = testConsole.stderr.inspect() 30 | return () => { 31 | stdoutOutput.restore() 32 | stderrOutput.restore() 33 | return { 34 | stdout: stdoutOutput.output 35 | .map((line) => stripAnsi(line)) 36 | .filter((line) => !line.trim().includes('adonis:require-ts')), 37 | stderr: stderrOutput.output 38 | .map((line) => stripAnsi(line)) 39 | .filter((line) => !line.trim().includes('adonis:require-ts')), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/cache-path.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { join } from 'path' 12 | import { getCachePathForFile } from '../src/utils' 13 | 14 | test.group('getCachePathForFile', () => { 15 | test('get cache root name for a given file', ({ assert }) => { 16 | const cwd = join(__dirname, 'app') 17 | const fileName = join(cwd, 'server.ts') 18 | 19 | const cacheRoot = getCachePathForFile(cwd, fileName) 20 | assert.equal(cacheRoot, 'server') 21 | }) 22 | 23 | test('get cache root name for a nested path', ({ assert }) => { 24 | const cwd = join(__dirname, 'app') 25 | const fileName = join(cwd, 'start/routes.ts') 26 | 27 | const cacheRoot = getCachePathForFile(cwd, fileName) 28 | assert.equal(cacheRoot, 'start-routes') 29 | }) 30 | 31 | test('get cache root name for deep nested path', ({ assert }) => { 32 | const cwd = join(__dirname, 'app') 33 | const fileName = join(cwd, 'app/Controllers/Http/HomeController.ts') 34 | 35 | const cacheRoot = getCachePathForFile(cwd, fileName) 36 | assert.equal(cacheRoot, 'app-Controllers-Http-HomeController') 37 | }) 38 | 39 | test('handle files without extension', ({ assert }) => { 40 | const cwd = join(__dirname, 'app') 41 | const fileName = join(cwd, 'app/Controllers/Http/HomeController') 42 | 43 | const cacheRoot = getCachePathForFile(cwd, fileName) 44 | assert.equal(cacheRoot, 'app-Controllers-Http-HomeController') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/cache.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { join } from 'path' 12 | import revHash from 'rev-hash' 13 | import { Filesystem } from '@poppinss/dev-utils' 14 | 15 | import { Cache } from '../src/Cache' 16 | 17 | const fs = new Filesystem(join(__dirname, 'app')) 18 | 19 | test.group('Cache', (group) => { 20 | group.each.teardown(async () => { 21 | await fs.cleanup() 22 | }) 23 | 24 | test('make path to the cache file', ({ assert }) => { 25 | const cwd = join(__dirname, '..') 26 | const fileName = join(cwd, 'server.ts') 27 | const cache = new Cache(cwd, fs.basePath) 28 | 29 | const fileCachePath = cache.makeCachePath(fileName, 'hello-world', '.js') 30 | assert.equal(fileCachePath, join(fs.basePath, 'server', `${revHash('hello-world')}.js`)) 31 | }) 32 | 33 | test('get file contents from the cache', async ({ assert }) => { 34 | const cwd = join(__dirname, '..') 35 | const fileName = join(cwd, 'server.ts') 36 | const cache = new Cache(cwd, fs.basePath) 37 | const cachedFileName = `${revHash('hello-world')}.js` 38 | 39 | await fs.add(`server/${cachedFileName}`, 'hello-world') 40 | 41 | const fileCachePath = cache.makeCachePath(fileName, 'hello-world', '.js') 42 | assert.equal(cache.get(fileCachePath), 'hello-world') 43 | }) 44 | 45 | test("return null when cache file doesn't exists", async ({ assert }) => { 46 | const cwd = join(__dirname, '..') 47 | const fileName = join(cwd, 'server.ts') 48 | const cache = new Cache(cwd, fs.basePath) 49 | 50 | const fileCachePath = cache.makeCachePath(fileName, 'hello-world', '.js') 51 | assert.isNull(cache.get(fileCachePath)) 52 | }) 53 | 54 | test('write cache files', async ({ assert }) => { 55 | const cwd = join(__dirname, '..') 56 | const fileName = join(cwd, 'server.ts') 57 | const cache = new Cache(cwd, fs.basePath) 58 | 59 | cache.set(cache.makeCachePath(fileName, 'hello-world', '.js'), 'hello-world') 60 | cache.set(cache.makeCachePath(fileName, 'hi-world', '.js'), 'hi-world') 61 | cache.set(cache.makeCachePath(fileName, 'hey-world', '.js'), 'hey-world') 62 | 63 | assert.isTrue( 64 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hello-world')}.js`)) 65 | ) 66 | assert.isTrue( 67 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hi-world')}.js`)) 68 | ) 69 | assert.isTrue( 70 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hey-world')}.js`)) 71 | ) 72 | 73 | cache.clearForFile(fileName) 74 | 75 | assert.isFalse( 76 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hello-world')}.js`)) 77 | ) 78 | assert.isFalse( 79 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hi-world')}.js`)) 80 | ) 81 | assert.isFalse( 82 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hey-world')}.js`)) 83 | ) 84 | }) 85 | 86 | test('write all cache files', async ({ assert }) => { 87 | const cwd = join(__dirname, '..') 88 | const fileName = join(cwd, 'server.ts') 89 | const cache = new Cache(cwd, fs.basePath) 90 | 91 | cache.set(cache.makeCachePath(fileName, 'hello-world', '.js'), 'hello-world') 92 | cache.set(cache.makeCachePath(fileName, 'hi-world', '.js'), 'hi-world') 93 | cache.set(cache.makeCachePath(fileName, 'hey-world', '.js'), 'hey-world') 94 | 95 | assert.isTrue( 96 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hello-world')}.js`)) 97 | ) 98 | assert.isTrue( 99 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hi-world')}.js`)) 100 | ) 101 | assert.isTrue( 102 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hey-world')}.js`)) 103 | ) 104 | 105 | cache.clearAll() 106 | 107 | assert.isFalse( 108 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hello-world')}.js`)) 109 | ) 110 | assert.isFalse( 111 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hi-world')}.js`)) 112 | ) 113 | assert.isFalse( 114 | await fs.fsExtra.pathExists(join(fs.basePath, 'server', `${revHash('hey-world')}.js`)) 115 | ) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /test/compiler.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { join } from 'path' 12 | import ts from 'typescript' 13 | import revHash from 'rev-hash' 14 | import { Filesystem } from '@poppinss/dev-utils' 15 | 16 | import { Compiler } from '../src/Compiler' 17 | import { stringToArray, inspectConsole } from '../test-helpers' 18 | 19 | const fs = new Filesystem(join(__dirname, 'app')) 20 | 21 | test.group('Compiler', (group) => { 22 | group.each.teardown(async () => { 23 | await fs.cleanup() 24 | }) 25 | 26 | test('parse typescript source and return back the compiled contents', async ({ assert }) => { 27 | const cwd = join(__dirname, '..') 28 | const cacheRoot = join(fs.basePath, 'cache') 29 | 30 | const compiler = new Compiler(cwd, cacheRoot, ts, { 31 | compilerOptions: {}, 32 | }) 33 | 34 | const inspect = inspectConsole() 35 | const output = compiler.compile( 36 | 'server.ts', 37 | ` 38 | const name: string = 'hello' 39 | export default name 40 | ` 41 | ) 42 | 43 | const { stdout, stderr } = inspect() 44 | 45 | assert.deepEqual(stdout, []) 46 | assert.deepEqual(stderr, []) 47 | assert.deepEqual( 48 | stringToArray(output).slice(0, -1), 49 | stringToArray(`"use strict"; 50 | Object.defineProperty(exports, "__esModule", { value: true }); 51 | var name = 'hello'; 52 | exports.default = name;`) 53 | ) 54 | }) 55 | 56 | test('cache file contents on disk', async ({ assert }) => { 57 | const cwd = join(__dirname, '..') 58 | const cacheRoot = join(fs.basePath, 'cache') 59 | const contents = ` 60 | const name: string = 'hello' 61 | export default name 62 | ` 63 | 64 | const fileHash = revHash(contents) 65 | const compiler = new Compiler(cwd, cacheRoot, ts, { 66 | compilerOptions: {}, 67 | }) 68 | 69 | const inspect = inspectConsole() 70 | const output = compiler.compile('server.ts', contents) 71 | 72 | const { stdout, stderr } = inspect() 73 | 74 | assert.deepEqual(stdout, []) 75 | assert.deepEqual(stderr, []) 76 | const cachedContents = await fs.get(`cache/server/${fileHash}.js`) 77 | assert.deepEqual(stringToArray(output), stringToArray(cachedContents)) 78 | }) 79 | 80 | test('do not re-compile file when already exists in cache', async ({ assert }) => { 81 | const cwd = join(__dirname, '..') 82 | const cacheRoot = join(fs.basePath, 'cache') 83 | const contents = ` 84 | const name: string = 'hello' 85 | export default name 86 | ` 87 | 88 | const fileHash = revHash(contents) 89 | await fs.add(`cache/server/${fileHash}.js`, 'hello') 90 | 91 | const compiler = new Compiler(cwd, cacheRoot, ts, { 92 | compilerOptions: {}, 93 | }) 94 | 95 | const inspect = inspectConsole() 96 | const output = compiler.compile('server.ts', contents) 97 | 98 | const { stdout, stderr } = inspect() 99 | 100 | assert.deepEqual(stdout, []) 101 | assert.deepEqual(stderr, []) 102 | assert.deepEqual(stringToArray(output), stringToArray('hello')) 103 | }) 104 | 105 | test('do not cache when caching is disabled', async ({ assert }) => { 106 | const cwd = join(__dirname, '..') 107 | const cacheRoot = join(fs.basePath, 'cache') 108 | const contents = ` 109 | const name: string = 'hello' 110 | export default name 111 | ` 112 | 113 | const fileHash = revHash(contents) 114 | const compiler = new Compiler( 115 | cacheRoot, 116 | cwd, 117 | ts, 118 | { 119 | compilerOptions: {}, 120 | }, 121 | false 122 | ) 123 | 124 | const inspect = inspectConsole() 125 | const output = compiler.compile('server.ts', contents) 126 | 127 | const { stdout, stderr } = inspect() 128 | 129 | assert.deepEqual(stdout, []) 130 | assert.deepEqual(stderr, []) 131 | 132 | const hasCacheFile = await fs.exists(`cache/server/${fileHash}.js`) 133 | assert.isFalse(hasCacheFile) 134 | 135 | assert.deepEqual( 136 | stringToArray(output).slice(0, -1), 137 | stringToArray(`"use strict"; 138 | Object.defineProperty(exports, "__esModule", { value: true }); 139 | var name = 'hello'; 140 | exports.default = name;`) 141 | ) 142 | }) 143 | 144 | test('apply transformers', async ({ assert }) => { 145 | const cwd = join(__dirname, '..') 146 | const cacheRoot = join(fs.basePath, 'cache') 147 | 148 | await fs.add( 149 | 'transformer.js', 150 | ` 151 | module.exports = function (ts, appRoot) { 152 | return (ctx) => { 153 | return (sourceFile) => { 154 | function visitor (node) { 155 | if ( 156 | ts.isCallExpression(node) 157 | && node.expression 158 | && ts.isIdentifier(node.expression) 159 | && node.expression.escapedText === 'require' 160 | ) { 161 | const moduleName = node.arguments[0].text 162 | return ts.factory.createCallExpression( 163 | ts.factory.createIdentifier('ioc.use'), 164 | undefined, 165 | [ts.factory.createStringLiteral(moduleName)], 166 | ) 167 | } 168 | return ts.visitEachChild(node, visitor, ctx) 169 | } 170 | return ts.visitEachChild(sourceFile, visitor, ctx) 171 | } 172 | } 173 | } 174 | ` 175 | ) 176 | 177 | const compiler = new Compiler(cwd, cacheRoot, ts, { 178 | compilerOptions: {}, 179 | transformers: { 180 | after: [{ transform: join(fs.basePath, 'transformer.js') }], 181 | }, 182 | }) 183 | 184 | const inspect = inspectConsole() 185 | const output = compiler.compile('server.ts', `import'foo'`) 186 | 187 | const { stdout, stderr } = inspect() 188 | 189 | assert.deepEqual(stdout, []) 190 | assert.deepEqual(stderr, []) 191 | 192 | assert.deepEqual( 193 | stringToArray(output).slice(0, -1), 194 | stringToArray(`"use strict"; 195 | Object.defineProperty(exports, "__esModule", { value: true }); 196 | ioc.use("foo");`) 197 | ) 198 | }) 199 | 200 | test('complain when rootDir is defined is file is not marked as virtual', async ({ assert }) => { 201 | const cwd = join(__dirname, '..') 202 | const cacheRoot = join(fs.basePath, 'cache') 203 | 204 | const compiler = new Compiler(cwd, cacheRoot, ts, { 205 | compilerOptions: { 206 | rootDir: join(__dirname), 207 | }, 208 | }) 209 | 210 | const inspect = inspectConsole() 211 | const output = compiler.compile( 212 | 'server.ts', 213 | ` 214 | const name: string = 'hello' 215 | export default name 216 | `, 217 | false 218 | ) 219 | 220 | const { stdout, stderr } = inspect() 221 | 222 | assert.match(stdout[0], /error TS6059: File 'server\.ts' is not under 'rootDir'/) 223 | assert.deepEqual(stderr, []) 224 | 225 | assert.deepEqual( 226 | stringToArray(output).slice(0, -1), 227 | stringToArray(`"use strict"; 228 | Object.defineProperty(exports, "__esModule", { value: true }); 229 | var name = 'hello'; 230 | exports.default = name;`) 231 | ) 232 | }) 233 | 234 | test('work fine when rootDir is defined is file is marked as virtual', async ({ assert }) => { 235 | const cwd = join(__dirname, '..') 236 | const cacheRoot = join(fs.basePath, 'cache') 237 | 238 | const compiler = new Compiler(cwd, cacheRoot, ts, { 239 | compilerOptions: { 240 | rootDir: join(__dirname), 241 | }, 242 | }) 243 | 244 | const inspect = inspectConsole() 245 | const output = compiler.compile( 246 | 'server.ts', 247 | ` 248 | const name: string = 'hello' 249 | export default name 250 | `, 251 | true 252 | ) 253 | 254 | const { stdout, stderr } = inspect() 255 | 256 | assert.deepEqual(stdout, []) 257 | assert.deepEqual(stderr, []) 258 | 259 | assert.deepEqual( 260 | stringToArray(output).slice(0, -1), 261 | stringToArray(`"use strict"; 262 | Object.defineProperty(exports, "__esModule", { value: true }); 263 | var name = 'hello'; 264 | exports.default = name;`) 265 | ) 266 | }) 267 | }) 268 | -------------------------------------------------------------------------------- /test/config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/require-ts 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { join } from 'path' 12 | import ts from 'typescript' 13 | import revisionHash from 'rev-hash' 14 | import { Filesystem } from '@poppinss/dev-utils' 15 | 16 | import { Config } from '../src/Config' 17 | 18 | const fs = new Filesystem(join(__dirname, 'app')) 19 | 20 | test.group('Config', (group) => { 21 | group.each.teardown(async () => { 22 | await fs.cleanup() 23 | }) 24 | 25 | test('parse tsconfig and return compiler options back', async ({ assert }) => { 26 | const cwd = fs.basePath 27 | const cacheRoot = join(fs.basePath, 'cache') 28 | const contents = JSON.stringify({ 29 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 30 | }) 31 | 32 | await fs.add('tsconfig.json', contents) 33 | 34 | const config = new Config(cwd, cacheRoot, ts) 35 | const options = config.parse() 36 | 37 | assert.isNull(options.error) 38 | assert.equal(options.version, Config.version) 39 | assert.property(options.options?.compilerOptions, 'esModuleInterop') 40 | assert.property(options.options?.compilerOptions, 'configFilePath') 41 | }) 42 | 43 | test('write compiled file to cache', async ({ assert }) => { 44 | const cwd = fs.basePath 45 | const cacheRoot = join(fs.basePath, 'cache') 46 | const contents = JSON.stringify({ 47 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 48 | }) 49 | 50 | await fs.add('tsconfig.json', contents) 51 | 52 | const config = new Config(cwd, cacheRoot, ts) 53 | const options = config.parse() 54 | 55 | const hasCacheFile = await fs.exists(`cache/tsconfig/${revisionHash(contents)}.json`) 56 | assert.isTrue(hasCacheFile) 57 | 58 | assert.isNull(options.error) 59 | assert.equal(options.version, Config.version) 60 | assert.property(options.options?.compilerOptions, 'esModuleInterop') 61 | assert.property(options.options?.compilerOptions, 'configFilePath') 62 | }) 63 | 64 | test('do not write to cache when caching is disabled', async ({ assert }) => { 65 | const cwd = fs.basePath 66 | const cacheRoot = join(fs.basePath, 'cache') 67 | const contents = JSON.stringify({ 68 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 69 | }) 70 | 71 | await fs.add('tsconfig.json', contents) 72 | 73 | const config = new Config(cwd, cacheRoot, ts, false) 74 | const options = config.parse() 75 | 76 | const hasCacheFile = await fs.exists(`cache/tsconfig/${revisionHash(contents)}.json`) 77 | assert.isFalse(hasCacheFile) 78 | 79 | assert.isNull(options.error) 80 | assert.equal(options.version, Config.version) 81 | assert.property(options.options?.compilerOptions, 'esModuleInterop') 82 | assert.property(options.options?.compilerOptions, 'configFilePath') 83 | }) 84 | 85 | test('return error when tsconfig file is missing', async ({ assert }) => { 86 | const cwd = fs.basePath 87 | const cacheRoot = join(fs.basePath, 'cache') 88 | 89 | const config = new Config(cwd, cacheRoot, ts) 90 | const options = () => config.parse() 91 | assert.throws( 92 | options, 93 | '"@adonisjs/require-ts" expects the "tsconfig.json" file to exists in the app root' 94 | ) 95 | }) 96 | 97 | test('return error when tsconfig file is invalid', async ({ assert }) => { 98 | const cwd = fs.basePath 99 | const cacheRoot = join(fs.basePath, 'cache') 100 | const contents = JSON.stringify({ 101 | extends: './node_modules/@adonisjs/mrm-preset/_tsconfig', 102 | }) 103 | 104 | await fs.add('tsconfig.json', contents) 105 | 106 | const config = new Config(cwd, cacheRoot, ts) 107 | const options = config.parse() 108 | 109 | const hasCacheFile = await fs.exists(`cache/tsconfig/${revisionHash(contents)}.json`) 110 | assert.isFalse(hasCacheFile) 111 | 112 | assert.exists(options.error) 113 | assert.isNull(options.options) 114 | assert.equal( 115 | options.error![0].messageText, 116 | `File './node_modules/@adonisjs/mrm-preset/_tsconfig' not found.` 117 | ) 118 | }) 119 | 120 | test('read from cache when exists', async ({ assert }) => { 121 | const cwd = fs.basePath 122 | const cacheRoot = join(fs.basePath, 'cache') 123 | const contents = JSON.stringify({ 124 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 125 | }) 126 | 127 | await fs.add('tsconfig.json', contents) 128 | 129 | /** 130 | * Creating cache file 131 | */ 132 | await fs.add( 133 | `cache/tsconfig/${revisionHash(contents)}.json`, 134 | JSON.stringify({ 135 | version: Config.version, 136 | options: { 137 | compilerOptions: { 138 | dummyValue: true, 139 | }, 140 | }, 141 | }) 142 | ) 143 | 144 | const config = new Config(cwd, cacheRoot, ts) 145 | const options = config.parse() 146 | assert.isNull(options.error) 147 | assert.deepEqual(options.options, { 148 | compilerOptions: { dummyValue: true }, 149 | transformers: undefined, 150 | }) 151 | }) 152 | 153 | test('do not read from cache when caching is disabled', async ({ assert }) => { 154 | const cwd = fs.basePath 155 | const cacheRoot = join(fs.basePath, 'cache') 156 | const contents = JSON.stringify({ 157 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 158 | }) 159 | 160 | await fs.add('tsconfig.json', contents) 161 | 162 | /** 163 | * Creating cache file 164 | */ 165 | await fs.add( 166 | `cache/tsconfig/${revisionHash(contents)}.json`, 167 | JSON.stringify({ 168 | version: Config.version, 169 | options: { 170 | compilerOptions: { 171 | dummyValue: true, 172 | }, 173 | }, 174 | }) 175 | ) 176 | 177 | const config = new Config(cwd, cacheRoot, ts, false) 178 | const options = config.parse() 179 | assert.isNull(options.error) 180 | assert.notProperty(options.options?.compilerOptions, 'dummyValue') 181 | }) 182 | 183 | test('do not read from cache during version mis-match', async ({ assert }) => { 184 | const cwd = fs.basePath 185 | const cacheRoot = join(fs.basePath, 'cache') 186 | const contents = JSON.stringify({ 187 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 188 | }) 189 | 190 | await fs.add('tsconfig.json', contents) 191 | 192 | /** 193 | * Creating cache file 194 | */ 195 | await fs.add( 196 | `cache/tsconfig/${revisionHash(contents)}.json`, 197 | JSON.stringify({ 198 | version: 'v0.0', 199 | options: { 200 | compilerOptions: { 201 | dummyValue: true, 202 | }, 203 | }, 204 | }) 205 | ) 206 | 207 | const config = new Config(cwd, cacheRoot, ts) 208 | const options = config.parse() 209 | assert.isNull(options.error) 210 | assert.notProperty(options.options?.compilerOptions, 'dummyValue') 211 | }) 212 | 213 | test('do not read from cache during when file contents has changed', async ({ assert }) => { 214 | const cwd = fs.basePath 215 | const cacheRoot = join(fs.basePath, 'cache') 216 | const contents = JSON.stringify({ 217 | extends: './node_modules/@adonisjs/mrm-preset/_tsconfig', 218 | }) 219 | 220 | await fs.add( 221 | 'tsconfig.json', 222 | JSON.stringify({ 223 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 224 | }) 225 | ) 226 | 227 | /** 228 | * Creating cache file 229 | */ 230 | await fs.add( 231 | `cache/tsconfig/${revisionHash(contents)}.json`, 232 | JSON.stringify({ 233 | version: Config.version, 234 | options: { 235 | compilerOptions: { 236 | dummyValue: true, 237 | }, 238 | }, 239 | }) 240 | ) 241 | 242 | const config = new Config(cwd, cacheRoot, ts) 243 | const options = config.parse() 244 | assert.isNull(options.error) 245 | assert.notProperty(options.options?.compilerOptions, 'dummyValue') 246 | }) 247 | 248 | test('fetch transformers from the raw config file', async ({ assert }) => { 249 | const cwd = fs.basePath 250 | const cacheRoot = join(fs.basePath, 'cache') 251 | 252 | await fs.add( 253 | 'tsconfig.json', 254 | JSON.stringify({ 255 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 256 | transformers: { 257 | after: [{ transform: '@adonisjs/ioc-transformer' }], 258 | }, 259 | }) 260 | ) 261 | 262 | const config = new Config(cwd, cacheRoot, ts) 263 | const { error, options } = config.parse() 264 | 265 | assert.isNull(error) 266 | assert.deepEqual(options?.transformers, { 267 | after: [{ transform: '@adonisjs/ioc-transformer' }], 268 | before: undefined, 269 | afterDeclarations: undefined, 270 | }) 271 | }) 272 | 273 | test('cache transformers', async ({ assert }) => { 274 | const cwd = fs.basePath 275 | const cacheRoot = join(fs.basePath, 'cache') 276 | 277 | const contents = JSON.stringify({ 278 | extends: '../../node_modules/@adonisjs/mrm-preset/_tsconfig', 279 | transformers: { 280 | after: [{ transform: '@adonisjs/ioc-transformer' }], 281 | }, 282 | }) 283 | await fs.add('tsconfig.json', contents) 284 | 285 | const config = new Config(cwd, cacheRoot, ts) 286 | config.parse() 287 | 288 | const cacheContents = await fs.get(`cache/tsconfig/${revisionHash(contents)}.json`) 289 | assert.deepEqual(JSON.parse(cacheContents).options.transformers, { 290 | after: [{ transform: '@adonisjs/ioc-transformer' }], 291 | }) 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig" 3 | } 4 | --------------------------------------------------------------------------------