├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── workflow.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── documentation └── asset │ ├── di-logo.png │ └── di-logo.svg ├── eslint.config.js ├── loader.cjs ├── package.json ├── pnpm-lock.yaml ├── sandhog.config.js ├── src ├── index.ts ├── loader │ ├── cjs │ │ └── loader.ts │ ├── esm │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ └── load.ts │ │ ├── loader.ts │ │ ├── register.ts │ │ └── types.ts │ └── shared.ts ├── transformer │ ├── after │ │ ├── after-transformer.ts │ │ ├── after-visitor-options.ts │ │ └── visitor │ │ │ ├── visit-define-array-literal-expression.ts │ │ │ ├── visit-node.ts │ │ │ ├── visit-root-block-block.ts │ │ │ ├── visit-root-block-source-file.ts │ │ │ └── visit-root-block.ts │ ├── before │ │ ├── before-transformer.ts │ │ ├── before-visitor-options.ts │ │ ├── util.ts │ │ └── visitor │ │ │ ├── visit-call-expression.ts │ │ │ ├── visit-class-like-declaration.ts │ │ │ └── visit-node.ts │ ├── cache.ts │ ├── constant.ts │ ├── di-method-kind.ts │ ├── di-options.ts │ ├── di.ts │ ├── get-base-visitor-context.ts │ ├── transform-options.ts │ ├── transform.ts │ ├── visitor-context.ts │ ├── visitor-continuation.ts │ └── visitor-options.ts ├── type │ ├── imported-symbol.ts │ ├── root-block.ts │ └── type.ts └── util │ ├── ts-util.ts │ └── util.ts ├── test ├── amd.test.ts ├── commonjs.test.ts ├── constructor-arguments.test.ts ├── container.test.ts ├── esm.test.ts ├── setup │ ├── setup-custom-transformer.ts │ └── setup-transform.ts ├── transform.test.ts ├── umd.test.ts ├── util │ ├── format-code.ts │ ├── include-emit-helper.ts │ └── test-runner.ts └── verbatim-module-syntax.test.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wessberg 2 | patreon: wessberg 3 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Main Workflow 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | name: Run 8 | 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [windows-latest, macos-latest, ubuntu-latest] 14 | node: [21, 22] 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@master 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@master 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: Setup pnpm 26 | run: npm install pnpm -g 27 | 28 | - name: Install 29 | run: pnpm install 30 | 31 | - name: Lint 32 | run: pnpm run lint 33 | 34 | - name: Test 35 | run: pnpm test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /compiled/ 2 | /.idea/ 3 | /.cache/ 4 | /.vscode/ 5 | *.log 6 | /logs/ 7 | npm-debug.log* 8 | /lib-cov/ 9 | /coverage/ 10 | /.nyc_output/ 11 | /.grunt/ 12 | *.7z 13 | *.dmg 14 | *.gz 15 | *.iso 16 | *.jar 17 | *.rar 18 | *.tar 19 | *.zip 20 | .tgz 21 | .env 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | ehthumbs.db 28 | Thumbs.db 29 | *.pem 30 | *.p12 31 | *.crt 32 | *.csr 33 | /node_modules/ 34 | /dist/ 35 | package-lock.json -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged --quiet -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.0.2](https://github.com/wessberg/di-compiler/compare/v4.0.1...v4.0.2) (2024-10-31) 2 | 3 | ## [4.0.1](https://github.com/wessberg/di-compiler/compare/v4.0.0...v4.0.1) (2024-10-11) 4 | 5 | # [4.0.0](https://github.com/wessberg/di-compiler/compare/v4.0.0-beta.1...v4.0.0) (2024-10-10) 6 | 7 | # [4.0.0-beta.1](https://github.com/wessberg/di-compiler/compare/v3.3.0...v4.0.0-beta.1) (2024-10-01) 8 | 9 | ### Features 10 | 11 | - add TypeScript v5.6 support ([c502c7d](https://github.com/wessberg/di-compiler/commit/c502c7dc843949d5ebe95cf90584ed276085c8ca)) 12 | - make compatible with TypeScript v5.6 and Node >= 18.20, including the new loader mechanism ([9e5fe79](https://github.com/wessberg/di-compiler/commit/9e5fe79156de25a19c3ab99fc0b8634db789455f)) 13 | 14 | # [3.3.0](https://github.com/wessberg/di-compiler/compare/v3.2.0...v3.3.0) (2023-08-04) 15 | 16 | ### Bug Fixes 17 | 18 | - run tests on Node v16.14.0 and up ([1357846](https://github.com/wessberg/di-compiler/commit/1357846d1c9bfc6e4f01f618de8e8ed57d1dce88)) 19 | - run tests on v16.15.1 instead ([b6bac40](https://github.com/wessberg/di-compiler/commit/b6bac405d50f803fbd77221a6fe07ed97d7b2b2a)) 20 | 21 | ### Features 22 | 23 | - add TypeScript v5.1 support ([3d0b582](https://github.com/wessberg/di-compiler/commit/3d0b582adee65dd54acdefed80ec337de6561809)) 24 | 25 | # [3.2.0](https://github.com/wessberg/di-compiler/compare/v3.1.0...v3.2.0) (2023-01-23) 26 | 27 | ### Features 28 | 29 | - add .js extensions to built types to make them compatible with codebases running with Node16 or NodeNext module resolution ([87b74d1](https://github.com/wessberg/di-compiler/commit/87b74d16f0c3a431151bfb7869b914f7eba9620a)) 30 | 31 | # [3.1.0](https://github.com/wessberg/di-compiler/compare/v3.0.0...v3.1.0) (2023-01-10) 32 | 33 | # [3.0.0](https://github.com/wessberg/di-compiler/compare/v2.2.6...v3.0.0) (2022-08-01) 34 | 35 | ### Features 36 | 37 | - add new sponsor: scrubtheweb ([80eb1e2](https://github.com/wessberg/di-compiler/commit/80eb1e2ecee16851de867eb50c4c2e53ea00653f)) 38 | - add TypeScript v4.6 support ([7072df7](https://github.com/wessberg/di-compiler/commit/7072df7d83d93e29ad4662d39d6b6047ef84f1e5)) 39 | - migrate to ESM. Make passing a TypeScript program optional ([e4d9d0b](https://github.com/wessberg/di-compiler/commit/e4d9d0bd1b75f0d7982375d739be634651b47056)) 40 | - support operating without a Program ([d489cbc](https://github.com/wessberg/di-compiler/commit/d489cbc91b7434653f55f581295ea12843c993fd)) 41 | 42 | ## [2.2.6](https://github.com/wessberg/di-compiler/compare/v2.2.5...v2.2.6) (2021-11-19) 43 | 44 | ### Features 45 | 46 | - add TypeScript v4.5 support ([b3960ce](https://github.com/wessberg/di-compiler/commit/b3960cee9b13a36ba38a6f10d8627e43e73800df)) 47 | 48 | ## [2.2.5](https://github.com/wessberg/di-compiler/compare/v2.2.4...v2.2.5) (2021-05-29) 49 | 50 | ## [2.2.4](https://github.com/wessberg/di-compiler/compare/v2.2.3...v2.2.4) (2021-05-29) 51 | 52 | ## [2.2.3](https://github.com/wessberg/di-compiler/compare/v2.2.2...v2.2.3) (2021-05-28) 53 | 54 | ## [2.2.2](https://github.com/wessberg/di-compiler/compare/v2.2.1...v2.2.2) (2021-05-28) 55 | 56 | ## [2.2.1](https://github.com/wessberg/di-compiler/compare/v2.2.0...v2.2.1) (2021-05-21) 57 | 58 | ### Bug Fixes 59 | 60 | - don't include type arguments as part of the service identifier inside parsed constructor arguments ([3a2f3ff](https://github.com/wessberg/di-compiler/commit/3a2f3ff2f825b0f85b073310da5d16bf3f0eaf98)) 61 | 62 | # [2.2.0](https://github.com/wessberg/di-compiler/compare/v2.1.1...v2.2.0) (2021-05-21) 63 | 64 | ### Bug Fixes 65 | 66 | - **imports:** fix a bug where registering the same implementation multiple times will generate multiple imports ([f8c388b](https://github.com/wessberg/di-compiler/commit/f8c388b07351e737b51ff021a013c867f6a3c008)) 67 | - make all tests pass on every TypeScript version in the range [3.0, 4.3-rc1] ([265ac93](https://github.com/wessberg/di-compiler/commit/265ac93282d58fab4d6ccc6ddd17e52592e27eff)) 68 | - type arguments passed to registerSingleton and registerTransient should themselves be allowed to receive type arguments, and these should not count towards the service/implementation name ([76773ab](https://github.com/wessberg/di-compiler/commit/76773ab5a0846c3858c86e111fac35656583070e)) 69 | 70 | ## [2.1.1](https://github.com/wessberg/di-compiler/compare/v2.1.0...v2.1.1) (2020-06-01) 71 | 72 | ### Bug Fixes 73 | 74 | - remove throw statement for SystemJS as compiletarget ([c8d370e](https://github.com/wessberg/di-compiler/commit/c8d370e4116d19cdd8a50276a86619ac91beb6d6)) 75 | 76 | # [2.1.0](https://github.com/wessberg/di-compiler/compare/v2.0.5...v2.1.0) (2020-05-27) 77 | 78 | ### Features 79 | 80 | - major refactoring with support for CommonJS, AMD, and SystemJS as targets ([bfeedef](https://github.com/wessberg/di-compiler/commit/bfeedef6db6c6624f1ed861db815b53f471cef30)) 81 | 82 | ## [2.0.5](https://github.com/wessberg/di-compiler/compare/v2.0.4...v2.0.5) (2019-06-21) 83 | 84 | ## [2.0.4](https://github.com/wessberg/di-compiler/compare/v2.0.3...v2.0.4) (2019-05-29) 85 | 86 | ## [2.0.3](https://github.com/wessberg/di-compiler/compare/v2.0.2...v2.0.3) (2019-05-29) 87 | 88 | ## [2.0.2](https://github.com/wessberg/di-compiler/compare/v2.0.1...v2.0.2) (2018-11-28) 89 | 90 | ## [2.0.1](https://github.com/wessberg/di-compiler/compare/v2.0.0...v2.0.1) (2018-11-14) 91 | 92 | # [2.0.0](https://github.com/wessberg/di-compiler/compare/v1.0.76...v2.0.0) (2018-11-12) 93 | 94 | ## [1.0.76](https://github.com/wessberg/di-compiler/compare/v1.0.75...v1.0.76) (2018-09-19) 95 | 96 | ## [1.0.75](https://github.com/wessberg/di-compiler/compare/v1.0.74...v1.0.75) (2018-07-31) 97 | 98 | ## [1.0.74](https://github.com/wessberg/di-compiler/compare/v1.0.73...v1.0.74) (2018-06-21) 99 | 100 | ## [1.0.73](https://github.com/wessberg/di-compiler/compare/v1.0.72...v1.0.73) (2018-06-21) 101 | 102 | ## [1.0.72](https://github.com/wessberg/di-compiler/compare/v1.0.71...v1.0.72) (2018-06-13) 103 | 104 | ## [1.0.71](https://github.com/wessberg/di-compiler/compare/v1.0.70...v1.0.71) (2018-06-13) 105 | 106 | ## [1.0.70](https://github.com/wessberg/di-compiler/compare/v1.0.69...v1.0.70) (2018-06-08) 107 | 108 | ## [1.0.69](https://github.com/wessberg/di-compiler/compare/v1.0.68...v1.0.69) (2018-05-10) 109 | 110 | ## [1.0.68](https://github.com/wessberg/di-compiler/compare/v1.0.67...v1.0.68) (2018-04-30) 111 | 112 | ## [1.0.67](https://github.com/wessberg/di-compiler/compare/v1.0.66...v1.0.67) (2018-04-25) 113 | 114 | ## [1.0.66](https://github.com/wessberg/di-compiler/compare/v1.0.65...v1.0.66) (2018-04-18) 115 | 116 | ## [1.0.65](https://github.com/wessberg/di-compiler/compare/v1.0.64...v1.0.65) (2018-04-03) 117 | 118 | ## [1.0.64](https://github.com/wessberg/di-compiler/compare/v1.0.63...v1.0.64) (2018-03-31) 119 | 120 | ## [1.0.63](https://github.com/wessberg/di-compiler/compare/v1.0.62...v1.0.63) (2018-03-31) 121 | 122 | ## [1.0.62](https://github.com/wessberg/di-compiler/compare/v1.0.61...v1.0.62) (2018-02-03) 123 | 124 | ## [1.0.61](https://github.com/wessberg/di-compiler/compare/v1.0.60...v1.0.61) (2017-10-21) 125 | 126 | ## [1.0.60](https://github.com/wessberg/di-compiler/compare/v1.0.59...v1.0.60) (2017-09-18) 127 | 128 | ## [1.0.59](https://github.com/wessberg/di-compiler/compare/v1.0.58...v1.0.59) (2017-09-10) 129 | 130 | ## [1.0.58](https://github.com/wessberg/di-compiler/compare/v1.0.57...v1.0.58) (2017-09-10) 131 | 132 | ## [1.0.57](https://github.com/wessberg/di-compiler/compare/v1.0.56...v1.0.57) (2017-09-10) 133 | 134 | ## [1.0.56](https://github.com/wessberg/di-compiler/compare/v1.0.55...v1.0.56) (2017-09-10) 135 | 136 | ## [1.0.55](https://github.com/wessberg/di-compiler/compare/v1.0.54...v1.0.55) (2017-09-04) 137 | 138 | ## [1.0.54](https://github.com/wessberg/di-compiler/compare/v1.0.53...v1.0.54) (2017-09-04) 139 | 140 | ## [1.0.53](https://github.com/wessberg/di-compiler/compare/v1.0.52...v1.0.53) (2017-09-04) 141 | 142 | ## [1.0.52](https://github.com/wessberg/di-compiler/compare/v1.0.51...v1.0.52) (2017-09-03) 143 | 144 | ## [1.0.51](https://github.com/wessberg/di-compiler/compare/v1.0.50...v1.0.51) (2017-09-03) 145 | 146 | ## [1.0.50](https://github.com/wessberg/di-compiler/compare/v1.0.49...v1.0.50) (2017-09-03) 147 | 148 | ## [1.0.49](https://github.com/wessberg/di-compiler/compare/v1.0.48...v1.0.49) (2017-09-03) 149 | 150 | ## [1.0.48](https://github.com/wessberg/di-compiler/compare/v1.0.47...v1.0.48) (2017-09-03) 151 | 152 | ## [1.0.47](https://github.com/wessberg/di-compiler/compare/v1.0.46...v1.0.47) (2017-08-28) 153 | 154 | ## [1.0.46](https://github.com/wessberg/di-compiler/compare/v1.0.45...v1.0.46) (2017-08-28) 155 | 156 | ## [1.0.45](https://github.com/wessberg/di-compiler/compare/v1.0.44...v1.0.45) (2017-08-28) 157 | 158 | ## [1.0.44](https://github.com/wessberg/di-compiler/compare/v1.0.43...v1.0.44) (2017-08-28) 159 | 160 | ## [1.0.43](https://github.com/wessberg/di-compiler/compare/v1.0.42...v1.0.43) (2017-08-28) 161 | 162 | ## [1.0.42](https://github.com/wessberg/di-compiler/compare/v1.0.41...v1.0.42) (2017-08-28) 163 | 164 | ## [1.0.41](https://github.com/wessberg/di-compiler/compare/v1.0.40...v1.0.41) (2017-08-28) 165 | 166 | ## [1.0.40](https://github.com/wessberg/di-compiler/compare/v1.0.39...v1.0.40) (2017-08-17) 167 | 168 | ## [1.0.39](https://github.com/wessberg/di-compiler/compare/v1.0.38...v1.0.39) (2017-08-03) 169 | 170 | ## [1.0.38](https://github.com/wessberg/di-compiler/compare/v1.0.37...v1.0.38) (2017-07-28) 171 | 172 | ## [1.0.37](https://github.com/wessberg/di-compiler/compare/v1.0.36...v1.0.37) (2017-07-28) 173 | 174 | ## [1.0.36](https://github.com/wessberg/di-compiler/compare/v1.0.34...v1.0.36) (2017-07-28) 175 | 176 | ## [1.0.34](https://github.com/wessberg/di-compiler/compare/v1.0.33...v1.0.34) (2017-07-20) 177 | 178 | ## [1.0.33](https://github.com/wessberg/di-compiler/compare/v1.0.32...v1.0.33) (2017-07-19) 179 | 180 | ## [1.0.32](https://github.com/wessberg/di-compiler/compare/v1.0.31...v1.0.32) (2017-07-19) 181 | 182 | ## [1.0.31](https://github.com/wessberg/di-compiler/compare/v1.0.30...v1.0.31) (2017-07-19) 183 | 184 | ## [1.0.30](https://github.com/wessberg/di-compiler/compare/v1.0.29...v1.0.30) (2017-07-19) 185 | 186 | ## [1.0.29](https://github.com/wessberg/di-compiler/compare/v1.0.28...v1.0.29) (2017-07-19) 187 | 188 | ## [1.0.28](https://github.com/wessberg/di-compiler/compare/v1.0.27...v1.0.28) (2017-07-19) 189 | 190 | ## [1.0.27](https://github.com/wessberg/di-compiler/compare/v1.0.26...v1.0.27) (2017-07-18) 191 | 192 | ## [1.0.26](https://github.com/wessberg/di-compiler/compare/v1.0.25...v1.0.26) (2017-07-18) 193 | 194 | ## [1.0.25](https://github.com/wessberg/di-compiler/compare/v1.0.24...v1.0.25) (2017-07-06) 195 | 196 | ## [1.0.24](https://github.com/wessberg/di-compiler/compare/v1.0.23...v1.0.24) (2017-07-06) 197 | 198 | ## [1.0.23](https://github.com/wessberg/di-compiler/compare/v1.0.22...v1.0.23) (2017-07-05) 199 | 200 | ## [1.0.22](https://github.com/wessberg/di-compiler/compare/v1.0.21...v1.0.22) (2017-07-05) 201 | 202 | ## [1.0.21](https://github.com/wessberg/di-compiler/compare/v1.0.20...v1.0.21) (2017-07-05) 203 | 204 | ## [1.0.20](https://github.com/wessberg/di-compiler/compare/v1.0.19...v1.0.20) (2017-07-05) 205 | 206 | ## [1.0.19](https://github.com/wessberg/di-compiler/compare/v1.0.18...v1.0.19) (2017-07-05) 207 | 208 | ## [1.0.18](https://github.com/wessberg/di-compiler/compare/v1.0.17...v1.0.18) (2017-05-31) 209 | 210 | ## [1.0.17](https://github.com/wessberg/di-compiler/compare/v1.0.16...v1.0.17) (2017-05-31) 211 | 212 | ## [1.0.16](https://github.com/wessberg/di-compiler/compare/v1.0.15...v1.0.16) (2017-05-31) 213 | 214 | ## [1.0.15](https://github.com/wessberg/di-compiler/compare/v1.0.14...v1.0.15) (2017-05-31) 215 | 216 | ## [1.0.14](https://github.com/wessberg/di-compiler/compare/v1.0.13...v1.0.14) (2017-05-31) 217 | 218 | ## [1.0.13](https://github.com/wessberg/di-compiler/compare/v1.0.12...v1.0.13) (2017-05-31) 219 | 220 | ## [1.0.12](https://github.com/wessberg/di-compiler/compare/v1.0.11...v1.0.12) (2017-05-31) 221 | 222 | ## [1.0.11](https://github.com/wessberg/di-compiler/compare/v1.0.10...v1.0.11) (2017-05-31) 223 | 224 | ## [1.0.10](https://github.com/wessberg/di-compiler/compare/v1.0.9...v1.0.10) (2017-05-31) 225 | 226 | ## [1.0.9](https://github.com/wessberg/di-compiler/compare/v1.0.8...v1.0.9) (2017-05-31) 227 | 228 | ## [1.0.8](https://github.com/wessberg/di-compiler/compare/v1.0.7...v1.0.8) (2017-05-30) 229 | 230 | ## [1.0.7](https://github.com/wessberg/di-compiler/compare/v1.0.6...v1.0.7) (2017-05-30) 231 | 232 | ## [1.0.6](https://github.com/wessberg/di-compiler/compare/v1.0.5...v1.0.6) (2017-05-30) 233 | 234 | ## [1.0.5](https://github.com/wessberg/di-compiler/compare/v1.0.4...v1.0.5) (2017-05-30) 235 | 236 | ## [1.0.4](https://github.com/wessberg/di-compiler/compare/v1.0.3...v1.0.4) (2017-05-25) 237 | 238 | ## [1.0.3](https://github.com/wessberg/di-compiler/compare/v1.0.2...v1.0.3) (2017-05-25) 239 | 240 | ## [1.0.2](https://github.com/wessberg/di-compiler/compare/v1.0.1...v1.0.2) (2017-05-24) 241 | 242 | ## 1.0.1 (2017-05-24) 243 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | 3 | Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting any of the code of conduct enforcers: . 59 | All complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | Attribution 69 | 70 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 71 | available at http://contributor-covenant.org/version/1/4/ 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | You are more than welcome to contribute to `undefined` in any way you please, including: 2 | 3 | - Updating documentation. 4 | - Fixing spelling and grammar 5 | - Adding tests 6 | - Fixing issues and suggesting new features 7 | - Blogging, tweeting, and creating tutorials about `undefined` 8 | 9 | - Submit an issue or a Pull Request 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /documentation/asset/di-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wessberg/DI-compiler/9619c1d44bde0b4eda8c6b85f84ad180fe065203/documentation/asset/di-logo.png -------------------------------------------------------------------------------- /documentation/asset/di-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import shared from "@wessberg/ts-config/eslint.config.js"; 2 | 3 | export default [ 4 | ...shared, 5 | { 6 | rules: {} 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /loader.cjs: -------------------------------------------------------------------------------- 1 | require("./dist/loader/cjs/loader.cjs"); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wessberg/di-compiler", 3 | "version": "4.0.2", 4 | "description": "A Custom Transformer for Typescript that enables compile-time Dependency Injection", 5 | "scripts": { 6 | "generate:sandhog": "sandhog all --yes", 7 | "generate:changelog": "standard-changelog --first-release", 8 | "generate:all": "pnpm run generate:sandhog && pnpm run generate:changelog", 9 | "clean": "rimraf dist", 10 | "lint": "tsc --noEmit && eslint \"src/**/*.ts\" --color", 11 | "prettier": "prettier --write \"{src,test,documentation}/**/*.{js,ts,json,html,xml,css,md}\"", 12 | "test": "node --import tsx --test \"./test/**/*.test.ts\"", 13 | "prebuild": "pnpm run clean", 14 | "build": "pnpm run clean && pnpm run build:esm && pnpm run build:cjs", 15 | "build:esm": "tsup --entry=\"src/index.ts\" --entry=\"src/loader/esm/loader.ts\" --entry=\"src/loader/esm/hooks/index.ts\" --sourcemap --dts --format esm", 16 | "build:cjs": "tsup --entry=\"src/index.ts\" --entry=\"src/loader/cjs/loader.ts\" --sourcemap --splitting --dts --format cjs", 17 | "preversion": "pnpm run lint && pnpm run build", 18 | "version": "pnpm run preversion && pnpm run generate:all && git add .", 19 | "release": "np --no-cleanup --no-yarn", 20 | "update:check": "pnpx npm-check-updates -x typescript-* --dep dev,prod", 21 | "update:commit": "pnpx npm-check-updates -u -x typescript-* --dep dev,prod && pnpm update && pnpm install" 22 | }, 23 | "keywords": [ 24 | "DI", 25 | "dependency injection", 26 | "ioc", 27 | "inversion", 28 | "service", 29 | "container", 30 | "newable", 31 | "reflection", 32 | "singleton", 33 | "transient", 34 | "compiler" 35 | ], 36 | "files": [ 37 | "dist/**/*.*", 38 | "loader.*" 39 | ], 40 | "contributors": [ 41 | { 42 | "name": "Frederik Wessberg", 43 | "email": "frederikwessberg@hotmail.com", 44 | "url": "https://github.com/wessberg", 45 | "imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4", 46 | "role": "Lead Developer", 47 | "twitter": "FredWessberg", 48 | "github": "wessberg" 49 | } 50 | ], 51 | "license": "MIT", 52 | "devDependencies": { 53 | "@prettier/sync": "0.5.2", 54 | "@types/node": "^22.7.5", 55 | "@types/semver": "^7.5.8", 56 | "@wessberg/prettier-config": "^1.0.0", 57 | "@wessberg/ts-config": "^5.0.20", 58 | "@eslint/js": "9.12.0", 59 | "eslint": "^9.12.0", 60 | "eslint-config-prettier": "^9.1.0", 61 | "eslint-plugin-jsdoc": "^50.3.1", 62 | "eslint-plugin-prettier": "^5.2.1", 63 | "typescript-eslint": "^8.8.1", 64 | "husky": "^9.1.6", 65 | "lint-staged": "^15.2.10", 66 | "np": "^10.0.7", 67 | "npm-check-updates": "^17.1.3", 68 | "pnpm": "^9.12.1", 69 | "prettier": "^3.3.3", 70 | "rimraf": "^6.0.1", 71 | "sandhog": "^3.0.1", 72 | "semver": "^7.6.3", 73 | "standard-changelog": "^6.0.0", 74 | "tsup": "^8.3.0", 75 | "tsx": "^4.19.1", 76 | "typescript": "^5.6.3", 77 | "typescript-3-4-1": "npm:typescript@3.4.1", 78 | "typescript-3-5-1": "npm:typescript@3.5.1", 79 | "typescript-3-6-2": "npm:typescript@3.6.2", 80 | "typescript-3-7-2": "npm:typescript@3.7.2", 81 | "typescript-3-8-3": "npm:typescript@3.8.3", 82 | "typescript-3-9-2": "npm:typescript@3.9.2", 83 | "typescript-4-0-3": "npm:typescript@4.0.3", 84 | "typescript-4-1-2": "npm:typescript@4.1.2", 85 | "typescript-4-2-4": "npm:typescript@4.2.4", 86 | "typescript-4-3-5": "npm:typescript@4.3.5", 87 | "typescript-4-4-2": "npm:typescript@4.4.2", 88 | "typescript-4-5-4": "npm:typescript@4.5.4", 89 | "typescript-4-6-4": "npm:typescript@4.6.4", 90 | "typescript-4-7-2": "npm:typescript@4.7.2", 91 | "typescript-4-8-2": "npm:typescript@4.8.2", 92 | "typescript-4-9-4": "npm:typescript@4.9.4", 93 | "typescript-5-0-4": "npm:typescript@5.0.4", 94 | "typescript-5-1-6": "npm:typescript@5.1.6", 95 | "typescript-5-2-2": "npm:typescript@5.2.2", 96 | "typescript-5-3-3": "npm:typescript@5.3.3", 97 | "typescript-5-4-5": "npm:typescript@5.4.5", 98 | "typescript-5-5-4": "npm:typescript@5.5.4", 99 | "typescript-5-6-2": "npm:typescript@5.6.2" 100 | }, 101 | "dependencies": { 102 | "compatfactory": "^4.0.2", 103 | "crosspath": "^2.0.0", 104 | "get-tsconfig": "^4.8.1", 105 | "helpertypes": "^0.0.19", 106 | "ts-evaluator": "^2.0.0" 107 | }, 108 | "peerDependencies": { 109 | "pirates": ">=4.x", 110 | "typescript": ">=3.x || >= 4.x || >= 5.x" 111 | }, 112 | "peerDependenciesMeta": { 113 | "pirates": { 114 | "optional": true 115 | } 116 | }, 117 | "repository": { 118 | "type": "git", 119 | "url": "https://github.com/wessberg/di-compiler.git" 120 | }, 121 | "bugs": { 122 | "url": "https://github.com/wessberg/di-compiler/issues" 123 | }, 124 | "exports": { 125 | "./loader": { 126 | "import": "./dist/loader/esm/loader.js", 127 | "require": "./dist/loader/cjs/loader.cjs" 128 | }, 129 | ".": { 130 | "import": "./dist/index.js", 131 | "require": "./dist/index.cjs" 132 | } 133 | }, 134 | "type": "module", 135 | "types": "./dist/index.d.ts", 136 | "main": "./dist/index.cjs", 137 | "module": "./dist/index.js", 138 | "funding": { 139 | "type": "github", 140 | "url": "https://github.com/wessberg/di-compiler?sponsor=1" 141 | }, 142 | "engines": { 143 | "node": ">=18.20.0" 144 | }, 145 | "lint-staged": { 146 | "*": "prettier --ignore-unknown --write" 147 | }, 148 | "prettier": "@wessberg/prettier-config" 149 | } 150 | -------------------------------------------------------------------------------- /sandhog.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@wessberg/ts-config/sandhog.config.js"; 2 | 3 | export default { 4 | ...baseConfig, 5 | logo: { 6 | url: "https://raw.githubusercontent.com/wessberg/di-compiler/master/documentation/asset/di-logo.png", 7 | height: 150, 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./transformer/di.js"; 2 | export * from "./transformer/transform.js"; 3 | export type * from "./transformer/transform-options.js"; 4 | export type * from "./transformer/di-options.js"; 5 | -------------------------------------------------------------------------------- /src/loader/cjs/loader.ts: -------------------------------------------------------------------------------- 1 | import typescript from "typescript"; 2 | import {transform} from "../../transformer/transform.js"; 3 | import pirates from "pirates"; 4 | import {ALLOWED_EXTENSIONS, resolveOptions} from "../shared.js"; 5 | import type {TS} from "../../type/type.js"; 6 | 7 | const transformOptions = resolveOptions(typescript as typeof TS); 8 | 9 | pirates.addHook( 10 | (code, filename) => 11 | transform(code.toString(), filename, { 12 | ...transformOptions, 13 | typescript: typescript as typeof TS 14 | }).code, 15 | {exts: [...ALLOWED_EXTENSIONS]} 16 | ); 17 | -------------------------------------------------------------------------------- /src/loader/esm/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./initialize.js"; 2 | export * from "./load.js"; 3 | -------------------------------------------------------------------------------- /src/loader/esm/hooks/initialize.ts: -------------------------------------------------------------------------------- 1 | import type {InitializeHook} from "node:module"; 2 | import typescript from "typescript"; 3 | import {resolveOptions} from "../../shared.js"; 4 | import type {InitializationOptions} from "../types.js"; 5 | import type {TS} from "../../../type/type.js"; 6 | 7 | let data: InitializationOptions | undefined; 8 | 9 | export function getData(): InitializationOptions | undefined { 10 | return data; 11 | } 12 | 13 | export const initialize: InitializeHook = (options?: InitializationOptions) => { 14 | if (options == null) { 15 | throw new Error("di-compiler must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0 and v18.19.0"); 16 | } 17 | 18 | if (data == null) { 19 | data = {port: options.port}; 20 | } else { 21 | data.port = options.port; 22 | } 23 | 24 | data.transformOptions = resolveOptions(typescript as typeof TS); 25 | data.typescript = typescript as typeof TS; 26 | }; 27 | -------------------------------------------------------------------------------- /src/loader/esm/hooks/load.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs/promises"; 3 | import urlModule from "node:url"; 4 | import type {LoadHook} from "node:module"; 5 | import {transform} from "../../../transformer/transform.js"; 6 | import {ALLOWED_EXTENSIONS} from "../../shared.js"; 7 | import {getData} from "./initialize.js"; 8 | 9 | export const load: LoadHook = async (url, context, nextLoad) => { 10 | const loaded = await nextLoad(url, context); 11 | const data = getData(); 12 | 13 | const filePath = url.startsWith("file://") ? urlModule.fileURLToPath(url) : url; 14 | 15 | if (ALLOWED_EXTENSIONS.has(path.extname(filePath))) { 16 | const rawSource = await fs.readFile(new URL(url), "utf-8"); 17 | 18 | const {code: source} = transform(rawSource.toString(), filePath, data?.transformOptions); 19 | loaded.source = source; 20 | } 21 | 22 | return loaded; 23 | }; 24 | -------------------------------------------------------------------------------- /src/loader/esm/loader.ts: -------------------------------------------------------------------------------- 1 | import {register} from "./register.js"; 2 | register(); 3 | -------------------------------------------------------------------------------- /src/loader/esm/register.ts: -------------------------------------------------------------------------------- 1 | import Module from "node:module"; 2 | import {MessageChannel} from "node:worker_threads"; 3 | import type {InitializationOptions} from "./types.js"; 4 | 5 | export function register() { 6 | process.setSourceMapsEnabled(true); 7 | 8 | const {port1, port2} = new MessageChannel(); 9 | port1.unref(); 10 | 11 | Module.register( 12 | // Load new copy of loader so it can be registered multiple times 13 | `./hooks/index.js`, 14 | { 15 | parentURL: import.meta.url, 16 | data: {port: port2} satisfies InitializationOptions, 17 | transferList: [port2] 18 | } 19 | ); 20 | 21 | port1.postMessage({stderrIsTTY: !!process.stderr.isTTY}); 22 | } 23 | -------------------------------------------------------------------------------- /src/loader/esm/types.ts: -------------------------------------------------------------------------------- 1 | import type {MessagePort} from "node:worker_threads"; 2 | import type {TransformOptions} from "../../transformer/transform-options.js"; 3 | import type {TS} from "../../type/type.js"; 4 | 5 | export interface InitializationOptions { 6 | port: MessagePort; 7 | transformOptions?: Partial; 8 | typescript?: typeof TS; 9 | } 10 | -------------------------------------------------------------------------------- /src/loader/shared.ts: -------------------------------------------------------------------------------- 1 | import {getTsconfig, parseTsconfig, type TsConfigResult, type TsConfigJsonResolved} from "get-tsconfig"; 2 | import type {MaybeArray} from "helpertypes"; 3 | import {FileCache} from "../transformer/cache.js"; 4 | import type {TransformOptions, TransformResult} from "../transformer/transform-options.js"; 5 | import type {TS} from "../type/type.js"; 6 | import {booleanize} from "../util/util.js"; 7 | 8 | export const ENV_VARIABLE_TSCONFIG_PATH = "DI_COMPILER_TSCONFIG_PATH"; 9 | export const ENV_VARIABLE_IDENTIFIER = "DI_COMPILER_IDENTIFIER"; 10 | export const ENV_VARIABLE_DISABLE_CACHE = "DI_COMPILER_DISABLE_CACHE"; 11 | // Only these formats have type information that can be transpiled with DICompiler 12 | export const ALLOWED_EXTENSIONS = new Set([".ts", ".mts", ".cts"]); 13 | 14 | interface DiTsconfigOptions { 15 | /** 16 | * The identifier(s) that should be considered instances of DIContainer. When not given, an attempt will be 17 | * made to evaluate and resolve the value of identifiers to check if they are instances of DIContainer. 18 | * Providing one or more identifiers up front can be considered an optimization, as this step can be skipped that way 19 | */ 20 | identifier?: MaybeArray; 21 | disableCache: boolean; 22 | } 23 | 24 | interface ExtendedTsconfig { 25 | di?: Partial; 26 | compilerOptions: TS.CompilerOptions; 27 | } 28 | 29 | export function resolveOptions(typescript: typeof TS): Partial { 30 | // check if a custom path is in use for the tsconfig.json file 31 | const tsconfigFlagIndex = typescript.sys.args.findIndex(arg => arg === "-p" || arg === "--project"); 32 | const tsconfigFile = tsconfigFlagIndex !== -1 ? typescript.sys.args[tsconfigFlagIndex + 1] : undefined; 33 | 34 | const tsconfig = upgradeTsconfig( 35 | typescript, 36 | process.env[ENV_VARIABLE_TSCONFIG_PATH] != null 37 | ? { 38 | path: process.env[ENV_VARIABLE_TSCONFIG_PATH], 39 | config: parseTsconfig(process.env[ENV_VARIABLE_TSCONFIG_PATH]) 40 | } 41 | : (getTsconfig(tsconfigFile) ?? undefined) 42 | ); 43 | 44 | let identifier = 45 | process.env[ENV_VARIABLE_IDENTIFIER]?.split(",") 46 | .map(item => item.trim()) 47 | .filter(item => item.length > 0) ?? tsconfig.di?.identifier; 48 | 49 | if (Array.isArray(identifier) && identifier.length === 1) { 50 | identifier = identifier[0]; 51 | } 52 | 53 | const disableCache = process.env[ENV_VARIABLE_DISABLE_CACHE] == null ? (tsconfig.di?.disableCache ?? false) : booleanize(process.env[ENV_VARIABLE_DISABLE_CACHE]); 54 | 55 | return { 56 | identifier, 57 | 58 | compilerOptions: tsconfig.compilerOptions, 59 | cache: disableCache ? new Map() : new FileCache(), 60 | printer: typescript.createPrinter() 61 | }; 62 | } 63 | 64 | function upgradeTsconfig(typescript: typeof TS, input?: TsConfigResult | TsConfigJsonResolved): ExtendedTsconfig { 65 | if (input == null) { 66 | return { 67 | compilerOptions: overrideCompilerOptions(typescript.getDefaultCompilerOptions()) 68 | }; 69 | } 70 | const inputDiOptions = "config" in input ? (input.config as {di?: Partial}).di : (input as {di?: Partial}).di; 71 | const inputCompilerOptions = "config" in input ? input.config.compilerOptions : input.compilerOptions; 72 | if (inputCompilerOptions == null) { 73 | return { 74 | di: inputDiOptions, 75 | compilerOptions: overrideCompilerOptions(typescript.getDefaultCompilerOptions()) 76 | }; 77 | } 78 | return { 79 | di: inputDiOptions, 80 | compilerOptions: overrideCompilerOptions(typescript.convertCompilerOptionsFromJson(inputCompilerOptions, inputCompilerOptions.baseUrl ?? ".").options) 81 | }; 82 | } 83 | 84 | function overrideCompilerOptions(input: TS.CompilerOptions): TS.CompilerOptions { 85 | return { 86 | ...input, 87 | // We always want to inline source maps when DICompiler is used as a loader 88 | ...(input.sourceMap === true ? {inlineSourceMap: true} : {}), 89 | preserveValueImports: true 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/transformer/after/after-transformer.ts: -------------------------------------------------------------------------------- 1 | import type {BaseVisitorContext, VisitorContext} from "../visitor-context.js"; 2 | import type {TS} from "../../type/type.js"; 3 | import type {AfterVisitorOptions} from "./after-visitor-options.js"; 4 | import {visitNode} from "./visitor/visit-node.js"; 5 | import {getDefineArrayLiteralExpression, getRootBlock} from "../../util/ts-util.js"; 6 | import {ensureNodeFactory} from "compatfactory"; 7 | 8 | type SourceFileWithEmitNodes = TS.SourceFile & { 9 | emitNode?: { 10 | helpers?: TS.EmitHelper[]; 11 | }; 12 | }; 13 | 14 | export function afterTransformer(context: BaseVisitorContext): TS.TransformerFactory { 15 | return transformationContext => { 16 | const factory = ensureNodeFactory((transformationContext as {factory?: TS.NodeFactory}).factory ?? context.typescript); 17 | 18 | return sourceFile => 19 | transformSourceFile(sourceFile, { 20 | ...context, 21 | transformationContext, 22 | factory 23 | }); 24 | }; 25 | } 26 | 27 | function transformSourceFile(sourceFile: SourceFileWithEmitNodes, context: VisitorContext): TS.SourceFile { 28 | // For TypeScript versions below 3.5, there may be instances 29 | // where EmitHelpers such as __importDefault or __importStar is duplicated. 30 | // For these TypeScript versions, well have to guard against this behavior 31 | if (sourceFile.emitNode?.helpers != null) { 32 | const seenNames = new Set(); 33 | const filtered = sourceFile.emitNode.helpers.filter(helper => { 34 | if (seenNames.has(helper.name)) return false; 35 | seenNames.add(helper.name); 36 | return true; 37 | }); 38 | 39 | // Reassign the emitNodes if they changed 40 | if (filtered.length !== sourceFile.emitNode.helpers.length) { 41 | sourceFile.emitNode.helpers = filtered; 42 | } 43 | } 44 | 45 | const visitorOptions: Omit, "node" | "sourceFile"> = { 46 | context, 47 | defineArrayLiteralExpression: getDefineArrayLiteralExpression(sourceFile, context), 48 | rootBlock: getRootBlock(sourceFile, context), 49 | 50 | continuation: node => 51 | visitNode({ 52 | ...visitorOptions, 53 | sourceFile, 54 | node 55 | }), 56 | childContinuation: node => 57 | context.typescript.visitEachChild( 58 | node, 59 | cbNode => 60 | visitNode({ 61 | ...visitorOptions, 62 | sourceFile, 63 | node: cbNode 64 | }), 65 | context.transformationContext 66 | ) 67 | }; 68 | 69 | return visitorOptions.continuation(sourceFile) as TS.SourceFile; 70 | } 71 | -------------------------------------------------------------------------------- /src/transformer/after/after-visitor-options.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../type/type.js"; 2 | import type {VisitorOptions} from "../visitor-options.js"; 3 | import type {RootBlock} from "../../type/root-block.js"; 4 | 5 | export interface AfterVisitorOptions extends VisitorOptions { 6 | defineArrayLiteralExpression: TS.ArrayLiteralExpression | undefined; 7 | rootBlock: RootBlock; 8 | } 9 | -------------------------------------------------------------------------------- /src/transformer/after/visitor/visit-define-array-literal-expression.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../../type/type.js"; 2 | import type {AfterVisitorOptions} from "../after-visitor-options.js"; 3 | 4 | export function visitDefineArrayLiteralExpression(options: AfterVisitorOptions): TS.ArrayLiteralExpression { 5 | const {node, sourceFile, context} = options; 6 | const {typescript, factory} = context; 7 | 8 | const trailingExtraExpressions: TS.Expression[] = []; 9 | 10 | for (const importedSymbol of context.sourceFileToRequiredImportedSymbolSet.get(sourceFile.fileName) ?? new Set()) { 11 | // Skip the node if it is already declared as a dependency 12 | if (node.elements.some(element => typescript.isStringLiteralLike(element) && element.text === importedSymbol.moduleSpecifier)) { 13 | continue; 14 | } 15 | 16 | trailingExtraExpressions.push(factory.createStringLiteral(importedSymbol.moduleSpecifier)); 17 | } 18 | 19 | if (context.sourceFileToAddTslibDefinition.get(sourceFile.fileName) === true) { 20 | trailingExtraExpressions.push(factory.createStringLiteral("tslib")); 21 | } 22 | 23 | if (trailingExtraExpressions.length < 1) { 24 | return node; 25 | } 26 | 27 | return factory.updateArrayLiteralExpression(node, [...node.elements, ...trailingExtraExpressions]); 28 | } 29 | -------------------------------------------------------------------------------- /src/transformer/after/visitor/visit-node.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../../type/type.js"; 2 | import {visitRootBlockSourceFile} from "./visit-root-block-source-file.js"; 3 | import type {AfterVisitorOptions} from "../after-visitor-options.js"; 4 | import {visitRootBlockBlock} from "./visit-root-block-block.js"; 5 | import {visitDefineArrayLiteralExpression} from "./visit-define-array-literal-expression.js"; 6 | 7 | export function visitNode(options: AfterVisitorOptions): TS.VisitResult { 8 | const { 9 | node, 10 | childContinuation, 11 | defineArrayLiteralExpression, 12 | rootBlock, 13 | context: {typescript} 14 | } = options; 15 | if (typescript.isSourceFile(node) && rootBlock === node) { 16 | return visitRootBlockSourceFile({...options, node}); 17 | } else if (typescript.isBlock(node) && rootBlock === node) { 18 | return visitRootBlockBlock({...options, node}); 19 | } else if (typescript.isArrayLiteralExpression(node) && defineArrayLiteralExpression === node) { 20 | return visitDefineArrayLiteralExpression({ 21 | ...options, 22 | node 23 | }); 24 | } 25 | 26 | return childContinuation(options.node); 27 | } 28 | -------------------------------------------------------------------------------- /src/transformer/after/visitor/visit-root-block-block.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../../type/type.js"; 2 | import type {AfterVisitorOptions} from "../after-visitor-options.js"; 3 | import {visitRootBlock} from "./visit-root-block.js"; 4 | 5 | export function visitRootBlockBlock(options: AfterVisitorOptions): TS.VisitResult { 6 | const {node, context} = options; 7 | const {factory} = context; 8 | 9 | return factory.updateBlock(node, visitRootBlock(options)); 10 | } 11 | -------------------------------------------------------------------------------- /src/transformer/after/visitor/visit-root-block-source-file.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../../type/type.js"; 2 | import type {AfterVisitorOptions} from "../after-visitor-options.js"; 3 | import {visitRootBlock} from "./visit-root-block.js"; 4 | 5 | export function visitRootBlockSourceFile(options: AfterVisitorOptions): TS.VisitResult { 6 | const {node, context} = options; 7 | const {factory} = context; 8 | 9 | return factory.updateSourceFile( 10 | node, 11 | visitRootBlock(options), 12 | node.isDeclarationFile, 13 | node.referencedFiles, 14 | node.typeReferenceDirectives, 15 | node.hasNoDefaultLib, 16 | node.libReferenceDirectives 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/transformer/after/visitor/visit-root-block.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../../type/type.js"; 2 | import type {AfterVisitorOptions} from "../after-visitor-options.js"; 3 | import {generateImportStatementForImportedSymbolInContext, getRootBlockInsertionPosition, isImportedSymbolImported} from "../../../util/ts-util.js"; 4 | import type {RootBlock} from "../../../type/root-block.js"; 5 | 6 | export function visitRootBlock(options: AfterVisitorOptions): TS.Statement[] { 7 | const {node, sourceFile, context} = options; 8 | const {typescript} = context; 9 | 10 | const leadingExtraStatements: TS.Statement[] = []; 11 | 12 | for (const importedSymbol of context.sourceFileToRequiredImportedSymbolSet.get(sourceFile.fileName) ?? new Set()) { 13 | if (isImportedSymbolImported(importedSymbol, node, context)) continue; 14 | 15 | const missingImportStatement = generateImportStatementForImportedSymbolInContext(importedSymbol, context); 16 | 17 | if (missingImportStatement != null) { 18 | leadingExtraStatements.push(missingImportStatement); 19 | } 20 | } 21 | 22 | const insertPosition = getRootBlockInsertionPosition(node, typescript); 23 | 24 | return [...node.statements.slice(0, insertPosition), ...leadingExtraStatements, ...node.statements.slice(insertPosition)]; 25 | } 26 | -------------------------------------------------------------------------------- /src/transformer/before/before-transformer.ts: -------------------------------------------------------------------------------- 1 | import type {BaseVisitorContext, VisitorContext} from "../visitor-context.js"; 2 | import type {TS} from "../../type/type.js"; 3 | import type {BeforeVisitorOptions} from "./before-visitor-options.js"; 4 | import {visitNode} from "./visitor/visit-node.js"; 5 | import type {ImportedSymbol} from "../../type/imported-symbol.js"; 6 | import {ensureNodeFactory} from "compatfactory"; 7 | 8 | export function beforeTransformer(context: BaseVisitorContext): TS.TransformerFactory { 9 | return transformationContext => { 10 | const factory = ensureNodeFactory((transformationContext as {factory?: TS.NodeFactory}).factory ?? context.typescript); 11 | 12 | return sourceFile => 13 | transformSourceFile(sourceFile, { 14 | ...context, 15 | transformationContext, 16 | factory 17 | }); 18 | }; 19 | } 20 | 21 | export function transformSourceFile(sourceFile: TS.SourceFile, context: VisitorContext): TS.SourceFile { 22 | const requiredImportedSymbolSet = new Set(); 23 | 24 | /** 25 | * An optimization in which every imported symbol is converted into 26 | * a string that can be matched against directly to guard against 27 | * duplicates 28 | */ 29 | const requiredImportedSymbolSetFlags = new Set(); 30 | 31 | if (context.needsImportPreservationLogic) { 32 | context.sourceFileToAddTslibDefinition.set(sourceFile.fileName, false); 33 | context.sourceFileToRequiredImportedSymbolSet.set(sourceFile.fileName, requiredImportedSymbolSet); 34 | } 35 | 36 | const computeImportedSymbolFlag = (symbol: ImportedSymbol): string => 37 | ["name", "propertyName", "moduleSpecifier", "isNamespaceImport", "isDefaultImport"] 38 | .map(property => `${property}:${(symbol[property as keyof ImportedSymbol] as (typeof symbol)[keyof ImportedSymbol] | undefined) ?? false}`) 39 | .join("|"); 40 | 41 | const visitorOptions: Omit, "node" | "sourceFile"> = { 42 | context, 43 | 44 | addTslibDefinition: (): void => { 45 | if (!context.needsImportPreservationLogic) return; 46 | context.sourceFileToAddTslibDefinition.set(sourceFile.fileName, true); 47 | }, 48 | 49 | requireImportedSymbol: (importedSymbol: ImportedSymbol): void => { 50 | if (!context.needsImportPreservationLogic) return; 51 | 52 | // Guard against duplicates and compute a string so we can do 53 | // constant time lookups to compare against existing symbols 54 | const flag = computeImportedSymbolFlag(importedSymbol); 55 | if (requiredImportedSymbolSetFlags.has(flag)) return; 56 | requiredImportedSymbolSetFlags.add(flag); 57 | 58 | requiredImportedSymbolSet.add(importedSymbol); 59 | }, 60 | 61 | continuation: node => 62 | visitNode({ 63 | ...visitorOptions, 64 | sourceFile, 65 | node 66 | }), 67 | childContinuation: node => 68 | context.typescript.visitEachChild( 69 | node, 70 | cbNode => 71 | visitNode({ 72 | ...visitorOptions, 73 | sourceFile, 74 | node: cbNode 75 | }), 76 | context.transformationContext 77 | ) 78 | }; 79 | 80 | return visitorOptions.continuation(sourceFile) as TS.SourceFile; 81 | } 82 | -------------------------------------------------------------------------------- /src/transformer/before/before-visitor-options.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../type/type.js"; 2 | import type {VisitorOptions} from "../visitor-options.js"; 3 | import type {ImportedSymbol} from "../../type/imported-symbol.js"; 4 | 5 | export interface BeforeVisitorOptions extends VisitorOptions { 6 | requireImportedSymbol: (importedSymbol: ImportedSymbol) => void; 7 | addTslibDefinition: () => void; 8 | } 9 | -------------------------------------------------------------------------------- /src/transformer/before/util.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../type/type.js"; 2 | import type {VisitorContext} from "../visitor-context.js"; 3 | 4 | /** 5 | * A TypeNode such as IFoo should still yield the service name "IFoo". 6 | * This helper generates a proper service name from a TypeNode 7 | */ 8 | export function pickServiceOrImplementationName(node: TS.Expression | TS.TypeNode | TS.EntityName, context: VisitorContext): string { 9 | const {typescript} = context; 10 | 11 | if (typescript.isTypeReferenceNode(node)) { 12 | return pickServiceOrImplementationName(node.typeName, context); 13 | } else if (typescript.isIndexedAccessTypeNode(node)) { 14 | return `${pickServiceOrImplementationName(node.objectType, context)}[${pickServiceOrImplementationName(node.indexType, context)}]`; 15 | } else { 16 | return node.getText().trim(); 17 | } 18 | } 19 | 20 | export function getModifierLikes(node: TS.Node): readonly TS.ModifierLike[] | undefined { 21 | const modifiers = "modifiers" in node && Array.isArray(node.modifiers) ? node.modifiers : []; 22 | if ("decorators" in node && Array.isArray(node.decorators)) { 23 | return [...(node as {decorators: TS.Decorator[]}).decorators, ...(modifiers as TS.Modifier[])]; 24 | } else { 25 | return modifiers as TS.ModifierLike[]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/transformer/before/visitor/visit-call-expression.ts: -------------------------------------------------------------------------------- 1 | import {DI_CONTAINER_NAME} from "../../constant.js"; 2 | import type {TS} from "../../../type/type.js"; 3 | import type {BeforeVisitorOptions} from "../before-visitor-options.js"; 4 | import type {DiMethodName} from "../../di-method-kind.js"; 5 | import type {VisitorContext} from "../../visitor-context.js"; 6 | import {getImportDefaultHelper, getImportStarHelper, moduleKindDefinesDependencies, moduleKindSupportsImportHelpers} from "../../../util/ts-util.js"; 7 | import {pickServiceOrImplementationName} from "../util.js"; 8 | import {ensureArray} from "../../../util/util.js"; 9 | 10 | export function visitCallExpression(options: BeforeVisitorOptions): TS.VisitResult { 11 | const {node, childContinuation, continuation, context, addTslibDefinition, requireImportedSymbol} = options; 12 | const {typescript, factory, compilerOptions, transformationContext, needsImportPreservationLogic} = context; 13 | 14 | const diMethod = getDiMethodName(node.expression, context); 15 | 16 | if (diMethod != null) { 17 | switch (diMethod) { 18 | case "get": 19 | case "has": { 20 | // If no type arguments are given, don't modify the node at all 21 | if (node.typeArguments?.[0] == null) { 22 | return childContinuation(node); 23 | } 24 | 25 | const [firstTypeArgument] = node.typeArguments; 26 | 27 | return factory.updateCallExpression(node, node.expression, node.typeArguments, [ 28 | factory.createObjectLiteralExpression([ 29 | factory.createPropertyAssignment("identifier", factory.createStringLiteral((firstTypeArgument.getFirstToken()?.getText() ?? firstTypeArgument.getText()).trim())) 30 | ]) 31 | ]); 32 | } 33 | 34 | case "registerSingleton": 35 | case "registerTransient": { 36 | const [typeArg, secondTypeArg] = (node.typeArguments ?? []) as unknown as [TS.TypeNode | undefined, TS.TypeNode | TS.Expression | undefined]; 37 | const [firstArgument] = (node as {arguments?: TS.NodeArray}).arguments ?? []; 38 | 39 | // The user may explicitly pass 'undefined' as a value here, which shouldn't count as a custom implementation 40 | const customImplementation = firstArgument == null || (typescript.isIdentifier(firstArgument) && firstArgument.text === "undefined") ? undefined : firstArgument; 41 | 42 | const implementationArg = 43 | // If another implementation is passed, used that one instead 44 | customImplementation ?? 45 | // If not implementation is provided, use the type argument *as* the implementation 46 | secondTypeArg ?? 47 | typeArg; 48 | 49 | if (typeArg == null || implementationArg == null) { 50 | return childContinuation(node); 51 | } 52 | 53 | const typeArgText = pickServiceOrImplementationName(typeArg, context); 54 | const implementationArgText = pickServiceOrImplementationName(implementationArg, context); 55 | 56 | // If the Implementation is a TypeNode, and if it originates from an ImportDeclaration, it may be stripped from the file since Typescript won't Type-check the updates from 57 | // a CustomTransformer and such a node would normally be removed from the imports. 58 | // to fix it, add an ImportDeclaration if needed. This is only needed if `preserveValueImports` is falsy 59 | if (needsImportPreservationLogic && customImplementation == null) { 60 | const matchingImport = findMatchingImportDeclarationForIdentifier(implementationArgText, options); 61 | if (matchingImport != null && typescript.isStringLiteralLike(matchingImport.importDeclaration.moduleSpecifier)) { 62 | switch (matchingImport.kind) { 63 | case "default": { 64 | // Log a request for the __importDefault helper already if we will 65 | // need it in a later transformation step 66 | if (moduleKindSupportsImportHelpers(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers !== true) { 67 | transformationContext.requestEmitHelper(getImportDefaultHelper(typescript)); 68 | } 69 | 70 | // Log a request for adding 'tslib' to the define([...]) array for the current 71 | // module system if it relies on declaring dependencies (such as UMD, AMD, and SystemJS does) 72 | if (moduleKindDefinesDependencies(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers === true) { 73 | addTslibDefinition(); 74 | } 75 | 76 | requireImportedSymbol({ 77 | isDefaultImport: true, 78 | moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, 79 | name: matchingImport.identifier.text, 80 | propertyName: matchingImport.identifier.text 81 | }); 82 | break; 83 | } 84 | 85 | case "namedImport": { 86 | requireImportedSymbol({ 87 | isDefaultImport: false, 88 | moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, 89 | name: matchingImport.importSpecifier.name.text, 90 | propertyName: matchingImport.importSpecifier.propertyName?.text ?? matchingImport.importSpecifier.name.text 91 | }); 92 | break; 93 | } 94 | 95 | case "namespace": { 96 | // Log a request for the __importStar helper already if you will 97 | // need it in a later transformation step 98 | if (moduleKindSupportsImportHelpers(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers !== true) { 99 | transformationContext.requestEmitHelper(getImportStarHelper(typescript)); 100 | } 101 | 102 | requireImportedSymbol({ 103 | isNamespaceImport: true, 104 | moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, 105 | name: matchingImport.identifier.text 106 | }); 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | 113 | return factory.updateCallExpression(node, node.expression, node.typeArguments, [ 114 | customImplementation == null ? factory.createIdentifier("undefined") : (continuation(implementationArg) as TS.Expression), 115 | factory.createObjectLiteralExpression([ 116 | factory.createPropertyAssignment("identifier", factory.createNoSubstitutionTemplateLiteral(typeArgText)), 117 | ...(customImplementation != null 118 | ? [] 119 | : [factory.createPropertyAssignment("implementation", factory.createIdentifier(rewriteImplementationName(implementationArgText, options)))]) 120 | ]) 121 | ]); 122 | } 123 | } 124 | } 125 | 126 | return childContinuation(node); 127 | } 128 | 129 | interface FindMatchingImportDeclarationForIdentifierBaseResult { 130 | kind: "default" | "namespace" | "namedImport"; 131 | importDeclaration: TS.ImportDeclaration; 132 | } 133 | 134 | interface FindMatchingImportDeclarationForIdentifierNamedImportResult extends FindMatchingImportDeclarationForIdentifierBaseResult { 135 | kind: "namedImport"; 136 | importSpecifier: TS.ImportSpecifier; 137 | } 138 | 139 | interface FindMatchingImportDeclarationForIdentifierDefaultResult extends FindMatchingImportDeclarationForIdentifierBaseResult { 140 | kind: "default"; 141 | identifier: TS.Identifier; 142 | } 143 | 144 | interface FindMatchingImportDeclarationForIdentifierNamespaceResult extends FindMatchingImportDeclarationForIdentifierBaseResult { 145 | kind: "namespace"; 146 | identifier: TS.Identifier; 147 | } 148 | 149 | type FindMatchingImportDeclarationForIdentifierResult = 150 | | FindMatchingImportDeclarationForIdentifierNamedImportResult 151 | | FindMatchingImportDeclarationForIdentifierDefaultResult 152 | | FindMatchingImportDeclarationForIdentifierNamespaceResult; 153 | 154 | function findMatchingImportDeclarationForIdentifier( 155 | identifier: string, 156 | options: BeforeVisitorOptions 157 | ): FindMatchingImportDeclarationForIdentifierResult | undefined { 158 | const { 159 | sourceFile, 160 | context: {typescript} 161 | } = options; 162 | 163 | // Find the matching import 164 | const importDeclarations = sourceFile.statements.filter(typescript.isImportDeclaration); 165 | 166 | for (const importDeclaration of importDeclarations) { 167 | if (importDeclaration.importClause == null) continue; 168 | 169 | // Default import 170 | if (importDeclaration.importClause.name?.text === identifier) { 171 | return { 172 | importDeclaration, 173 | kind: "default", 174 | identifier: importDeclaration.importClause.name 175 | }; 176 | } else if (importDeclaration.importClause.namedBindings != null) { 177 | if (typescript.isNamespaceImport(importDeclaration.importClause.namedBindings)) { 178 | if (importDeclaration.importClause.namedBindings.name.text === identifier) { 179 | return { 180 | importDeclaration, 181 | kind: "namespace", 182 | identifier: importDeclaration.importClause.namedBindings.name 183 | }; 184 | } 185 | } else { 186 | for (const importSpecifier of importDeclaration.importClause.namedBindings.elements) { 187 | if (importSpecifier.name.text === identifier) { 188 | return { 189 | importDeclaration, 190 | kind: "namedImport", 191 | importSpecifier: importSpecifier 192 | }; 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | // No import was matched 200 | return undefined; 201 | } 202 | 203 | function rewriteImplementationName(name: string, options: BeforeVisitorOptions): string { 204 | const { 205 | context: {typescript, compilerOptions} 206 | } = options; 207 | 208 | switch (compilerOptions.module) { 209 | case typescript.ModuleKind.ES2022: 210 | case typescript.ModuleKind.ES2020: 211 | case typescript.ModuleKind.ES2015: 212 | case typescript.ModuleKind.ESNext: 213 | return name; 214 | 215 | case typescript.ModuleKind.CommonJS: 216 | case typescript.ModuleKind.AMD: 217 | case typescript.ModuleKind.UMD: { 218 | // Find the matching import 219 | const match = findMatchingImportDeclarationForIdentifier(name, options); 220 | if (match == null) { 221 | return name; 222 | } 223 | 224 | switch (match.kind) { 225 | case "default": 226 | return `${name}.default`; 227 | case "namespace": 228 | return name; 229 | case "namedImport": 230 | return `${name}.${(match.importSpecifier.propertyName ?? match.importSpecifier.name).text}`; 231 | } 232 | 233 | // Fall back to returning the original name 234 | return name; 235 | } 236 | 237 | default: 238 | // TODO: Add support for SystemJS here 239 | return name; 240 | } 241 | } 242 | 243 | function getDiMethodName(node: TS.Expression, context: VisitorContext): DiMethodName | undefined { 244 | if (!context.typescript.isPropertyAccessExpression(node) && !context.typescript.isElementAccessExpression(node)) { 245 | return undefined; 246 | } 247 | 248 | // If it is an element access expression, evaluate the argument expression 249 | if (context.typescript.isElementAccessExpression(node)) { 250 | // Do nothing at this point if this isn't a DIContainer instance, as we can avoid invoking evaluate at this point 251 | if (!isDiContainerInstance(node, context)) { 252 | return undefined; 253 | } 254 | 255 | const evaluationResult = context.evaluate(node.argumentExpression); 256 | 257 | // If no value could be computed, or if the value isn't of type string, do nothing 258 | if (!evaluationResult.success || typeof evaluationResult.value !== "string") { 259 | return undefined; 260 | } else { 261 | return isDiContainerMethodName(evaluationResult.value) ? evaluationResult.value : undefined; 262 | } 263 | } else { 264 | // If the name is any of the relevant ones, assert that it is invoked on an instance of DIContainer 265 | return isDiContainerMethodName(node.name.text) && isDiContainerInstance(node, context) ? node.name.text : undefined; 266 | } 267 | } 268 | 269 | function isDiContainerMethodName(name: string): name is DiMethodName { 270 | switch (name) { 271 | case "get": 272 | case "has": 273 | case "registerSingleton": 274 | case "registerTransient": 275 | return true; 276 | default: 277 | return false; 278 | } 279 | } 280 | 281 | function isDiContainerInstance(node: TS.PropertyAccessExpression | TS.ElementAccessExpression, context: VisitorContext): boolean { 282 | if ("typeChecker" in context) { 283 | // Don't proceed unless the left-hand expression is the DIServiceContainer 284 | const type = context.typeChecker.getTypeAtLocation(node.expression); 285 | 286 | // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition 287 | if (type?.symbol == null || type.symbol.escapedName !== DI_CONTAINER_NAME) { 288 | return false; 289 | } 290 | } else { 291 | // If one or more variable names were passed in, check those directly 292 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 293 | if (context.identifier != null && context.identifier.length > 0) { 294 | // Pick the left-hand side of the expression here 295 | const name = (node.expression.getFirstToken()?.getText() ?? node.expression.getText()).trim(); 296 | // If not a single matcher matches the text, this does not represent an instance of DIContainer. 297 | if (!ensureArray(context.identifier).some(matcher => name === matcher)) { 298 | return false; 299 | } 300 | } else { 301 | // Otherwise, attempt to resolve the value of the expression and check if it is an instance of DIContainer 302 | const evaluationResult = context.evaluate(node.expression); 303 | 304 | if ( 305 | !evaluationResult.success || 306 | evaluationResult.value == null || 307 | typeof evaluationResult.value !== "object" || 308 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 309 | evaluationResult.value.constructor?.name !== DI_CONTAINER_NAME 310 | ) { 311 | return false; 312 | } 313 | } 314 | } 315 | return true; 316 | } 317 | -------------------------------------------------------------------------------- /src/transformer/before/visitor/visit-class-like-declaration.ts: -------------------------------------------------------------------------------- 1 | import {CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER} from "../../constant.js"; 2 | import type {TS} from "../../../type/type.js"; 3 | import type {BeforeVisitorOptions} from "../before-visitor-options.js"; 4 | import type {VisitorContext} from "../../visitor-context.js"; 5 | import {getModifierLikes, pickServiceOrImplementationName} from "../util.js"; 6 | 7 | export function visitClassLikeDeclaration(options: BeforeVisitorOptions): TS.VisitResult { 8 | const {node, childContinuation, continuation, context} = options; 9 | const {typescript, factory} = context; 10 | const constructorDeclaration = node.members.find(typescript.isConstructorDeclaration); 11 | 12 | // If there are no constructor declaration for the ClassLikeDeclaration, there's nothing to do 13 | if (constructorDeclaration == null) { 14 | return childContinuation(node); 15 | } 16 | 17 | const updatedClassMembers: readonly TS.ClassElement[] = [ 18 | ...(node.members.map(continuation) as TS.ClassElement[]), 19 | factory.createGetAccessorDeclaration( 20 | [factory.createModifier(typescript.SyntaxKind.StaticKeyword)], 21 | factory.createComputedPropertyName(factory.createIdentifier(`Symbol.for("${CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER}")`)), 22 | [], 23 | undefined, 24 | factory.createBlock([factory.createReturnStatement(getParameterTypeNamesAsArrayLiteral(constructorDeclaration.parameters, context))]) 25 | ) 26 | ]; 27 | 28 | const modifierLikes = getModifierLikes(node); 29 | 30 | if (typescript.isClassDeclaration(node)) { 31 | return factory.updateClassDeclaration(node, modifierLikes, node.name, node.typeParameters, node.heritageClauses, updatedClassMembers); 32 | } else { 33 | return factory.updateClassExpression(node, modifierLikes, node.name, node.typeParameters, node.heritageClauses, updatedClassMembers); 34 | } 35 | } 36 | 37 | /** 38 | * Takes ConstructorParams for the given NodeArray of ParameterDeclarations 39 | */ 40 | function getParameterTypeNamesAsArrayLiteral(parameters: TS.NodeArray, context: VisitorContext): TS.ArrayLiteralExpression { 41 | const {factory} = context; 42 | const constructorParams: TS.Expression[] = []; 43 | 44 | for (let i = 0; i < parameters.length; i++) { 45 | const parameter = parameters[i]!; 46 | // If the parameter has no type, there's nothing to extract 47 | if (parameter.type == null) { 48 | constructorParams[i] = factory.createIdentifier("undefined"); 49 | } else { 50 | constructorParams[i] = factory.createNoSubstitutionTemplateLiteral(pickServiceOrImplementationName(parameter.type, context)); 51 | } 52 | } 53 | 54 | return factory.createArrayLiteralExpression(constructorParams); 55 | } 56 | -------------------------------------------------------------------------------- /src/transformer/before/visitor/visit-node.ts: -------------------------------------------------------------------------------- 1 | import type {BeforeVisitorOptions} from "../before-visitor-options.js"; 2 | import type {TS} from "../../../type/type.js"; 3 | import {visitClassLikeDeclaration} from "./visit-class-like-declaration.js"; 4 | import {visitCallExpression} from "./visit-call-expression.js"; 5 | 6 | export function visitNode(options: BeforeVisitorOptions): TS.VisitResult { 7 | if (options.context.typescript.isClassLike(options.node)) { 8 | return visitClassLikeDeclaration({...options, node: options.node}); 9 | } else if (options.context.typescript.isCallExpression(options.node)) { 10 | return visitCallExpression({...options, node: options.node}); 11 | } 12 | 13 | return options.childContinuation(options.node); 14 | } 15 | -------------------------------------------------------------------------------- /src/transformer/cache.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import os from "os"; 4 | import {NOOP} from "../util/util.js"; 5 | 6 | /** 7 | * This implementation is very closely inspired by that found in https://github.com/esbuild-kit/core-utils. 8 | */ 9 | 10 | const DEFAULT_TTL_DAYS = 7; 11 | const DEFAULT_TTL = 60 * 60 * 24 * DEFAULT_TTL_DAYS * 1000; 12 | 13 | interface FileCacheEntry { 14 | time: number; 15 | key: string; 16 | fileName: string; 17 | } 18 | 19 | interface FileCacheOptions { 20 | cacheName: string; 21 | ttl: number; 22 | } 23 | 24 | export class FileCache extends Map { 25 | private readonly cacheFiles: FileCacheEntry[] = []; 26 | private readonly options: FileCacheOptions; 27 | 28 | constructor({cacheName = "di-compiler", ttl = DEFAULT_TTL}: Partial = {}) { 29 | super(); 30 | 31 | this.options = {cacheName, ttl}; 32 | 33 | // Initialize the disk cache 34 | fs.mkdirSync(this.cacheDirectory, {recursive: true}); 35 | this.cacheFiles = fs.readdirSync(this.cacheDirectory).map(fileName => { 36 | const [time, key] = fileName.split("-") as [string, string]; 37 | return { 38 | time: Number(time), 39 | key, 40 | fileName 41 | }; 42 | }); 43 | 44 | setImmediate(() => this.expireDiskCache()); 45 | } 46 | 47 | private get cacheDirectory() { 48 | return path.join(os.tmpdir(), this.options.cacheName); 49 | } 50 | 51 | private readTransformResult(filePath: string): T | undefined { 52 | try { 53 | const jsonString = fs.readFileSync(filePath, "utf8"); 54 | return JSON.parse(jsonString) as T; 55 | } catch { 56 | return undefined; 57 | } 58 | } 59 | 60 | get(key: string) { 61 | const memoryCacheHit = super.get(key); 62 | 63 | if (memoryCacheHit != null) { 64 | return memoryCacheHit; 65 | } 66 | 67 | const diskCacheHit = this.cacheFiles.find(cache => cache.key === key); 68 | if (diskCacheHit == null) { 69 | return; 70 | } 71 | 72 | const cacheFilePath = path.join(this.cacheDirectory, diskCacheHit.fileName); 73 | const cachedResult = this.readTransformResult(cacheFilePath); 74 | 75 | if (cachedResult == null) { 76 | // Remove broken cache file 77 | fs.promises.unlink(cacheFilePath).then(() => { 78 | const index = this.cacheFiles.indexOf(diskCacheHit); 79 | this.cacheFiles.splice(index, 1); 80 | }, NOOP); 81 | return; 82 | } 83 | 84 | // Load it into memory 85 | super.set(key, cachedResult); 86 | 87 | return cachedResult; 88 | } 89 | 90 | set(key: string, value: T) { 91 | super.set(key, value); 92 | 93 | if (value != null) { 94 | const time = Date.now(); 95 | 96 | fs.promises.writeFile(path.join(this.cacheDirectory, `${time}-${key}`), JSON.stringify(value)).catch(NOOP); 97 | } 98 | 99 | return this; 100 | } 101 | 102 | expireDiskCache() { 103 | const time = Date.now(); 104 | 105 | for (const cache of this.cacheFiles) { 106 | // Remove if older than ~7 days 107 | if (time - cache.time > this.options.ttl) { 108 | fs.promises.unlink(path.join(this.cacheDirectory, cache.fileName)).catch(NOOP); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/transformer/constant.ts: -------------------------------------------------------------------------------- 1 | export const CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER = `___CTOR_ARGS___`; 2 | export const DI_CONTAINER_NAME = "DIContainer"; 3 | -------------------------------------------------------------------------------- /src/transformer/di-method-kind.ts: -------------------------------------------------------------------------------- 1 | export type DiMethodName = "get" | "has" | "registerSingleton" | "registerTransient"; 2 | -------------------------------------------------------------------------------- /src/transformer/di-options.ts: -------------------------------------------------------------------------------- 1 | import type {MaybeArray} from "helpertypes"; 2 | import type {TS} from "../type/type.js"; 3 | interface DiOptionsBase { 4 | typescript?: typeof TS; 5 | } 6 | 7 | export interface DiProgramOptions extends DiOptionsBase { 8 | program: TS.Program; 9 | } 10 | 11 | export interface DiIsolatedModulesOptions extends DiOptionsBase { 12 | /** 13 | * The identifier(s) that should be considered instances of DIContainer. When not given, an attempt will be 14 | * made to evaluate and resolve the value of identifiers to check if they are instances of DIContainer. 15 | * Providing one or more identifiers up front can be considered an optimization, as this step can be skipped that way 16 | */ 17 | identifier?: MaybeArray; 18 | compilerOptions?: TS.CompilerOptions; 19 | } 20 | 21 | export type DiOptions = DiProgramOptions | DiIsolatedModulesOptions; 22 | -------------------------------------------------------------------------------- /src/transformer/di.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../type/type.js"; 2 | import type {DiOptions} from "./di-options.js"; 3 | import {beforeTransformer} from "./before/before-transformer.js"; 4 | import {afterTransformer} from "./after/after-transformer.js"; 5 | import {getBaseVisitorContext} from "./get-base-visitor-context.js"; 6 | 7 | /** 8 | * CustomTransformer that associates constructor arguments with any given class declaration 9 | */ 10 | export function di(options: DiOptions): TS.CustomTransformers { 11 | const baseVisitorContext = getBaseVisitorContext(options); 12 | 13 | return { 14 | before: [beforeTransformer(baseVisitorContext)], 15 | after: baseVisitorContext.needsImportPreservationLogic ? [afterTransformer(baseVisitorContext)] : [] 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/transformer/get-base-visitor-context.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import type {DiOptions} from "./di-options.js"; 3 | import type {BaseVisitorContext} from "./visitor-context.js"; 4 | import {evaluate} from "ts-evaluator"; 5 | import TSModule from "typescript"; 6 | import {DI_CONTAINER_NAME} from "./constant.js"; 7 | import {needsImportPreservationLogic} from "../util/ts-util.js"; 8 | import type {TS} from "../type/type.js"; 9 | 10 | /** 11 | * Shim the @wessberg/di module 12 | */ 13 | const EVALUATE_MODULE_OVERRIDES = { 14 | "@wessberg/di": { 15 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 16 | [DI_CONTAINER_NAME]: class {} 17 | } 18 | }; 19 | 20 | export function getBaseVisitorContext({typescript = TSModule as typeof TS, ...rest}: DiOptions = {}): BaseVisitorContext { 21 | // Prepare a VisitorContext 22 | const visitorContextShared = { 23 | sourceFileToAddTslibDefinition: new Map(), 24 | sourceFileToRequiredImportedSymbolSet: new Map() 25 | }; 26 | 27 | if ("program" in rest) { 28 | const typeChecker = rest.program.getTypeChecker(); 29 | const compilerOptions = rest.program.getCompilerOptions(); 30 | return { 31 | ...rest, 32 | ...visitorContextShared, 33 | needsImportPreservationLogic: needsImportPreservationLogic(typescript, compilerOptions), 34 | typescript, 35 | typeChecker, 36 | compilerOptions, 37 | 38 | evaluate: node => 39 | evaluate({ 40 | node, 41 | typeChecker, 42 | typescript, 43 | moduleOverrides: EVALUATE_MODULE_OVERRIDES 44 | }) 45 | }; 46 | } else { 47 | const compilerOptions = rest.compilerOptions ?? typescript.getDefaultCompilerOptions(); 48 | return { 49 | identifier: [], 50 | 51 | ...rest, 52 | ...visitorContextShared, 53 | needsImportPreservationLogic: needsImportPreservationLogic(typescript, compilerOptions), 54 | typescript, 55 | compilerOptions, 56 | 57 | evaluate: node => 58 | evaluate({ 59 | node, 60 | typescript, 61 | moduleOverrides: EVALUATE_MODULE_OVERRIDES 62 | }) 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/transformer/transform-options.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../type/type.js"; 2 | import type {DiOptions} from "./di-options.js"; 3 | 4 | export interface TransformResult { 5 | code: string; 6 | map?: string; 7 | } 8 | 9 | export type TransformOptions = DiOptions & { 10 | printer?: TS.Printer; 11 | cache?: Map; 12 | }; 13 | -------------------------------------------------------------------------------- /src/transformer/transform.ts: -------------------------------------------------------------------------------- 1 | import path from "crosspath"; 2 | import {getBaseVisitorContext} from "./get-base-visitor-context.js"; 3 | import type {TransformOptions, TransformResult} from "./transform-options.js"; 4 | import {ensureArray, sha1} from "../util/util.js"; 5 | import type {BaseVisitorContext} from "./visitor-context.js"; 6 | import type {TSEmitHost, TSExtended, TSExtendedPrinter, TSSourceMapGenerator, TSSourceMapGeneratorOptions} from "../type/type.js"; 7 | import {ensureNodeFactory} from "compatfactory"; 8 | import {transformSourceFile} from "./before/before-transformer.js"; 9 | 10 | /** 11 | * CustomTransformer that associates constructor arguments with any given class declaration 12 | */ 13 | export function transform(source: string, options?: TransformOptions): TransformResult; 14 | export function transform(source: string, filename: string, options?: TransformOptions): TransformResult; 15 | export function transform(source: string, filenameOrOptions: string | TransformOptions | undefined, optionsOrUndefined?: TransformOptions): TransformResult { 16 | const filename = typeof filenameOrOptions === "string" ? filenameOrOptions : "file.ts"; 17 | const options = typeof filenameOrOptions === "string" ? optionsOrUndefined : filenameOrOptions; 18 | 19 | const baseVisitorContext = getBaseVisitorContext(options); 20 | 21 | // By preserving value imports, we can avoid the `after` transformer entirely, 22 | // as well as adding/tracking imports,since nothing will be stripped away. 23 | // eslint-disable-next-line @typescript-eslint/no-deprecated 24 | baseVisitorContext.compilerOptions.preserveValueImports = true; 25 | 26 | const {compilerOptions} = baseVisitorContext; 27 | const typescript = baseVisitorContext.typescript as TSExtended; 28 | 29 | const hash = generateCacheKey(source, baseVisitorContext, options); 30 | const cacheHit = hash == null ? undefined : options?.cache?.get(hash); 31 | 32 | if (cacheHit != null) { 33 | return cacheHit; 34 | } 35 | 36 | const newLine = typescript.sys.newLine; 37 | const printer = (options?.printer ?? typescript.createPrinter()) as TSExtendedPrinter; 38 | const factory = ensureNodeFactory(typescript); 39 | 40 | // An undocumented internal helper can be leveraged here 41 | const transformationContext = typescript.nullTransformationContext; 42 | const visitorContext = {...baseVisitorContext, transformationContext, factory}; 43 | 44 | const sourceFile = typescript.createSourceFile(filename, source, typescript.ScriptTarget.ESNext, true); 45 | const transformedSourceFile = transformSourceFile(sourceFile, visitorContext); 46 | 47 | let result: TransformResult; 48 | 49 | if (Boolean(compilerOptions.sourceMap)) { 50 | const sourceMapOptions: TSSourceMapGeneratorOptions = { 51 | sourceMap: Boolean(compilerOptions.sourceMap), 52 | sourceRoot: "", 53 | mapRoot: "", 54 | extendedDiagnostics: false 55 | }; 56 | 57 | const emitHost: TSEmitHost = { 58 | getCanonicalFileName: typescript.createGetCanonicalFileName(typescript.sys.useCaseSensitiveFileNames), 59 | getCompilerOptions: () => compilerOptions, 60 | getCurrentDirectory: () => path.dirname(filename) 61 | }; 62 | 63 | const sourceMapGenerator = typescript.createSourceMapGenerator(emitHost, path.basename(filename), sourceMapOptions.sourceRoot, path.dirname(filename), sourceMapOptions); 64 | const writer = typescript.createTextWriter(newLine); 65 | printer.writeFile(transformedSourceFile, writer, sourceMapGenerator); 66 | 67 | const sourceMappingUrl = getSourceMappingUrl(sourceMapGenerator, filename, Boolean(compilerOptions.inlineSourceMap)); 68 | if (sourceMappingUrl.length > 0) { 69 | if (!writer.isAtStartOfLine()) writer.rawWrite(newLine); 70 | writer.writeComment("//# ".concat("sourceMappingURL", "=").concat(sourceMappingUrl)); // Tools can sometimes see this line as a source mapping url comment 71 | } 72 | 73 | result = { 74 | code: writer.getText(), 75 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 76 | map: Boolean(compilerOptions.inlineSourceMap) ? undefined : sourceMapGenerator.toString() 77 | }; 78 | } else { 79 | result = { 80 | code: printer.printFile(transformedSourceFile) 81 | }; 82 | } 83 | 84 | if (hash != null && options?.cache != null) { 85 | options.cache.set(hash, result); 86 | } 87 | 88 | return result; 89 | } 90 | 91 | function generateCacheKey(source: string, context: BaseVisitorContext, options: TransformOptions | undefined): string | undefined { 92 | // No point in calculating a hash if there's no cache in use 93 | if (options?.cache == null) return undefined; 94 | 95 | const identifier = "identifier" in options ? options.identifier : undefined; 96 | let key = source; 97 | if (identifier != null) { 98 | key += ensureArray(identifier).join(","); 99 | } 100 | 101 | key += String(Boolean(context.compilerOptions.sourceMap)); 102 | return sha1(key); 103 | } 104 | 105 | function getSourceMappingUrl(sourceMapGenerator: TSSourceMapGenerator, filePath: string, inline: boolean) { 106 | if (inline) { 107 | // Encode the sourceMap into the sourceMap url 108 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 109 | const sourceMapText = sourceMapGenerator.toString(); 110 | const base64SourceMapText = Buffer.from(sourceMapText).toString("base64"); 111 | return "data:application/json;base64,".concat(base64SourceMapText); 112 | } 113 | const sourceMapFilePath = `${filePath}.map`; 114 | const sourceMapFile = path.basename(sourceMapFilePath); 115 | 116 | return encodeURI(sourceMapFile); 117 | } 118 | -------------------------------------------------------------------------------- /src/transformer/visitor-context.ts: -------------------------------------------------------------------------------- 1 | import type {DiProgramOptions, DiIsolatedModulesOptions} from "./di-options.js"; 2 | import type {TS} from "../type/type.js"; 3 | import type {EvaluateResult} from "ts-evaluator"; 4 | import type {SourceFileToImportedSymbolSet} from "../type/imported-symbol.js"; 5 | 6 | export interface BaseVisitorContextShared { 7 | compilerOptions: TS.CompilerOptions; 8 | evaluate(node: TS.Declaration | TS.Expression | TS.Statement): EvaluateResult; 9 | needsImportPreservationLogic: boolean; 10 | 11 | // Some files need to add 'tslib' to their 'define' arrays 12 | sourceFileToAddTslibDefinition: Map; 13 | 14 | // We might need to add in additional ImportDeclarations for 15 | // things like type-only implementation arguments, but we'll need to add 16 | // those in an after-transformer, since we will need to check if another import 17 | // already exists for that binding after transpilation 18 | sourceFileToRequiredImportedSymbolSet: SourceFileToImportedSymbolSet; 19 | } 20 | 21 | interface BaseVisitorContextProgram extends BaseVisitorContextShared, Required { 22 | typeChecker: TS.TypeChecker; 23 | } 24 | 25 | interface BaseVisitorContextIsolatedModules extends BaseVisitorContextShared, Required {} 26 | 27 | export type BaseVisitorContext = BaseVisitorContextProgram | BaseVisitorContextIsolatedModules; 28 | 29 | interface VisitorContextShared { 30 | factory: TS.NodeFactory; 31 | transformationContext: TS.TransformationContext; 32 | } 33 | 34 | interface VisitorContextProgram extends BaseVisitorContextProgram, VisitorContextShared {} 35 | 36 | interface VisitorContextIsolatedModules extends BaseVisitorContextIsolatedModules, VisitorContextShared {} 37 | 38 | export type VisitorContext = VisitorContextProgram | VisitorContextIsolatedModules; 39 | -------------------------------------------------------------------------------- /src/transformer/visitor-continuation.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../type/type.js"; 2 | 3 | export type VisitorContinuation = (node: T) => TS.VisitResult; 4 | -------------------------------------------------------------------------------- /src/transformer/visitor-options.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../type/type.js"; 2 | import type {VisitorContext} from "./visitor-context.js"; 3 | import type {VisitorContinuation} from "./visitor-continuation.js"; 4 | 5 | export interface VisitorOptions { 6 | node: T; 7 | sourceFile: TS.SourceFile; 8 | context: VisitorContext; 9 | continuation: VisitorContinuation; 10 | childContinuation: VisitorContinuation; 11 | } 12 | -------------------------------------------------------------------------------- /src/type/imported-symbol.ts: -------------------------------------------------------------------------------- 1 | export interface ImportedSymbolBase { 2 | moduleSpecifier: string; 3 | } 4 | 5 | export interface NamedImportedSymbol extends ImportedSymbolBase { 6 | isDefaultImport: boolean; 7 | name: string; 8 | propertyName: string; 9 | } 10 | 11 | export interface NamespaceImportedSymbol extends ImportedSymbolBase { 12 | isNamespaceImport: true; 13 | name: string; 14 | } 15 | 16 | export type ImportedSymbol = NamedImportedSymbol | NamespaceImportedSymbol; 17 | 18 | /** 19 | * A Set of imported symbols and data about them 20 | */ 21 | export type ImportedSymbolSet = Set; 22 | 23 | /** 24 | * A Map between source files and their ImportedSymbolSets 25 | */ 26 | export type SourceFileToImportedSymbolSet = Map; 27 | -------------------------------------------------------------------------------- /src/type/root-block.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "./type.js"; 2 | 3 | export type RootBlock = TS.Node & { 4 | statements: TS.NodeArray; 5 | }; 6 | -------------------------------------------------------------------------------- /src/type/type.ts: -------------------------------------------------------------------------------- 1 | import * as TS from "typescript"; 2 | export {TS}; 3 | 4 | export interface TSTextWriter { 5 | rawWrite(text: string): void; 6 | isAtStartOfLine(): boolean; 7 | getText(): string; 8 | writeComment(comment: string): void; 9 | } 10 | 11 | export interface TSSourceMapGenerator {} 12 | 13 | export interface TSSourceMapGeneratorOptions { 14 | sourceMap: boolean; 15 | sourceRoot: string; 16 | mapRoot: string; 17 | extendedDiagnostics: boolean; 18 | } 19 | 20 | export interface TSEmitHost { 21 | getCanonicalFileName(input: string): string; 22 | getCompilerOptions(): TS.CompilerOptions; 23 | getCurrentDirectory(): string; 24 | } 25 | 26 | export type TSExtended = typeof TS & { 27 | nullTransformationContext: TS.TransformationContext; 28 | createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): (input: string) => string; 29 | createSourceMapGenerator(emitHost: TSEmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, mapOptions: TSSourceMapGeneratorOptions): TSSourceMapGenerator; 30 | createTextWriter(newLine: string): TSTextWriter; 31 | }; 32 | 33 | export type TSExtendedPrinter = TS.Printer & { 34 | writeFile(file: TS.SourceFile, writer: TSTextWriter, sourceMapGenerator: TSSourceMapGenerator): void; 35 | }; 36 | -------------------------------------------------------------------------------- /src/util/ts-util.ts: -------------------------------------------------------------------------------- 1 | import {TS} from "../type/type.js"; 2 | import type {ImportedSymbol} from "../type/imported-symbol.js"; 3 | import type {BaseVisitorContext, VisitorContext} from "../transformer/visitor-context.js"; 4 | import type {RootBlock} from "../type/root-block.js"; 5 | 6 | type TSWithHelpers = typeof TS & { 7 | importDefaultHelper?: TS.EmitHelper; 8 | importStarHelper?: TS.EmitHelper; 9 | }; 10 | 11 | // For some TypeScript versions, such as 3.1, these helpers are not exposed by TypeScript, 12 | // so they will have to be duplicated and reused from here in these rare cases 13 | const HELPERS = { 14 | importDefaultHelper: { 15 | name: "typescript:commonjsimportdefault", 16 | scoped: false, 17 | text: '\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { "default": mod };\n};' 18 | }, 19 | importStarHelper: { 20 | name: "typescript:commonjsimportstar", 21 | scoped: false, 22 | text: '\nvar __importStar = (this && this.__importStar) || function (mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n result["default"] = mod;\n return result;\n};' 23 | } 24 | } as const; 25 | 26 | export function needsImportPreservationLogic(context: Pick): boolean; 27 | export function needsImportPreservationLogic(typescript: typeof TS, compilerOptions: TS.CompilerOptions): boolean; 28 | export function needsImportPreservationLogic( 29 | typescriptOrContext: Pick | typeof TS, 30 | compilerOptionsOrUndefined?: TS.CompilerOptions 31 | ): boolean { 32 | const typescript = arguments.length >= 2 ? (typescriptOrContext as typeof TS) : (typescriptOrContext as Pick).typescript; 33 | const compilerOptions = arguments.length >= 2 ? compilerOptionsOrUndefined! : (typescriptOrContext as Pick).compilerOptions; 34 | 35 | // If value imports shouldn't always be preserved, we'll have to perform import preservation logic 36 | // eslint-disable-next-line @typescript-eslint/no-deprecated 37 | const preserveValueImports = !Boolean(compilerOptions.verbatimModuleSyntax) && Boolean(compilerOptions.preserveValueImports); 38 | if (!Boolean(preserveValueImports)) return true; 39 | 40 | // Only TypeScript v4.5 and newer supports the `preserValueImports` Compiler option 41 | if (parseFloat(typescript.version) < 4.5) return true; 42 | 43 | switch (compilerOptions.module) { 44 | case typescript.ModuleKind.AMD: 45 | case typescript.ModuleKind.UMD: 46 | case typescript.ModuleKind.CommonJS: 47 | case typescript.ModuleKind.System: 48 | case typescript.ModuleKind.None: 49 | // None of these module systems support the `preserValueImports` Compiler option 50 | return true; 51 | default: 52 | return false; 53 | } 54 | } 55 | 56 | export function getImportDefaultHelper(typescript: TSWithHelpers): TS.EmitHelper { 57 | return typescript.importDefaultHelper ?? HELPERS.importDefaultHelper; 58 | } 59 | 60 | export function getImportStarHelper(typescript: TSWithHelpers): TS.EmitHelper { 61 | return typescript.importStarHelper ?? HELPERS.importStarHelper; 62 | } 63 | 64 | export function moduleKindSupportsImportHelpers(moduleKind: TS.ModuleKind = TS.ModuleKind.CommonJS, typescript: typeof TS): boolean { 65 | switch (moduleKind) { 66 | case typescript.ModuleKind.CommonJS: 67 | case typescript.ModuleKind.UMD: 68 | case typescript.ModuleKind.AMD: 69 | return true; 70 | default: 71 | return false; 72 | } 73 | } 74 | 75 | export function moduleKindDefinesDependencies(moduleKind: TS.ModuleKind = TS.ModuleKind.CommonJS, typescript: typeof TS): boolean { 76 | switch (moduleKind) { 77 | case typescript.ModuleKind.UMD: 78 | case typescript.ModuleKind.AMD: 79 | return true; 80 | default: 81 | return false; 82 | } 83 | } 84 | 85 | interface EmitHelperFactory { 86 | getUnscopedHelperName(helperName: string): TS.Identifier; 87 | } 88 | 89 | type TSWithEmitHelpers = typeof TS & 90 | ( 91 | | EmitHelperFactory 92 | | { 93 | createEmitHelperFactory(factory: TS.TransformationContext): EmitHelperFactory; 94 | } 95 | | { 96 | getHelperName(helperName: string): TS.Identifier; 97 | } 98 | ); 99 | 100 | export function getUnscopedHelperName(context: VisitorContext, helperName: string): TS.Identifier { 101 | const typescript = context.typescript as TSWithEmitHelpers; 102 | if ("getUnscopedHelperName" in typescript) { 103 | return typescript.getUnscopedHelperName(helperName); 104 | } else if ("createEmitHelperFactory" in typescript) { 105 | return typescript.createEmitHelperFactory(context.transformationContext).getUnscopedHelperName(helperName); 106 | } else { 107 | return typescript.getHelperName(helperName); 108 | } 109 | } 110 | 111 | export function getRootBlockInsertionPosition(rootBlock: RootBlock, typescript: typeof TS): number { 112 | let insertPosition = 0; 113 | 114 | for (let i = 0; i < rootBlock.statements.length; i++) { 115 | const statement = rootBlock.statements[i]!; 116 | 117 | const isUseStrict = typescript.isExpressionStatement(statement) && typescript.isStringLiteralLike(statement.expression) && statement.expression.text === "use strict"; 118 | 119 | const isEsModuleSymbol = 120 | typescript.isExpressionStatement(statement) && 121 | typescript.isCallExpression(statement.expression) && 122 | typescript.isPropertyAccessExpression(statement.expression.expression) && 123 | typescript.isIdentifier(statement.expression.expression.expression) && 124 | typescript.isIdentifier(statement.expression.expression.name) && 125 | statement.expression.expression.expression.text === "Object" && 126 | statement.expression.expression.name.text === "defineProperty" && 127 | statement.expression.arguments.length >= 2 && 128 | typescript.isIdentifier(statement.expression.arguments[0]!) && 129 | statement.expression.arguments[0].text === "exports" && 130 | typescript.isStringLiteralLike(statement.expression.arguments[1]!) && 131 | statement.expression.arguments[1].text === "__esModule"; 132 | 133 | if (isUseStrict || isEsModuleSymbol) { 134 | insertPosition = Math.max(insertPosition, i + 1); 135 | } 136 | } 137 | return insertPosition; 138 | } 139 | 140 | export function getDefineArrayLiteralExpression(sourceFile: TS.SourceFile, context: VisitorContext): TS.ArrayLiteralExpression | undefined { 141 | const {compilerOptions, typescript} = context; 142 | 143 | switch (compilerOptions.module) { 144 | case typescript.ModuleKind.ESNext: 145 | case typescript.ModuleKind.ES2015: 146 | case typescript.ModuleKind.ES2020: 147 | case typescript.ModuleKind.ES2022: 148 | // There are no such thing for these module types 149 | return undefined; 150 | 151 | // If we're targeting UMD, the root block won't be the root scope, but the Function Body of an iife 152 | case typescript.ModuleKind.UMD: { 153 | for (const statement of sourceFile.statements) { 154 | if ( 155 | typescript.isExpressionStatement(statement) && 156 | typescript.isCallExpression(statement.expression) && 157 | typescript.isParenthesizedExpression(statement.expression.expression) && 158 | typescript.isFunctionExpression(statement.expression.expression.expression) && 159 | statement.expression.expression.expression.parameters.length === 1 160 | ) { 161 | const [firstParameter] = statement.expression.expression.expression.parameters; 162 | if (firstParameter != null && typescript.isIdentifier(firstParameter.name)) { 163 | if (firstParameter.name.text === "factory") { 164 | for (const subStatement of statement.expression.expression.expression.body.statements) { 165 | if ( 166 | typescript.isIfStatement(subStatement) && 167 | subStatement.elseStatement != null && 168 | typescript.isIfStatement(subStatement.elseStatement) && 169 | typescript.isBlock(subStatement.elseStatement.thenStatement) 170 | ) { 171 | for (const subSubStatement of subStatement.elseStatement.thenStatement.statements) { 172 | if ( 173 | typescript.isExpressionStatement(subSubStatement) && 174 | typescript.isCallExpression(subSubStatement.expression) && 175 | subSubStatement.expression.arguments.length === 2 && 176 | typescript.isIdentifier(subSubStatement.expression.expression) && 177 | subSubStatement.expression.expression.text === "define" 178 | ) { 179 | const [firstSubSubStatementExpressionArgument] = subSubStatement.expression.arguments; 180 | if (firstSubSubStatementExpressionArgument != null && typescript.isArrayLiteralExpression(firstSubSubStatementExpressionArgument)) { 181 | return firstSubSubStatementExpressionArgument; 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | } 191 | break; 192 | } 193 | 194 | case typescript.ModuleKind.AMD: { 195 | for (const statement of sourceFile.statements) { 196 | if ( 197 | typescript.isExpressionStatement(statement) && 198 | typescript.isCallExpression(statement.expression) && 199 | typescript.isIdentifier(statement.expression.expression) && 200 | statement.expression.expression.text === "define" && 201 | statement.expression.arguments.length === 2 202 | ) { 203 | const [firstArgument, secondArgument] = statement.expression.arguments; 204 | if (firstArgument != null && secondArgument != null && typescript.isArrayLiteralExpression(firstArgument)) { 205 | if (typescript.isFunctionExpression(secondArgument) && secondArgument.parameters.length >= 2) { 206 | const [firstParameter, secondParameter] = secondArgument.parameters; 207 | if ( 208 | firstParameter != null && 209 | secondParameter != null && 210 | typescript.isIdentifier(firstParameter.name) && 211 | typescript.isIdentifier(secondParameter.name) && 212 | firstParameter.name.text === "require" && 213 | secondParameter.name.text === "exports" 214 | ) { 215 | return firstArgument; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | break; 222 | } 223 | } 224 | 225 | return undefined; 226 | } 227 | 228 | export function getRootBlock(sourceFile: TS.SourceFile, context: VisitorContext): RootBlock { 229 | const {compilerOptions, typescript} = context; 230 | 231 | switch (compilerOptions.module) { 232 | // If we're targeting UMD, the root block won't be the root scope, but the Function Body of an iife 233 | case typescript.ModuleKind.UMD: { 234 | for (const statement of sourceFile.statements) { 235 | if (typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && statement.expression.arguments.length === 1) { 236 | const [firstArgument] = statement.expression.arguments; 237 | if (firstArgument != null && typescript.isFunctionExpression(firstArgument) && firstArgument.parameters.length === 2) { 238 | const [firstParameter, secondParameter] = firstArgument.parameters; 239 | if ( 240 | firstParameter != null && 241 | secondParameter != null && 242 | typescript.isIdentifier(firstParameter.name) && 243 | typescript.isIdentifier(secondParameter.name) && 244 | firstParameter.name.text === "require" && 245 | secondParameter.name.text === "exports" 246 | ) { 247 | return firstArgument.body; 248 | } 249 | } 250 | } 251 | } 252 | break; 253 | } 254 | 255 | // If we're targeting AMD, the root block won't be the root scope, but the Function Body of the 256 | // anonymous function provided as a second argument to the define() function 257 | case typescript.ModuleKind.AMD: { 258 | for (const statement of sourceFile.statements) { 259 | if ( 260 | typescript.isExpressionStatement(statement) && 261 | typescript.isCallExpression(statement.expression) && 262 | typescript.isIdentifier(statement.expression.expression) && 263 | statement.expression.expression.text === "define" && 264 | statement.expression.arguments.length === 2 265 | ) { 266 | const [, secondArgument] = statement.expression.arguments; 267 | if (secondArgument != null && typescript.isFunctionExpression(secondArgument) && secondArgument.parameters.length >= 2) { 268 | const [firstParameter, secondParameter] = secondArgument.parameters; 269 | if ( 270 | firstParameter != null && 271 | secondParameter != null && 272 | typescript.isIdentifier(firstParameter.name) && 273 | typescript.isIdentifier(secondParameter.name) && 274 | firstParameter.name.text === "require" && 275 | secondParameter.name.text === "exports" 276 | ) { 277 | return secondArgument.body; 278 | } 279 | } 280 | } 281 | } 282 | break; 283 | } 284 | } 285 | 286 | return sourceFile; 287 | } 288 | 289 | export function isImportedSymbolImported(importedSymbol: ImportedSymbol, rootBlock: RootBlock, context: VisitorContext): boolean { 290 | const {compilerOptions, typescript} = context; 291 | 292 | switch (compilerOptions.module) { 293 | case typescript.ModuleKind.ES2022: 294 | case typescript.ModuleKind.ES2020: 295 | case typescript.ModuleKind.ES2015: 296 | case typescript.ModuleKind.ESNext: { 297 | for (const statement of rootBlock.statements) { 298 | if (!typescript.isImportDeclaration(statement)) continue; 299 | if (!typescript.isStringLiteralLike(statement.moduleSpecifier)) { 300 | continue; 301 | } 302 | if (statement.moduleSpecifier.text !== importedSymbol.moduleSpecifier) { 303 | continue; 304 | } 305 | if (statement.importClause == null) { 306 | continue; 307 | } 308 | 309 | if ("isDefaultImport" in importedSymbol) { 310 | if (importedSymbol.isDefaultImport) { 311 | if (statement.importClause.name == null) { 312 | continue; 313 | } 314 | if (statement.importClause.name.text !== importedSymbol.name) { 315 | continue; 316 | } 317 | return true; 318 | } else { 319 | if (statement.importClause.namedBindings == null) continue; 320 | if (!typescript.isNamedImports(statement.importClause.namedBindings)) { 321 | continue; 322 | } 323 | for (const importSpecifier of statement.importClause.namedBindings.elements) { 324 | if (importSpecifier.name.text !== importedSymbol.name) continue; 325 | return true; 326 | } 327 | } 328 | } else if ("isNamespaceImport" in importedSymbol) { 329 | if (statement.importClause.namedBindings == null) continue; 330 | if (!typescript.isNamespaceImport(statement.importClause.namedBindings)) { 331 | continue; 332 | } 333 | if (statement.importClause.namedBindings.name.text !== importedSymbol.name) { 334 | continue; 335 | } 336 | return true; 337 | } 338 | } 339 | 340 | return false; 341 | } 342 | 343 | case typescript.ModuleKind.CommonJS: 344 | case typescript.ModuleKind.AMD: 345 | case typescript.ModuleKind.UMD: { 346 | for (const statement of rootBlock.statements) { 347 | if (!typescript.isVariableStatement(statement)) continue; 348 | for (const declaration of statement.declarationList.declarations) { 349 | if (!typescript.isIdentifier(declaration.name)) continue; 350 | if (declaration.name.text !== importedSymbol.name) continue; 351 | return true; 352 | } 353 | } 354 | } 355 | } 356 | 357 | // TODO: Add support for other module systems 358 | return false; 359 | } 360 | 361 | export function generateImportStatementForImportedSymbolInContext(importedSymbol: ImportedSymbol, context: VisitorContext): TS.Statement | undefined { 362 | const {compilerOptions, typescript, factory} = context; 363 | 364 | switch (compilerOptions.module) { 365 | case typescript.ModuleKind.ES2022: 366 | case typescript.ModuleKind.ES2020: 367 | case typescript.ModuleKind.ES2015: 368 | case typescript.ModuleKind.ESNext: { 369 | return factory.createImportDeclaration( 370 | undefined, 371 | "isDefaultImport" in importedSymbol 372 | ? factory.createImportClause( 373 | false, 374 | !importedSymbol.isDefaultImport ? undefined : factory.createIdentifier(importedSymbol.name), 375 | importedSymbol.isDefaultImport 376 | ? undefined 377 | : factory.createNamedImports([ 378 | factory.createImportSpecifier( 379 | false, 380 | importedSymbol.propertyName === importedSymbol.name ? undefined : factory.createIdentifier(importedSymbol.propertyName), 381 | factory.createIdentifier(importedSymbol.name) 382 | ) 383 | ]) 384 | ) 385 | : "isNamespaceImport" in importedSymbol 386 | ? factory.createImportClause(false, undefined, factory.createNamespaceImport(factory.createIdentifier(importedSymbol.name))) 387 | : undefined, 388 | factory.createStringLiteral(importedSymbol.moduleSpecifier) 389 | ); 390 | } 391 | 392 | case typescript.ModuleKind.CommonJS: 393 | case typescript.ModuleKind.AMD: 394 | case typescript.ModuleKind.UMD: { 395 | const requireCall = factory.createCallExpression(factory.createIdentifier("require"), undefined, [factory.createStringLiteral(importedSymbol.moduleSpecifier)]); 396 | 397 | let wrappedRequireCall = requireCall; 398 | 399 | // We'll need to use a helper, '__importDefault', and wrap the require call with it 400 | if ( 401 | compilerOptions.esModuleInterop === true && 402 | (("isDefaultImport" in importedSymbol && importedSymbol.isDefaultImport) || (!("isDefaultImport" in importedSymbol) && Boolean(importedSymbol.isNamespaceImport))) 403 | ) { 404 | // If tslib is being used, we can do something like 'require("tslib").__import{Default|Star}()' 405 | if (compilerOptions.importHelpers === true) { 406 | wrappedRequireCall = factory.createCallExpression( 407 | factory.createPropertyAccessExpression( 408 | factory.createCallExpression(factory.createIdentifier("require"), undefined, [factory.createStringLiteral("tslib")]), 409 | getUnscopedHelperName(context, "isDefaultImport" in importedSymbol ? "__importDefault" : "__importStar") 410 | ), 411 | undefined, 412 | [requireCall] 413 | ); 414 | } 415 | 416 | // Otherwise, we'll have to make sure that the helper is being inlined in an transformation step later 417 | else { 418 | // We've already requested the __importDefault helper in the before transformer under these 419 | // circumstances 420 | wrappedRequireCall = factory.createCallExpression(getUnscopedHelperName(context, "isDefaultImport" in importedSymbol ? "__importDefault" : "__importStar"), undefined, [ 421 | requireCall 422 | ]); 423 | } 424 | } 425 | 426 | return factory.createVariableStatement( 427 | undefined, 428 | factory.createVariableDeclarationList( 429 | [factory.createVariableDeclaration(factory.createIdentifier(importedSymbol.name), undefined, undefined, wrappedRequireCall)], 430 | typescript.NodeFlags.Const 431 | ) 432 | ); 433 | } 434 | } 435 | 436 | // TODO: Handle other module types as well 437 | return undefined; 438 | } 439 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import type {Nullable} from "helpertypes"; 2 | import crypto from "crypto"; 3 | 4 | /** 5 | * Ensures that the given item is an array 6 | */ 7 | export function ensureArray(item: T[] | T): T[] { 8 | return Array.isArray(item) ? item : [item]; 9 | } 10 | 11 | /** 12 | * Converts the given string to a boolean 13 | */ 14 | export function booleanize(str: string | boolean | undefined): boolean { 15 | if (str == null) return false; 16 | if (typeof str === "boolean") return str; 17 | 18 | if (isTrueLike(str)) { 19 | return true; 20 | } else if (isFalseLike(str)) { 21 | return false; 22 | } else { 23 | return Boolean(str); 24 | } 25 | } 26 | 27 | export function isTrueLike(str: Nullable): boolean { 28 | if (typeof str === "boolean") return str; 29 | if (str == null) return false; 30 | 31 | switch (str.toLowerCase().trim()) { 32 | case "true": 33 | case "yes": 34 | case "1": 35 | case "": 36 | return true; 37 | 38 | default: 39 | return false; 40 | } 41 | } 42 | 43 | export function isFalseLike(str: Nullable): boolean { 44 | if (typeof str === "boolean") return !str; 45 | if (str == null) return true; 46 | 47 | switch (str.toLowerCase().trim()) { 48 | case "false": 49 | case "no": 50 | case "0": 51 | return true; 52 | 53 | default: 54 | return false; 55 | } 56 | } 57 | 58 | export const sha1 = (data: string) => crypto.createHash("sha1").update(data).digest("hex"); 59 | export const NOOP = () => { 60 | // Noop 61 | }; 62 | -------------------------------------------------------------------------------- /test/amd.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import {includeEmitHelper} from "./util/include-emit-helper.js"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("AMD => Preserves Type-only imports. #1", "*", (_, {typescript, useProgram}) => { 8 | const bundle = generateCustomTransformerResult( 9 | [ 10 | { 11 | entry: true, 12 | fileName: "index.ts", 13 | text: ` 14 | import {DIContainer} from "@wessberg/di"; 15 | import Foo, {IFoo} from "./foo"; 16 | console.log(Foo); 17 | 18 | const container = new DIContainer(); 19 | container.registerSingleton(); 20 | ` 21 | }, 22 | { 23 | entry: false, 24 | fileName: "foo.ts", 25 | text: ` 26 | export interface IFoo {} 27 | export default class Foo implements IFoo {} 28 | ` 29 | } 30 | ], 31 | { 32 | typescript, 33 | useProgram, 34 | compilerOptions: { 35 | module: typescript.ModuleKind.AMD 36 | } 37 | } 38 | ); 39 | 40 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 41 | 42 | assert.deepEqual( 43 | formatCode(file.text), 44 | formatCode(`\ 45 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1, foo_1) { 46 | "use strict"; 47 | Object.defineProperty(exports, "__esModule", { value: true }); 48 | const Foo = require("./foo"); 49 | console.log(foo_1.default); 50 | const container = new di_1.DIContainer(); 51 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 52 | }); 53 | `) 54 | ); 55 | }); 56 | 57 | test("AMD => Preserves type-only imports. #2", "*", (_, {typescript, useProgram}) => { 58 | const bundle = generateCustomTransformerResult( 59 | [ 60 | { 61 | entry: true, 62 | fileName: "index.ts", 63 | text: ` 64 | import {DIContainer} from "@wessberg/di"; 65 | import Foo, {IFoo} from "./foo"; 66 | 67 | const container = new DIContainer(); 68 | container.registerSingleton(); 69 | ` 70 | }, 71 | { 72 | entry: false, 73 | fileName: "foo.ts", 74 | text: ` 75 | export interface IFoo {} 76 | export default class Foo implements IFoo {} 77 | ` 78 | } 79 | ], 80 | { 81 | typescript, 82 | useProgram, 83 | compilerOptions: { 84 | module: typescript.ModuleKind.AMD 85 | } 86 | } 87 | ); 88 | 89 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 90 | 91 | assert.deepEqual( 92 | formatCode(file.text), 93 | formatCode(`\ 94 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 95 | "use strict"; 96 | Object.defineProperty(exports, "__esModule", { value: true }); 97 | const Foo = require("./foo"); 98 | const container = new di_1.DIContainer(); 99 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 100 | }); 101 | `) 102 | ); 103 | }); 104 | 105 | test("AMD => Preserves type-only imports. #3", "*", (_, {typescript, useProgram}) => { 106 | const bundle = generateCustomTransformerResult( 107 | [ 108 | { 109 | entry: true, 110 | fileName: "index.ts", 111 | text: ` 112 | import {DIContainer} from "@wessberg/di"; 113 | import {Foo, IFoo} from "./foo"; 114 | 115 | const container = new DIContainer(); 116 | container.registerSingleton(); 117 | ` 118 | }, 119 | { 120 | entry: false, 121 | fileName: "foo.ts", 122 | text: ` 123 | export interface IFoo {} 124 | export class Foo implements IFoo {} 125 | ` 126 | } 127 | ], 128 | { 129 | typescript, 130 | useProgram, 131 | compilerOptions: { 132 | module: typescript.ModuleKind.AMD 133 | } 134 | } 135 | ); 136 | 137 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 138 | 139 | assert.deepEqual( 140 | formatCode(file.text), 141 | formatCode(`\ 142 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 143 | "use strict"; 144 | Object.defineProperty(exports, "__esModule", { value: true }); 145 | const Foo = require("./foo"); 146 | const container = new di_1.DIContainer(); 147 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 148 | }); 149 | `) 150 | ); 151 | }); 152 | 153 | test("AMD => Preserves type-only imports. #4", "*", (_, {typescript, useProgram}) => { 154 | const bundle = generateCustomTransformerResult( 155 | [ 156 | { 157 | entry: true, 158 | fileName: "index.ts", 159 | text: ` 160 | import {DIContainer} from "@wessberg/di"; 161 | import * as Foo from "./foo"; 162 | import {IFoo} from "./foo"; 163 | 164 | const container = new DIContainer(); 165 | container.registerSingleton(); 166 | ` 167 | }, 168 | { 169 | entry: false, 170 | fileName: "foo.ts", 171 | text: ` 172 | export interface IFoo {} 173 | export class Foo implements IFoo {} 174 | ` 175 | } 176 | ], 177 | { 178 | typescript, 179 | useProgram, 180 | compilerOptions: { 181 | module: typescript.ModuleKind.AMD 182 | } 183 | } 184 | ); 185 | 186 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 187 | 188 | assert.deepEqual( 189 | formatCode(file.text), 190 | formatCode(`\ 191 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 192 | "use strict"; 193 | Object.defineProperty(exports, "__esModule", { value: true }); 194 | const Foo = require("./foo"); 195 | const container = new di_1.DIContainer(); 196 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 197 | }); 198 | `) 199 | ); 200 | }); 201 | 202 | test("AMD => Preserves type-only imports. #5", "*", (_, {typescript, useProgram}) => { 203 | const bundle = generateCustomTransformerResult( 204 | [ 205 | { 206 | entry: true, 207 | fileName: "index.ts", 208 | text: ` 209 | import {DIContainer} from "@wessberg/di"; 210 | import {Bar as Foo, IFoo} from "./foo"; 211 | 212 | const container = new DIContainer(); 213 | container.registerSingleton(); 214 | ` 215 | }, 216 | { 217 | entry: false, 218 | fileName: "foo.ts", 219 | text: ` 220 | export interface IFoo {} 221 | export class Bar implements IFoo {} 222 | ` 223 | } 224 | ], 225 | { 226 | typescript, 227 | useProgram, 228 | compilerOptions: { 229 | module: typescript.ModuleKind.AMD 230 | } 231 | } 232 | ); 233 | 234 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 235 | 236 | assert.deepEqual( 237 | formatCode(file.text), 238 | formatCode(`\ 239 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 240 | "use strict"; 241 | Object.defineProperty(exports, "__esModule", { value: true }); 242 | const Foo = require("./foo"); 243 | const container = new di_1.DIContainer(); 244 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Bar }); 245 | }); 246 | `) 247 | ); 248 | }); 249 | 250 | test("AMD => Preserves type-only imports. #6", "*", (_, {typescript, useProgram}) => { 251 | const bundle = generateCustomTransformerResult( 252 | [ 253 | { 254 | entry: true, 255 | fileName: "index.ts", 256 | text: ` 257 | import {DIContainer} from "@wessberg/di"; 258 | import {default as Foo, IFoo} from "./foo"; 259 | 260 | const container = new DIContainer(); 261 | container.registerSingleton(); 262 | ` 263 | }, 264 | { 265 | entry: false, 266 | fileName: "foo.ts", 267 | text: ` 268 | export interface IFoo {} 269 | export default class Bar implements IFoo {} 270 | ` 271 | } 272 | ], 273 | { 274 | typescript, 275 | useProgram, 276 | compilerOptions: { 277 | module: typescript.ModuleKind.AMD 278 | } 279 | } 280 | ); 281 | 282 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 283 | 284 | assert.deepEqual( 285 | formatCode(file.text), 286 | formatCode(`\ 287 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 288 | "use strict"; 289 | Object.defineProperty(exports, "__esModule", { value: true }); 290 | const Foo = require("./foo"); 291 | const container = new di_1.DIContainer(); 292 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 293 | }); 294 | `) 295 | ); 296 | }); 297 | 298 | test("AMD => Preserves type-only imports. #7", "*", (_, {typescript, useProgram}) => { 299 | const bundle = generateCustomTransformerResult( 300 | [ 301 | { 302 | entry: true, 303 | fileName: "index.ts", 304 | text: ` 305 | import {DIContainer} from "@wessberg/di"; 306 | import {Foo, Bar, IFoo} from "./foo"; 307 | console.log(Bar); 308 | 309 | const container = new DIContainer(); 310 | container.registerSingleton(); 311 | ` 312 | }, 313 | { 314 | entry: false, 315 | fileName: "foo.ts", 316 | text: ` 317 | export interface IFoo {} 318 | export class Foo implements IFoo {} 319 | export class Bar {} 320 | ` 321 | } 322 | ], 323 | { 324 | typescript, 325 | useProgram, 326 | compilerOptions: { 327 | module: typescript.ModuleKind.AMD 328 | } 329 | } 330 | ); 331 | 332 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 333 | 334 | assert.deepEqual( 335 | formatCode(file.text), 336 | formatCode(`\ 337 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1, foo_1) { 338 | "use strict"; 339 | Object.defineProperty(exports, "__esModule", { value: true }); 340 | const Foo = require("./foo"); 341 | console.log(foo_1.Bar); 342 | const container = new di_1.DIContainer(); 343 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 344 | }); 345 | `) 346 | ); 347 | }); 348 | 349 | test("AMD => Preserves type-only imports when 'preserveValueImports' is true. #1", `>=4.5`, (_, {typescript, useProgram}) => { 350 | const bundle = generateCustomTransformerResult( 351 | [ 352 | { 353 | entry: true, 354 | fileName: "index.ts", 355 | text: ` 356 | import {DIContainer} from "@wessberg/di"; 357 | import {Foo, Bar, IFoo} from "./foo"; 358 | console.log(Bar); 359 | 360 | const container = new DIContainer(); 361 | container.registerSingleton(); 362 | ` 363 | }, 364 | { 365 | entry: false, 366 | fileName: "foo.ts", 367 | text: ` 368 | export interface IFoo {} 369 | export class Foo implements IFoo {} 370 | export class Bar {} 371 | ` 372 | } 373 | ], 374 | { 375 | typescript, 376 | useProgram, 377 | compilerOptions: { 378 | module: typescript.ModuleKind.AMD, 379 | preserveValueImports: true 380 | } 381 | } 382 | ); 383 | 384 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 385 | 386 | assert.deepEqual( 387 | formatCode(file.text), 388 | formatCode(`\ 389 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1, foo_1) { 390 | "use strict"; 391 | Object.defineProperty(exports, "__esModule", { value: true }); 392 | const Foo = require("./foo"); 393 | console.log(foo_1.Bar); 394 | const container = new di_1.DIContainer(); 395 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 396 | }); 397 | `) 398 | ); 399 | }); 400 | 401 | test("AMD => Preserves type-only imports with esModuleInterop and importHelpers. #1", "*", (_, {typescript, useProgram}) => { 402 | const bundle = generateCustomTransformerResult( 403 | [ 404 | { 405 | entry: true, 406 | fileName: "index.ts", 407 | text: ` 408 | import {DIContainer} from "@wessberg/di"; 409 | import Foo, {IFoo} from "./foo"; 410 | 411 | const container = new DIContainer(); 412 | container.registerSingleton(); 413 | ` 414 | }, 415 | { 416 | entry: false, 417 | fileName: "foo.ts", 418 | text: ` 419 | export interface IFoo {} 420 | export default class Foo implements IFoo {} 421 | ` 422 | } 423 | ], 424 | { 425 | typescript, 426 | useProgram, 427 | compilerOptions: { 428 | esModuleInterop: true, 429 | importHelpers: true, 430 | module: typescript.ModuleKind.AMD 431 | } 432 | } 433 | ); 434 | 435 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 436 | 437 | assert.deepEqual( 438 | formatCode(file.text), 439 | formatCode(`\ 440 | define(["require", "exports", "@wessberg/di", "./foo", "tslib"], function (require, exports, di_1) { 441 | "use strict"; 442 | Object.defineProperty(exports, "__esModule", { value: true }); 443 | const Foo = require("tslib").__importDefault(require("./foo")); 444 | const container = new di_1.DIContainer(); 445 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 446 | }); 447 | `) 448 | ); 449 | }); 450 | 451 | test("AMD => Preserves type-only imports with esModuleInterop. #1", "*", (_, {typescript, useProgram}) => { 452 | const bundle = generateCustomTransformerResult( 453 | [ 454 | { 455 | entry: true, 456 | fileName: "index.ts", 457 | text: ` 458 | import {DIContainer} from "@wessberg/di"; 459 | import Foo, {IFoo} from "./foo"; 460 | 461 | const container = new DIContainer(); 462 | container.registerSingleton(); 463 | ` 464 | }, 465 | { 466 | entry: false, 467 | fileName: "foo.ts", 468 | text: ` 469 | export interface IFoo {} 470 | export default class Foo implements IFoo {} 471 | ` 472 | } 473 | ], 474 | { 475 | typescript, 476 | useProgram, 477 | compilerOptions: { 478 | esModuleInterop: true, 479 | module: typescript.ModuleKind.AMD 480 | } 481 | } 482 | ); 483 | 484 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 485 | 486 | assert.deepEqual( 487 | formatCode(file.text), 488 | formatCode(`\ 489 | ${includeEmitHelper(typescript, "__importDefault")} 490 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 491 | "use strict"; 492 | Object.defineProperty(exports, "__esModule", { value: true }); 493 | const Foo = __importDefault(require("./foo")); 494 | const container = new di_1.DIContainer(); 495 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 496 | }); 497 | `) 498 | ); 499 | }); 500 | 501 | test("AMD => Preserves type-only imports with esModuleInterop. #2", "*", (_, {typescript, useProgram}) => { 502 | const bundle = generateCustomTransformerResult( 503 | [ 504 | { 505 | entry: true, 506 | fileName: "index.ts", 507 | text: ` 508 | import {DIContainer} from "@wessberg/di"; 509 | import Foo, {IFoo} from "./foo"; 510 | console.log(Foo); 511 | 512 | const container = new DIContainer(); 513 | container.registerSingleton(); 514 | ` 515 | }, 516 | { 517 | entry: false, 518 | fileName: "foo.ts", 519 | text: ` 520 | export interface IFoo {} 521 | export default class Foo implements IFoo {} 522 | ` 523 | } 524 | ], 525 | { 526 | typescript, 527 | useProgram, 528 | compilerOptions: { 529 | esModuleInterop: true, 530 | module: typescript.ModuleKind.AMD 531 | } 532 | } 533 | ); 534 | 535 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 536 | 537 | assert.deepEqual( 538 | formatCode(file.text), 539 | formatCode(`\ 540 | ${includeEmitHelper(typescript, "__importDefault")} 541 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1, foo_1) { 542 | "use strict"; 543 | Object.defineProperty(exports, "__esModule", { value: true }); 544 | const Foo = __importDefault(require("./foo")); 545 | foo_1 = __importDefault(foo_1); 546 | console.log(foo_1.default); 547 | const container = new di_1.DIContainer(); 548 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 549 | }); 550 | `) 551 | ); 552 | }); 553 | 554 | test("AMD => Preserves type-only imports with esModuleInterop. #3", "*", (_, {typescript, useProgram}) => { 555 | const bundle = generateCustomTransformerResult( 556 | [ 557 | { 558 | entry: true, 559 | fileName: "index.ts", 560 | text: ` 561 | import {DIContainer} from "@wessberg/di"; 562 | import * as Foo from "./foo"; 563 | import {IFoo} from "./foo"; 564 | 565 | const container = new DIContainer(); 566 | container.registerSingleton(); 567 | ` 568 | }, 569 | { 570 | entry: false, 571 | fileName: "foo.ts", 572 | text: ` 573 | export interface IFoo {} 574 | export class Foo implements IFoo {} 575 | ` 576 | } 577 | ], 578 | { 579 | typescript, 580 | useProgram, 581 | compilerOptions: { 582 | esModuleInterop: true, 583 | module: typescript.ModuleKind.AMD 584 | } 585 | } 586 | ); 587 | 588 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 589 | 590 | assert.deepEqual( 591 | formatCode(file.text), 592 | formatCode(`\ 593 | ${includeEmitHelper(typescript, "__importStar")} 594 | define(["require", "exports", "@wessberg/di", "./foo"], function (require, exports, di_1) { 595 | "use strict"; 596 | Object.defineProperty(exports, "__esModule", { value: true }); 597 | const Foo = __importStar(require("./foo")); 598 | const container = new di_1.DIContainer(); 599 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 600 | }); 601 | `) 602 | ); 603 | }); 604 | -------------------------------------------------------------------------------- /test/commonjs.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import {includeEmitHelper} from "./util/include-emit-helper.js"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("CommonJS => Preserves Type-only imports. #1", "*", (_, {typescript, useProgram}) => { 8 | const bundle = generateCustomTransformerResult( 9 | [ 10 | { 11 | entry: true, 12 | fileName: "index.ts", 13 | text: ` 14 | import {DIContainer} from "@wessberg/di"; 15 | import Foo, {IFoo} from "./foo"; 16 | console.log(Foo); 17 | 18 | const container = new DIContainer(); 19 | container.registerSingleton(); 20 | ` 21 | }, 22 | { 23 | entry: false, 24 | fileName: "foo.ts", 25 | text: ` 26 | export interface IFoo {} 27 | export default class Foo implements IFoo {} 28 | ` 29 | } 30 | ], 31 | { 32 | typescript, 33 | useProgram, 34 | compilerOptions: { 35 | module: typescript.ModuleKind.CommonJS 36 | } 37 | } 38 | ); 39 | 40 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 41 | 42 | assert.deepEqual( 43 | formatCode(file.text), 44 | formatCode(`\ 45 | "use strict"; 46 | Object.defineProperty(exports, "__esModule", { value: true }); 47 | const Foo = require("./foo"); 48 | const di_1 = require("@wessberg/di"); 49 | const foo_1 = require("./foo"); 50 | console.log(foo_1.default); 51 | const container = new di_1.DIContainer(); 52 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 53 | `) 54 | ); 55 | }); 56 | 57 | test("CommonJS => Preserves type-only imports. #2", "*", (_, {typescript, useProgram}) => { 58 | const bundle = generateCustomTransformerResult( 59 | [ 60 | { 61 | entry: true, 62 | fileName: "index.ts", 63 | text: ` 64 | import {DIContainer} from "@wessberg/di"; 65 | import Foo, {IFoo} from "./foo"; 66 | 67 | const container = new DIContainer(); 68 | container.registerSingleton(); 69 | ` 70 | }, 71 | { 72 | entry: false, 73 | fileName: "foo.ts", 74 | text: ` 75 | export interface IFoo {} 76 | export default class Foo implements IFoo {} 77 | ` 78 | } 79 | ], 80 | { 81 | typescript, 82 | useProgram, 83 | compilerOptions: { 84 | module: typescript.ModuleKind.CommonJS 85 | } 86 | } 87 | ); 88 | 89 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 90 | 91 | assert.deepEqual( 92 | formatCode(file.text), 93 | formatCode(`\ 94 | "use strict"; 95 | Object.defineProperty(exports, "__esModule", { value: true }); 96 | const Foo = require("./foo"); 97 | const di_1 = require("@wessberg/di"); 98 | const container = new di_1.DIContainer(); 99 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 100 | `) 101 | ); 102 | }); 103 | 104 | test("CommonJS => Preserves type-only imports. #3", "*", (_, {typescript, useProgram}) => { 105 | const bundle = generateCustomTransformerResult( 106 | [ 107 | { 108 | entry: true, 109 | fileName: "index.ts", 110 | text: ` 111 | import {DIContainer} from "@wessberg/di"; 112 | import {Foo, IFoo} from "./foo"; 113 | 114 | const container = new DIContainer(); 115 | container.registerSingleton(); 116 | ` 117 | }, 118 | { 119 | entry: false, 120 | fileName: "foo.ts", 121 | text: ` 122 | export interface IFoo {} 123 | export class Foo implements IFoo {} 124 | ` 125 | } 126 | ], 127 | { 128 | typescript, 129 | useProgram, 130 | compilerOptions: { 131 | module: typescript.ModuleKind.CommonJS 132 | } 133 | } 134 | ); 135 | 136 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 137 | 138 | assert.deepEqual( 139 | formatCode(file.text), 140 | formatCode(`\ 141 | "use strict"; 142 | Object.defineProperty(exports, "__esModule", { value: true }); 143 | const Foo = require("./foo"); 144 | const di_1 = require("@wessberg/di"); 145 | const container = new di_1.DIContainer(); 146 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 147 | `) 148 | ); 149 | }); 150 | 151 | test("CommonJS => Preserves type-only imports. #4", "*", (_, {typescript, useProgram}) => { 152 | const bundle = generateCustomTransformerResult( 153 | [ 154 | { 155 | entry: true, 156 | fileName: "index.ts", 157 | text: ` 158 | import {DIContainer} from "@wessberg/di"; 159 | import * as Foo from "./foo"; 160 | import {IFoo} from "./foo"; 161 | 162 | const container = new DIContainer(); 163 | container.registerSingleton(); 164 | ` 165 | }, 166 | { 167 | entry: false, 168 | fileName: "foo.ts", 169 | text: ` 170 | export interface IFoo {} 171 | export class Foo implements IFoo {} 172 | ` 173 | } 174 | ], 175 | { 176 | typescript, 177 | useProgram, 178 | compilerOptions: { 179 | module: typescript.ModuleKind.CommonJS 180 | } 181 | } 182 | ); 183 | 184 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 185 | 186 | assert.deepEqual( 187 | formatCode(file.text), 188 | formatCode(`\ 189 | "use strict"; 190 | Object.defineProperty(exports, "__esModule", { value: true }); 191 | const Foo = require("./foo"); 192 | const di_1 = require("@wessberg/di"); 193 | const container = new di_1.DIContainer(); 194 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 195 | `) 196 | ); 197 | }); 198 | 199 | test("CommonJS => Preserves type-only imports. #5", "*", (_, {typescript, useProgram}) => { 200 | const bundle = generateCustomTransformerResult( 201 | [ 202 | { 203 | entry: true, 204 | fileName: "index.ts", 205 | text: ` 206 | import {DIContainer} from "@wessberg/di"; 207 | import {Bar as Foo, IFoo} from "./foo"; 208 | 209 | const container = new DIContainer(); 210 | container.registerSingleton(); 211 | ` 212 | }, 213 | { 214 | entry: false, 215 | fileName: "foo.ts", 216 | text: ` 217 | export interface IFoo {} 218 | export class Bar implements IFoo {} 219 | ` 220 | } 221 | ], 222 | { 223 | typescript, 224 | useProgram, 225 | compilerOptions: { 226 | module: typescript.ModuleKind.CommonJS 227 | } 228 | } 229 | ); 230 | 231 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 232 | 233 | assert.deepEqual( 234 | formatCode(file.text), 235 | formatCode(`\ 236 | "use strict"; 237 | Object.defineProperty(exports, "__esModule", { value: true }); 238 | const Foo = require("./foo"); 239 | const di_1 = require("@wessberg/di"); 240 | const container = new di_1.DIContainer(); 241 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Bar }); 242 | `) 243 | ); 244 | }); 245 | 246 | test("CommonJS => Preserves type-only imports. #6", "*", (_, {typescript, useProgram}) => { 247 | const bundle = generateCustomTransformerResult( 248 | [ 249 | { 250 | entry: true, 251 | fileName: "index.ts", 252 | text: ` 253 | import {DIContainer} from "@wessberg/di"; 254 | import {default as Foo, IFoo} from "./foo"; 255 | 256 | const container = new DIContainer(); 257 | container.registerSingleton(); 258 | ` 259 | }, 260 | { 261 | entry: false, 262 | fileName: "foo.ts", 263 | text: ` 264 | export interface IFoo {} 265 | export default class Bar implements IFoo {} 266 | ` 267 | } 268 | ], 269 | { 270 | typescript, 271 | useProgram, 272 | compilerOptions: { 273 | module: typescript.ModuleKind.CommonJS 274 | } 275 | } 276 | ); 277 | 278 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 279 | 280 | assert.deepEqual( 281 | formatCode(file.text), 282 | formatCode(`\ 283 | "use strict"; 284 | Object.defineProperty(exports, "__esModule", { value: true }); 285 | const Foo = require("./foo"); 286 | const di_1 = require("@wessberg/di"); 287 | const container = new di_1.DIContainer(); 288 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 289 | `) 290 | ); 291 | }); 292 | 293 | test("CommonJS => Preserves type-only imports. #7", "*", (_, {typescript, useProgram}) => { 294 | const bundle = generateCustomTransformerResult( 295 | [ 296 | { 297 | entry: true, 298 | fileName: "index.ts", 299 | text: ` 300 | import {DIContainer} from "@wessberg/di"; 301 | import {Foo, Bar, IFoo} from "./foo"; 302 | console.log(Bar); 303 | 304 | const container = new DIContainer(); 305 | container.registerSingleton(); 306 | ` 307 | }, 308 | { 309 | entry: false, 310 | fileName: "foo.ts", 311 | text: ` 312 | export interface IFoo {} 313 | export class Foo implements IFoo {} 314 | export class Bar {} 315 | ` 316 | } 317 | ], 318 | { 319 | typescript, 320 | useProgram, 321 | compilerOptions: { 322 | module: typescript.ModuleKind.CommonJS 323 | } 324 | } 325 | ); 326 | 327 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 328 | 329 | assert.deepEqual( 330 | formatCode(file.text), 331 | formatCode(`\ 332 | "use strict"; 333 | Object.defineProperty(exports, "__esModule", { value: true }); 334 | const Foo = require("./foo"); 335 | const di_1 = require("@wessberg/di"); 336 | const foo_1 = require("./foo"); 337 | console.log(foo_1.Bar); 338 | const container = new di_1.DIContainer(); 339 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 340 | `) 341 | ); 342 | }); 343 | 344 | test("CommonJS => Preserves type-only imports with esModuleInterop and importHelpers. #1", "*", (_, {typescript, useProgram}) => { 345 | const bundle = generateCustomTransformerResult( 346 | [ 347 | { 348 | entry: true, 349 | fileName: "index.ts", 350 | text: ` 351 | import {DIContainer} from "@wessberg/di"; 352 | import Foo, {IFoo} from "./foo"; 353 | 354 | const container = new DIContainer(); 355 | container.registerSingleton(); 356 | ` 357 | }, 358 | { 359 | entry: false, 360 | fileName: "foo.ts", 361 | text: ` 362 | export interface IFoo {} 363 | export default class Foo implements IFoo {} 364 | ` 365 | } 366 | ], 367 | { 368 | typescript, 369 | useProgram, 370 | compilerOptions: { 371 | esModuleInterop: true, 372 | importHelpers: true, 373 | module: typescript.ModuleKind.CommonJS 374 | } 375 | } 376 | ); 377 | 378 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 379 | 380 | assert.deepEqual( 381 | formatCode(file.text), 382 | formatCode(`\ 383 | "use strict"; 384 | Object.defineProperty(exports, "__esModule", { value: true }); 385 | const Foo = require("tslib").__importDefault(require("./foo")); 386 | const di_1 = require("@wessberg/di"); 387 | const container = new di_1.DIContainer(); 388 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 389 | `) 390 | ); 391 | }); 392 | 393 | test("CommonJS => Preserves type-only imports with esModuleInterop. #1", "*", (_, {typescript, useProgram}) => { 394 | const bundle = generateCustomTransformerResult( 395 | [ 396 | { 397 | entry: true, 398 | fileName: "index.ts", 399 | text: ` 400 | import {DIContainer} from "@wessberg/di"; 401 | import Foo, {IFoo} from "./foo"; 402 | 403 | const container = new DIContainer(); 404 | container.registerSingleton(); 405 | ` 406 | }, 407 | { 408 | entry: false, 409 | fileName: "foo.ts", 410 | text: ` 411 | export interface IFoo {} 412 | export default class Foo implements IFoo {} 413 | ` 414 | } 415 | ], 416 | { 417 | typescript, 418 | useProgram, 419 | compilerOptions: { 420 | esModuleInterop: true, 421 | module: typescript.ModuleKind.CommonJS 422 | } 423 | } 424 | ); 425 | 426 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 427 | 428 | assert.deepEqual( 429 | formatCode(file.text), 430 | formatCode(`\ 431 | "use strict"; 432 | ${includeEmitHelper(typescript, "__importDefault")} 433 | Object.defineProperty(exports, "__esModule", { value: true }); 434 | const Foo = __importDefault(require("./foo")); 435 | const di_1 = require("@wessberg/di"); 436 | const container = new di_1.DIContainer(); 437 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 438 | `) 439 | ); 440 | }); 441 | 442 | test("CommonJS => Preserves type-only imports with esModuleInterop. #2", "*", (_, {typescript, useProgram}) => { 443 | const bundle = generateCustomTransformerResult( 444 | [ 445 | { 446 | entry: true, 447 | fileName: "index.ts", 448 | text: ` 449 | import {DIContainer} from "@wessberg/di"; 450 | import Foo, {IFoo} from "./foo"; 451 | console.log(Foo); 452 | 453 | const container = new DIContainer(); 454 | container.registerSingleton(); 455 | ` 456 | }, 457 | { 458 | entry: false, 459 | fileName: "foo.ts", 460 | text: ` 461 | export interface IFoo {} 462 | export default class Foo implements IFoo {} 463 | ` 464 | } 465 | ], 466 | { 467 | typescript, 468 | useProgram, 469 | compilerOptions: { 470 | esModuleInterop: true, 471 | module: typescript.ModuleKind.CommonJS 472 | } 473 | } 474 | ); 475 | 476 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 477 | 478 | assert.deepEqual( 479 | formatCode(file.text), 480 | formatCode(`\ 481 | "use strict"; 482 | ${includeEmitHelper(typescript, "__importDefault")} 483 | Object.defineProperty(exports, "__esModule", { value: true }); 484 | const Foo = __importDefault(require("./foo")); 485 | const di_1 = require("@wessberg/di"); 486 | const foo_1 = __importDefault(require("./foo")); 487 | console.log(foo_1.default); 488 | const container = new di_1.DIContainer(); 489 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 490 | `) 491 | ); 492 | }); 493 | 494 | test("CommonJS => preserves type-only imports with esmoduleinterop. #3", "*", (_, {typescript, useProgram}) => { 495 | const bundle = generateCustomTransformerResult( 496 | [ 497 | { 498 | entry: true, 499 | fileName: "index.ts", 500 | text: ` 501 | import {DIContainer} from "@wessberg/di"; 502 | import * as Foo from "./foo"; 503 | import {IFoo} from "./foo"; 504 | 505 | const container = new DIContainer(); 506 | container.registerSingleton(); 507 | ` 508 | }, 509 | { 510 | entry: false, 511 | fileName: "foo.ts", 512 | text: ` 513 | export interface IFoo {} 514 | export class Foo implements IFoo {} 515 | ` 516 | } 517 | ], 518 | { 519 | typescript, 520 | useProgram, 521 | compilerOptions: { 522 | esModuleInterop: true, 523 | module: typescript.ModuleKind.CommonJS 524 | } 525 | } 526 | ); 527 | 528 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 529 | assert.deepEqual( 530 | formatCode(file.text), 531 | formatCode(`\ 532 | "use strict"; 533 | ${includeEmitHelper(typescript, "__importStar")} 534 | Object.defineProperty(exports, "__esModule", { value: true }); 535 | const Foo = __importStar(require("./foo")); 536 | const di_1 = require("@wessberg/di"); 537 | const container = new di_1.DIContainer(); 538 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 539 | `) 540 | ); 541 | }); 542 | -------------------------------------------------------------------------------- /test/constructor-arguments.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import semver from "semver"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("Can parse constructor parameters and extend with an internal static class member. #1", "*", (_, {typescript, useProgram}) => { 8 | const bundle = generateCustomTransformerResult( 9 | [ 10 | { 11 | entry: true, 12 | fileName: "index.ts", 13 | text: ` 14 | interface IFoo {} 15 | class Foo { 16 | constructor (private foo: IFoo) {} 17 | } 18 | ` 19 | } 20 | ], 21 | {typescript, useProgram} 22 | ); 23 | const [file] = bundle; 24 | assert.deepEqual( 25 | formatCode(file!.text), 26 | formatCode(`\ 27 | class Foo {${semver.gte(typescript.version, "4.3.0") ? `\n\t\tfoo;` : ""} 28 | constructor(foo) { 29 | this.foo = foo; 30 | } 31 | static get [Symbol.for("___CTOR_ARGS___")]() { return [\`IFoo\`]; } 32 | } 33 | `) 34 | ); 35 | }); 36 | 37 | test("Can parse constructor parameters and extend with an internal static class member. #2", "*", (_, {typescript, useProgram}) => { 38 | const bundle = generateCustomTransformerResult( 39 | [ 40 | { 41 | entry: true, 42 | fileName: "index.ts", 43 | text: ` 44 | interface IFoo {} 45 | class Foo { 46 | constructor (private foo: IFoo = {}, private bar) {} 47 | } 48 | ` 49 | } 50 | ], 51 | {typescript, useProgram} 52 | ); 53 | const [file] = bundle; 54 | assert.deepEqual( 55 | formatCode(file!.text), 56 | formatCode(`\ 57 | class Foo {${semver.gte(typescript.version, "4.3.0") ? `\n\t\tfoo;\n\t\tbar;` : ""} 58 | constructor(foo = {}, bar) { 59 | this.foo = foo; 60 | this.bar = bar; 61 | } 62 | static get [Symbol.for("___CTOR_ARGS___")]() { return [\`IFoo\`, undefined]; } 63 | } 64 | `) 65 | ); 66 | }); 67 | 68 | test("When declaring service dependencies via constructor arguments, their type arguments should be irrelevant. #1", "*", (_, {typescript, useProgram}) => { 69 | const bundle = generateCustomTransformerResult( 70 | [ 71 | { 72 | entry: true, 73 | fileName: "index.ts", 74 | text: ` 75 | interface IFoo {} 76 | class Foo { 77 | constructor (private foo: IFoo) {} 78 | } 79 | ` 80 | } 81 | ], 82 | {typescript, useProgram} 83 | ); 84 | const [file] = bundle; 85 | 86 | assert.deepEqual( 87 | formatCode(file!.text), 88 | formatCode(`\ 89 | class Foo {${semver.gte(typescript.version, "4.3.0") ? `\n\t\tfoo;` : ""} 90 | constructor(foo) { 91 | this.foo = foo; 92 | } 93 | static get [Symbol.for("___CTOR_ARGS___")]() { return [\`IFoo\`]; } 94 | } 95 | `) 96 | ); 97 | }); 98 | -------------------------------------------------------------------------------- /test/container.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import semver from "semver"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("Only considers containers that are instances of DIContainer. #1", "*", (_, {typescript, useProgram}) => { 8 | const bundle = generateCustomTransformerResult( 9 | [ 10 | { 11 | entry: true, 12 | fileName: "index.ts", 13 | text: ` 14 | class Foo {} 15 | class MyContainer { 16 | registerSingleton (): T|undefined { 17 | console.log("foo"); 18 | return undefined; 19 | } 20 | } 21 | const container = new MyContainer(); 22 | container.registerSingleton(); 23 | ` 24 | } 25 | ], 26 | {typescript, useProgram} 27 | ); 28 | const [file] = bundle; 29 | 30 | assert.deepEqual( 31 | formatCode(file!.text), 32 | formatCode(`\ 33 | class Foo {} 34 | class MyContainer { 35 | registerSingleton () { 36 | console.log("foo"); 37 | return undefined; 38 | } 39 | } 40 | const container = new MyContainer(); 41 | container.registerSingleton(); 42 | `) 43 | ); 44 | }); 45 | 46 | test("Supports ElementAccessExpressions. #1", "*", (_, {typescript, useProgram}) => { 47 | const bundle = generateCustomTransformerResult( 48 | [ 49 | { 50 | entry: true, 51 | fileName: "index.ts", 52 | text: ` 53 | import {DIContainer} from "@wessberg/di"; 54 | class Foo {} 55 | const container = new DIContainer(); 56 | 57 | container["registerSingleton"](); 58 | ` 59 | } 60 | ], 61 | {typescript, useProgram} 62 | ); 63 | const [file] = bundle; 64 | 65 | assert.deepEqual( 66 | formatCode(file!.text), 67 | formatCode(`\ 68 | import { DIContainer } from "@wessberg/di"; 69 | class Foo { 70 | } 71 | const container = new DIContainer(); 72 | container["registerSingleton"](undefined, { identifier: \`Foo\`, implementation: Foo }); 73 | `) 74 | ); 75 | }); 76 | 77 | test("Supports ElementAccessExpressions when an identifier is passed. #1", "*", (_, {typescript, useProgram}) => { 78 | const bundle = generateCustomTransformerResult( 79 | [ 80 | { 81 | entry: true, 82 | fileName: "index.ts", 83 | text: ` 84 | import {DIContainer} from "@wessberg/di"; 85 | class Foo {} 86 | const container = new DIContainer(); 87 | 88 | container["registerSingleton"](); 89 | ` 90 | } 91 | ], 92 | {typescript, useProgram, identifier: "container"} 93 | ); 94 | const [file] = bundle; 95 | 96 | assert.deepEqual( 97 | formatCode(file!.text), 98 | formatCode(`\ 99 | import { DIContainer } from "@wessberg/di"; 100 | class Foo { 101 | } 102 | const container = new DIContainer(); 103 | container["registerSingleton"](undefined, { identifier: \`Foo\`, implementation: Foo }); 104 | `) 105 | ); 106 | }); 107 | 108 | test("Supports ElementAccessExpressions. #2", "*", (_, {typescript, useProgram}) => { 109 | const bundle = generateCustomTransformerResult( 110 | [ 111 | { 112 | entry: true, 113 | fileName: "index.ts", 114 | text: ` 115 | import {DIContainer} from "@wessberg/di"; 116 | class Foo {} 117 | const container = new DIContainer(); 118 | const argumentExpression = "registerSingleton"; 119 | 120 | container[argumentExpression](); 121 | ` 122 | } 123 | ], 124 | {typescript, useProgram} 125 | ); 126 | const [file] = bundle; 127 | 128 | assert.deepEqual( 129 | formatCode(file!.text), 130 | formatCode(`\ 131 | import { DIContainer } from "@wessberg/di"; 132 | class Foo { 133 | } 134 | const container = new DIContainer(); 135 | const argumentExpression = "registerSingleton"; 136 | container[argumentExpression](undefined, { identifier: \`Foo\`, implementation: Foo }); 137 | `) 138 | ); 139 | }); 140 | 141 | test("Supports ElementAccessExpressions when an identifier is passed. #2", "*", (_, {typescript, useProgram}) => { 142 | const bundle = generateCustomTransformerResult( 143 | [ 144 | { 145 | entry: true, 146 | fileName: "index.ts", 147 | text: ` 148 | import {DIContainer} from "@wessberg/di"; 149 | class Foo {} 150 | const container = new DIContainer(); 151 | const argumentExpression = "registerSingleton"; 152 | 153 | container[argumentExpression](); 154 | ` 155 | } 156 | ], 157 | {typescript, useProgram, identifier: "container"} 158 | ); 159 | const [file] = bundle; 160 | 161 | assert.deepEqual( 162 | formatCode(file!.text), 163 | formatCode(`\ 164 | import { DIContainer } from "@wessberg/di"; 165 | class Foo { 166 | } 167 | const container = new DIContainer(); 168 | const argumentExpression = "registerSingleton"; 169 | container[argumentExpression](undefined, { identifier: \`Foo\`, implementation: Foo }); 170 | `) 171 | ); 172 | }); 173 | 174 | test("Supports PropertyAccessExpressions. #1", "*", (_, {typescript, useProgram}) => { 175 | const bundle = generateCustomTransformerResult( 176 | [ 177 | { 178 | entry: true, 179 | fileName: "index.ts", 180 | text: ` 181 | import {DIContainer} from "@wessberg/di"; 182 | 183 | interface IFoo {} 184 | 185 | class Foo implements IFoo {} 186 | 187 | const container = new DIContainer(); 188 | container.registerSingleton(); 189 | ` 190 | } 191 | ], 192 | {typescript, useProgram} 193 | ); 194 | const [file] = bundle; 195 | 196 | assert.deepEqual( 197 | formatCode(file!.text), 198 | formatCode(`\ 199 | import { DIContainer } from "@wessberg/di"; 200 | class Foo { 201 | } 202 | const container = new DIContainer(); 203 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 204 | `) 205 | ); 206 | }); 207 | 208 | test("Supports PropertyAccessExpressions when an identifier is passed. #1", "*", (_, {typescript, useProgram}) => { 209 | const bundle = generateCustomTransformerResult( 210 | [ 211 | { 212 | entry: true, 213 | fileName: "index.ts", 214 | text: ` 215 | import {DIContainer} from "@wessberg/di"; 216 | 217 | interface IFoo {} 218 | 219 | class Foo implements IFoo {} 220 | 221 | const container = new DIContainer(); 222 | container.registerSingleton(); 223 | ` 224 | } 225 | ], 226 | {typescript, useProgram, identifier: "container"} 227 | ); 228 | const [file] = bundle; 229 | 230 | assert.deepEqual( 231 | formatCode(file!.text), 232 | formatCode(`\ 233 | import { DIContainer } from "@wessberg/di"; 234 | class Foo { 235 | } 236 | const container = new DIContainer(); 237 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 238 | `) 239 | ); 240 | }); 241 | 242 | test("Supports PropertyAccessExpressions. #2", "*", (_, {typescript, useProgram}) => { 243 | const bundle = generateCustomTransformerResult( 244 | [ 245 | { 246 | entry: true, 247 | fileName: "index.ts", 248 | text: ` 249 | import {DIContainer} from "@wessberg/di"; 250 | 251 | interface IFoo {} 252 | 253 | class Foo implements IFoo {} 254 | 255 | const container = new DIContainer(); 256 | container.registerSingleton(undefined); 257 | ` 258 | } 259 | ], 260 | {typescript, useProgram} 261 | ); 262 | const [file] = bundle; 263 | 264 | assert.deepEqual( 265 | formatCode(file!.text), 266 | formatCode(`\ 267 | import { DIContainer } from "@wessberg/di"; 268 | class Foo { 269 | } 270 | const container = new DIContainer(); 271 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 272 | `) 273 | ); 274 | }); 275 | 276 | test("Won't include imports multiple times when the same implementation is registered multiple times. #1", "*", (_, {typescript, useProgram}) => { 277 | const bundle = generateCustomTransformerResult( 278 | [ 279 | { 280 | entry: true, 281 | fileName: "index.ts", 282 | text: ` 283 | import {DIContainer} from "@wessberg/di"; 284 | import {IFoo, Foo} from "./foo"; 285 | 286 | const container = new DIContainer(); 287 | container.registerSingleton(); 288 | container.registerSingleton(); 289 | ` 290 | }, 291 | { 292 | entry: false, 293 | fileName: "foo.ts", 294 | text: ` 295 | export interface IFoo {} 296 | export class Foo implements IFoo {} 297 | ` 298 | } 299 | ], 300 | {typescript, useProgram} 301 | ); 302 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 303 | 304 | assert.deepEqual( 305 | formatCode(file.text), 306 | formatCode(`\ 307 | import { Foo } from "./foo"; 308 | import { DIContainer } from "@wessberg/di"; 309 | const container = new DIContainer(); 310 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 311 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 312 | `) 313 | ); 314 | }); 315 | 316 | test("Supports custom implementation functions. #1", "*", (_, {typescript, useProgram}) => { 317 | const bundle = generateCustomTransformerResult( 318 | [ 319 | { 320 | entry: true, 321 | fileName: "index.ts", 322 | text: ` 323 | import {DIContainer} from "@wessberg/di"; 324 | 325 | interface IFoo { 326 | foo: string; 327 | } 328 | 329 | const container = new DIContainer(); 330 | container.registerSingleton(() => ({foo: "hello"})); 331 | ` 332 | } 333 | ], 334 | {typescript, useProgram} 335 | ); 336 | const [file] = bundle; 337 | 338 | assert.deepEqual( 339 | formatCode(file!.text), 340 | formatCode(`\ 341 | import { DIContainer } from "@wessberg/di"; 342 | const container = new DIContainer(); 343 | container.registerSingleton(() => ({foo: "hello"}), { identifier: \`IFoo\` }); 344 | `) 345 | ); 346 | }); 347 | 348 | test("Supports custom implementation functions. #2", "*", (_, {typescript, useProgram}) => { 349 | const bundle = generateCustomTransformerResult( 350 | [ 351 | { 352 | entry: true, 353 | fileName: "index.ts", 354 | text: ` 355 | import {DIContainer} from "@wessberg/di"; 356 | 357 | interface Foo { 358 | 359 | } 360 | function foo (options: any): Foo { 361 | return {}; 362 | } 363 | 364 | export const container = new DIContainer(); 365 | // DAL 366 | container.registerSingleton(() => foo(options)); 367 | ` 368 | } 369 | ], 370 | {typescript, useProgram} 371 | ); 372 | const [file] = bundle; 373 | 374 | assert.deepEqual( 375 | formatCode(file!.text), 376 | formatCode(`\ 377 | import { DIContainer } from "@wessberg/di"; 378 | function foo(options) { 379 | return {}; 380 | } 381 | export const container = new DIContainer(); 382 | // DAL 383 | container.registerSingleton(() => foo(options), { identifier: \`Foo\` });`) 384 | ); 385 | }); 386 | 387 | test("Supports custom implementation functions. #3", "*", (_, {typescript, useProgram}) => { 388 | const bundle = generateCustomTransformerResult( 389 | [ 390 | { 391 | entry: true, 392 | fileName: "index.ts", 393 | text: ` 394 | import {DIContainer} from "@wessberg/di"; 395 | 396 | interface Foo { 397 | 398 | } 399 | function foo (options: any): Foo { 400 | return {}; 401 | } 402 | 403 | export const container = new DIContainer(); 404 | // DAL 405 | container.registerSingleton(() => foo(options)); 406 | ` 407 | } 408 | ], 409 | {typescript, useProgram} 410 | ); 411 | const [file] = bundle; 412 | 413 | assert.deepEqual( 414 | formatCode(file!.text), 415 | formatCode(`\ 416 | import { DIContainer } from "@wessberg/di"; 417 | function foo(options) { 418 | return {}; 419 | } 420 | export const container = new DIContainer(); 421 | // DAL 422 | container.registerSingleton(() => foo(options), { identifier: \`Foo\` });`) 423 | ); 424 | }); 425 | 426 | test("When registering a service, the implementation type argument is treated as an optional argument. #1", "*", (_, {typescript, useProgram}) => { 427 | const bundle = generateCustomTransformerResult( 428 | [ 429 | { 430 | entry: true, 431 | fileName: "index.ts", 432 | text: ` 433 | import {DIContainer} from "@wessberg/di"; 434 | 435 | class Foo {} 436 | 437 | const container = new DIContainer(); 438 | container.registerSingleton(); 439 | ` 440 | } 441 | ], 442 | {typescript, useProgram} 443 | ); 444 | const [file] = bundle; 445 | 446 | assert.deepEqual( 447 | formatCode(file!.text), 448 | formatCode(`\ 449 | import { DIContainer } from "@wessberg/di"; 450 | class Foo { 451 | } 452 | const container = new DIContainer(); 453 | container.registerSingleton(undefined, { identifier: \`Foo\`, implementation: Foo }); 454 | `) 455 | ); 456 | }); 457 | 458 | test("When registering a service, the type arguments should be irrelevant. #1", "*", (_, {typescript, useProgram}) => { 459 | const bundle = generateCustomTransformerResult( 460 | [ 461 | { 462 | entry: true, 463 | fileName: "index.ts", 464 | text: ` 465 | import {DIContainer} from "@wessberg/di"; 466 | 467 | interface IFoo {} 468 | class Foo {} 469 | 470 | const container = new DIContainer(); 471 | container.registerSingleton, Foo>(); 472 | ` 473 | } 474 | ], 475 | {typescript, useProgram} 476 | ); 477 | const [file] = bundle; 478 | 479 | assert.deepEqual( 480 | formatCode(file!.text), 481 | formatCode(`\ 482 | import { DIContainer } from "@wessberg/di"; 483 | class Foo { 484 | } 485 | const container = new DIContainer(); 486 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 487 | `) 488 | ); 489 | }); 490 | 491 | test("When registering a service, the type arguments should be irrelevant. #2", "*", (_, {typescript, useProgram}) => { 492 | const bundle = generateCustomTransformerResult( 493 | [ 494 | { 495 | entry: true, 496 | fileName: "index.ts", 497 | text: ` 498 | import {DIContainer} from "@wessberg/di"; 499 | 500 | interface IFoo {} 501 | class Foo {} 502 | 503 | const container = new DIContainer(); 504 | container.registerSingleton, Foo>(() => new Foo()); 505 | ` 506 | } 507 | ], 508 | {typescript, useProgram} 509 | ); 510 | const [file] = bundle; 511 | 512 | assert.deepEqual( 513 | formatCode(file!.text), 514 | formatCode(`\ 515 | import { DIContainer } from "@wessberg/di"; 516 | class Foo { 517 | } 518 | const container = new DIContainer(); 519 | container.registerSingleton(() => new Foo(), { identifier: \`IFoo\` }); 520 | `) 521 | ); 522 | }); 523 | 524 | test("When registering a service, the type arguments should be irrelevant. #3", "*", (_, {typescript, useProgram}) => { 525 | const bundle = generateCustomTransformerResult( 526 | [ 527 | { 528 | entry: true, 529 | fileName: "index.ts", 530 | text: ` 531 | import {DIContainer} from "@wessberg/di"; 532 | 533 | interface IFoo { 534 | foo: {bar: T}; 535 | } 536 | class Foo { 537 | bar: T; 538 | } 539 | 540 | const container = new DIContainer(); 541 | container.registerSingleton["foo"], Foo>(); 542 | ` 543 | } 544 | ], 545 | {typescript, useProgram} 546 | ); 547 | const [file] = bundle; 548 | 549 | assert.deepEqual( 550 | formatCode(file!.text), 551 | formatCode(`\ 552 | import { DIContainer } from "@wessberg/di"; 553 | class Foo {${semver.gte(typescript.version, "4.3.0") ? "\n\t\tbar" : ""} 554 | } 555 | const container = new DIContainer(); 556 | container.registerSingleton(undefined, { identifier: \`IFoo["foo"]\`, implementation: Foo }); 557 | `) 558 | ); 559 | }); 560 | 561 | test("When registering a service, the type argument can be a PropertyAccessTypeNode. #1", "*", (_, {typescript, useProgram}) => { 562 | const bundle = generateCustomTransformerResult( 563 | [ 564 | { 565 | entry: true, 566 | fileName: "index.ts", 567 | text: ` 568 | import {DIContainer} from "@wessberg/di"; 569 | 570 | const container = new DIContainer(); 571 | container.registerSingleton(); 572 | ` 573 | } 574 | ], 575 | {typescript, useProgram} 576 | ); 577 | const [file] = bundle; 578 | 579 | assert.deepEqual( 580 | formatCode(file!.text), 581 | formatCode(`\ 582 | import { DIContainer } from "@wessberg/di"; 583 | const container = new DIContainer(); 584 | container.registerSingleton(undefined, { identifier: \`Intl.RelativeTimeFormat\`, implementation: Intl.RelativeTimeFormat }); 585 | `) 586 | ); 587 | }); 588 | 589 | test("When registering a service, the type argument can be a TypeQueryNode. #1", "*", (_, {typescript, useProgram}) => { 590 | const bundle = generateCustomTransformerResult( 591 | [ 592 | { 593 | entry: true, 594 | fileName: "index.ts", 595 | text: ` 596 | import {DIContainer} from "@wessberg/di"; 597 | 598 | const container = new DIContainer(); 599 | container.registerSingleton(); 600 | ` 601 | } 602 | ], 603 | {typescript, useProgram} 604 | ); 605 | const [file] = bundle; 606 | 607 | assert.deepEqual( 608 | formatCode(file!.text), 609 | formatCode(`\ 610 | import { DIContainer } from "@wessberg/di"; 611 | const container = new DIContainer(); 612 | container.registerSingleton(undefined, { identifier: \`typeof foo\`, implementation: {} }); 613 | `) 614 | ); 615 | }); 616 | -------------------------------------------------------------------------------- /test/esm.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import {test} from "./util/test-runner.js"; 4 | import assert from "node:assert"; 5 | 6 | test("Preserves type-only imports. #1", "*", (_, {typescript, useProgram}) => { 7 | const bundle = generateCustomTransformerResult( 8 | [ 9 | { 10 | entry: true, 11 | fileName: "index.ts", 12 | text: ` 13 | import {DIContainer} from "@wessberg/di"; 14 | import Foo, {IFoo} from "./foo"; 15 | 16 | const container = new DIContainer(); 17 | container.registerSingleton(); 18 | ` 19 | }, 20 | { 21 | entry: false, 22 | fileName: "foo.ts", 23 | text: ` 24 | export interface IFoo {} 25 | export default class Foo implements IFoo {} 26 | ` 27 | } 28 | ], 29 | {typescript, useProgram} 30 | ); 31 | 32 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 33 | 34 | assert.deepEqual( 35 | formatCode(file.text), 36 | formatCode(`\ 37 | import Foo from "./foo"; 38 | import { DIContainer } from "@wessberg/di"; 39 | const container = new DIContainer(); 40 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 41 | `) 42 | ); 43 | }); 44 | 45 | test("Preserves type-only imports. #2", "*", (_, {typescript, useProgram}) => { 46 | const bundle = generateCustomTransformerResult( 47 | [ 48 | { 49 | entry: true, 50 | fileName: "index.ts", 51 | text: ` 52 | import {DIContainer} from "@wessberg/di"; 53 | import {Foo, IFoo} from "./foo"; 54 | 55 | const container = new DIContainer(); 56 | container.registerSingleton(); 57 | ` 58 | }, 59 | { 60 | entry: false, 61 | fileName: "foo.ts", 62 | text: ` 63 | export interface IFoo {} 64 | export class Foo implements IFoo {} 65 | ` 66 | } 67 | ], 68 | {typescript, useProgram} 69 | ); 70 | 71 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 72 | 73 | assert.deepEqual( 74 | formatCode(file.text), 75 | formatCode(`\ 76 | import {Foo} from "./foo"; 77 | import { DIContainer } from "@wessberg/di"; 78 | const container = new DIContainer(); 79 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 80 | `) 81 | ); 82 | }); 83 | 84 | test("Preserves type-only imports. #3", "*", (_, {typescript, useProgram}) => { 85 | const bundle = generateCustomTransformerResult( 86 | [ 87 | { 88 | entry: true, 89 | fileName: "index.ts", 90 | text: ` 91 | import {DIContainer} from "@wessberg/di"; 92 | import * as Foo from "./foo"; 93 | import {IFoo} from "./foo"; 94 | 95 | const container = new DIContainer(); 96 | container.registerSingleton(); 97 | ` 98 | }, 99 | { 100 | entry: false, 101 | fileName: "foo.ts", 102 | text: ` 103 | export interface IFoo {} 104 | export class Foo implements IFoo {} 105 | ` 106 | } 107 | ], 108 | {typescript, useProgram} 109 | ); 110 | 111 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 112 | 113 | assert.deepEqual( 114 | formatCode(file.text), 115 | formatCode(`\ 116 | import * as Foo from "./foo"; 117 | import { DIContainer } from "@wessberg/di"; 118 | const container = new DIContainer(); 119 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 120 | `) 121 | ); 122 | }); 123 | 124 | test("Preserves type-only imports. #4", "*", (_, {typescript, useProgram}) => { 125 | const bundle = generateCustomTransformerResult( 126 | [ 127 | { 128 | entry: true, 129 | fileName: "index.ts", 130 | text: ` 131 | import {DIContainer} from "@wessberg/di"; 132 | import {Bar as Foo, IFoo} from "./foo"; 133 | 134 | const container = new DIContainer(); 135 | container.registerSingleton(); 136 | ` 137 | }, 138 | { 139 | entry: false, 140 | fileName: "foo.ts", 141 | text: ` 142 | export interface IFoo {} 143 | export class Bar implements IFoo {} 144 | ` 145 | } 146 | ], 147 | {typescript, useProgram} 148 | ); 149 | 150 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 151 | 152 | assert.deepEqual( 153 | formatCode(file.text), 154 | formatCode(`\ 155 | import {Bar as Foo} from "./foo"; 156 | import { DIContainer } from "@wessberg/di"; 157 | const container = new DIContainer(); 158 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 159 | `) 160 | ); 161 | }); 162 | 163 | test("Preserves type-only imports. #5", "*", (_, {typescript, useProgram}) => { 164 | const bundle = generateCustomTransformerResult( 165 | [ 166 | { 167 | entry: true, 168 | fileName: "index.ts", 169 | text: ` 170 | import {DIContainer} from "@wessberg/di"; 171 | import {default as Foo, IFoo} from "./foo"; 172 | 173 | const container = new DIContainer(); 174 | container.registerSingleton(); 175 | ` 176 | }, 177 | { 178 | entry: false, 179 | fileName: "foo.ts", 180 | text: ` 181 | export interface IFoo {} 182 | export default class Bar implements IFoo {} 183 | ` 184 | } 185 | ], 186 | {typescript, useProgram} 187 | ); 188 | 189 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 190 | 191 | assert.deepEqual( 192 | formatCode(file.text), 193 | formatCode(`\ 194 | import {default as Foo} from "./foo"; 195 | import { DIContainer } from "@wessberg/di"; 196 | const container = new DIContainer(); 197 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 198 | `) 199 | ); 200 | }); 201 | 202 | test("Preserves type-only imports. #6", "*", (_, {typescript, useProgram}) => { 203 | const bundle = generateCustomTransformerResult( 204 | [ 205 | { 206 | entry: true, 207 | fileName: "index.ts", 208 | text: ` 209 | import {DIContainer} from "@wessberg/di"; 210 | import {Foo, Bar, IFoo} from "./foo"; 211 | console.log(Bar); 212 | 213 | const container = new DIContainer(); 214 | container.registerSingleton(); 215 | ` 216 | }, 217 | { 218 | entry: false, 219 | fileName: "foo.ts", 220 | text: ` 221 | export interface IFoo {} 222 | export class Foo implements IFoo {} 223 | export class Bar {} 224 | ` 225 | } 226 | ], 227 | {typescript, useProgram} 228 | ); 229 | 230 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 231 | 232 | assert.deepEqual( 233 | formatCode(file.text), 234 | formatCode(`\ 235 | import {Foo} from "./foo"; 236 | import {DIContainer} from "@wessberg/di"; 237 | import {Bar} from "./foo"; 238 | console.log(Bar); 239 | const container = new DIContainer(); 240 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 241 | `) 242 | ); 243 | }); 244 | 245 | test("Preserves type-only imports when the 'preserveValueImports' CompilerOption is set. #1", `>-4.5`, (_, {typescript, useProgram}) => { 246 | const bundle = generateCustomTransformerResult( 247 | [ 248 | { 249 | entry: true, 250 | fileName: "index.ts", 251 | text: ` 252 | import {DIContainer} from "@wessberg/di"; 253 | import {Foo, Bar, IFoo} from "./foo"; 254 | console.log(Bar); 255 | 256 | const container = new DIContainer(); 257 | container.registerSingleton(); 258 | ` 259 | }, 260 | { 261 | entry: false, 262 | fileName: "foo.ts", 263 | text: ` 264 | export interface IFoo {} 265 | export class Foo implements IFoo {} 266 | export class Bar {} 267 | ` 268 | } 269 | ], 270 | { 271 | typescript, 272 | useProgram, 273 | compilerOptions: { 274 | preserveValueImports: true 275 | } 276 | } 277 | ); 278 | 279 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 280 | 281 | assert.deepEqual( 282 | formatCode(file.text), 283 | formatCode(`\ 284 | import { DIContainer } from "@wessberg/di"; 285 | import { Foo, Bar } from "./foo"; 286 | console.log(Bar); 287 | const container = new DIContainer(); 288 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 289 | `) 290 | ); 291 | }); 292 | 293 | test("Won't lead to duplicate imports. #1", "*", (_, {typescript, useProgram}) => { 294 | const bundle = generateCustomTransformerResult( 295 | [ 296 | { 297 | entry: true, 298 | fileName: "index.ts", 299 | text: ` 300 | import {DIContainer} from "@wessberg/di"; 301 | import Foo, {IFoo} from "./foo"; 302 | console.log(Foo); 303 | 304 | const container = new DIContainer(); 305 | container.registerSingleton(); 306 | ` 307 | }, 308 | { 309 | entry: false, 310 | fileName: "foo.ts", 311 | text: ` 312 | export interface IFoo {} 313 | export default class Foo implements IFoo {} 314 | ` 315 | } 316 | ], 317 | {typescript, useProgram} 318 | ); 319 | 320 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 321 | 322 | assert.deepEqual( 323 | formatCode(file.text), 324 | formatCode(`\ 325 | import { DIContainer } from "@wessberg/di"; 326 | import Foo from "./foo"; 327 | console.log(Foo); 328 | const container = new DIContainer(); 329 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 330 | `) 331 | ); 332 | }); 333 | -------------------------------------------------------------------------------- /test/setup/setup-custom-transformer.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../src/type/type.js"; 2 | import {di} from "../../src/transformer/di.js"; 3 | import {ensureArray} from "../../src/util/util.js"; 4 | import path from "crosspath"; 5 | import type {DiIsolatedModulesOptions} from "../../src/transformer/di-options.js"; 6 | 7 | export interface ITestFile { 8 | fileName: string; 9 | text: string; 10 | entry: boolean; 11 | } 12 | 13 | export type TestFile = ITestFile | string; 14 | 15 | interface GenerateCustomTransformerResultOptionsShared { 16 | typescript: typeof TS; 17 | cwd?: string; 18 | compilerOptions?: Partial; 19 | stackTraceLength?: number; 20 | } 21 | 22 | interface GenerateCustomTransformerResultOptionsProgram extends GenerateCustomTransformerResultOptionsShared { 23 | useProgram?: true; 24 | } 25 | 26 | interface GenerateCustomTransformerResultOptionsIsolatedModules extends GenerateCustomTransformerResultOptionsShared { 27 | useProgram?: false; 28 | identifier?: DiIsolatedModulesOptions["identifier"]; 29 | } 30 | 31 | export type GenerateCustomTransformerResultOptions = GenerateCustomTransformerResultOptionsProgram | GenerateCustomTransformerResultOptionsIsolatedModules; 32 | 33 | const VIRTUAL_ROOT = "#root"; 34 | const VIRTUAL_SRC = "src"; 35 | const VIRTUAL_DIST = "dist"; 36 | 37 | const BASE_FILES = [ 38 | { 39 | entry: false, 40 | fileName: "../node_modules/@wessberg/di/package.json", 41 | text: ` 42 | { 43 | "name": "@wessberg/di", 44 | "main": "index.js" 45 | "types": "index.d.ts", 46 | "typings": "index.d.ts" 47 | } 48 | ` 49 | }, 50 | { 51 | entry: false, 52 | fileName: "../node_modules/@wessberg/di/index.d.ts", 53 | text: ` 54 | export declare class DIContainer { 55 | registerSingleton(newExpression: unknown, options: unknown): void; 56 | registerSingleton(newExpression: undefined, options: unknown): void; 57 | registerSingleton(newExpression?: unknown | undefined, options?: unknown): void; 58 | registerTransient(newExpression: unknown, options: unknown): void; 59 | registerTransient(newExpression: undefined, options: unknown): void; 60 | registerTransient(newExpression?: unknown | undefined, options?: unknown): void; 61 | get(options?: unknown): T; 62 | has(options?: unknown): boolean; 63 | } 64 | ` 65 | } 66 | ]; 67 | 68 | /** 69 | * Prepares a test 70 | */ 71 | export function generateCustomTransformerResult( 72 | inputFiles: TestFile[] | TestFile, 73 | {typescript, cwd = path.join(process.cwd(), VIRTUAL_ROOT), compilerOptions: inputCompilerOptions, stackTraceLength, ...rest}: GenerateCustomTransformerResultOptions 74 | ): {fileName: string; text: string}[] { 75 | // Optionally set the stack trace length limit 76 | if (stackTraceLength != null) { 77 | Error.stackTraceLimit = stackTraceLength; 78 | } 79 | 80 | const files: ITestFile[] = [...BASE_FILES, ...ensureArray(inputFiles)] 81 | .map(file => 82 | typeof file === "string" 83 | ? { 84 | text: file, 85 | fileName: `auto-generated-${Math.floor(Math.random() * 100000)}.ts`, 86 | entry: true 87 | } 88 | : file 89 | ) 90 | .map(file => ({ 91 | ...file, 92 | fileName: path.native.join(cwd, VIRTUAL_SRC, file.fileName) 93 | })); 94 | 95 | const entryFile = files.find(file => file.entry); 96 | if (entryFile == null) { 97 | throw new ReferenceError(`No entry could be found`); 98 | } 99 | 100 | const outputFiles: {fileName: string; text: string}[] = []; 101 | 102 | const fileSystem = { 103 | readFile: (fileName: string): string | undefined => { 104 | const normalized = path.native.join(fileName); 105 | const matchedFile = files.find(currentFile => path.native.normalize(currentFile.fileName) === normalized); 106 | 107 | return matchedFile == null ? undefined : matchedFile.text; 108 | }, 109 | fileExists: (fileName: string): boolean => { 110 | const normalized = path.native.normalize(fileName); 111 | return files.some(currentFile => currentFile.fileName === normalized); 112 | }, 113 | 114 | directoryExists: (dirName: string): boolean => { 115 | const normalized = path.native.normalize(dirName); 116 | return ( 117 | files.some(file => path.native.dirname(file.fileName) === normalized || path.native.dirname(file.fileName).startsWith(path.native.normalize(`${normalized}/`))) || 118 | typescript.sys.directoryExists(dirName) 119 | ); 120 | } 121 | }; 122 | 123 | /** 124 | * Gets a ScriptKind from the given path 125 | */ 126 | const getScriptKindFromPath = (p: string): TS.ScriptKind => { 127 | if (p.endsWith(".js")) { 128 | return typescript.ScriptKind.JS; 129 | } else if (p.endsWith(".ts")) { 130 | return typescript.ScriptKind.TS; 131 | } else if (p.endsWith(".tsx")) { 132 | return typescript.ScriptKind.TSX; 133 | } else if (p.endsWith(".jsx")) { 134 | return typescript.ScriptKind.JSX; 135 | } else if (p.endsWith(".json")) { 136 | return typescript.ScriptKind.JSON; 137 | } else { 138 | return typescript.ScriptKind.Unknown; 139 | } 140 | }; 141 | 142 | const compilerOptions: TS.CompilerOptions = { 143 | module: typescript.ModuleKind.ESNext, 144 | target: typescript.ScriptTarget.ESNext, 145 | allowJs: true, 146 | sourceMap: false, 147 | outDir: path.join(cwd, VIRTUAL_DIST), 148 | rootDir: path.normalize(cwd), 149 | // eslint-disable-next-line @typescript-eslint/no-deprecated, @typescript-eslint/naming-convention 150 | moduleResolution: (typescript.ModuleResolutionKind as {NodeNext?: TS.ModuleResolutionKind}).NodeNext ?? typescript.ModuleResolutionKind.NodeJs, 151 | ...inputCompilerOptions 152 | }; 153 | 154 | const program = typescript.createProgram({ 155 | rootNames: files.map(file => path.normalize(file.fileName)), 156 | options: compilerOptions, 157 | host: { 158 | ...fileSystem, 159 | writeFile: () => { 160 | // This is a noop 161 | }, 162 | 163 | getSourceFile(fileName: string, languageVersion: TS.ScriptTarget): TS.SourceFile | undefined { 164 | const normalized = path.normalize(fileName); 165 | const sourceText = this.readFile(fileName); 166 | 167 | if (sourceText == null) return undefined; 168 | 169 | return typescript.createSourceFile(normalized, sourceText, languageVersion, true, getScriptKindFromPath(normalized)); 170 | }, 171 | 172 | getCurrentDirectory() { 173 | return path.native.normalize(cwd); 174 | }, 175 | 176 | getDirectories(directoryName: string) { 177 | return typescript.sys.getDirectories(directoryName).map(name => path.native.normalize(name)); 178 | }, 179 | 180 | getDefaultLibFileName(options: TS.CompilerOptions): string { 181 | return typescript.getDefaultLibFileName(options); 182 | }, 183 | 184 | getCanonicalFileName(fileName: string): string { 185 | return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); 186 | }, 187 | 188 | getNewLine(): string { 189 | return typescript.sys.newLine; 190 | }, 191 | 192 | useCaseSensitiveFileNames() { 193 | return typescript.sys.useCaseSensitiveFileNames; 194 | }, 195 | 196 | realpath(p: string): string { 197 | return path.native.normalize(p); 198 | } 199 | } 200 | }); 201 | 202 | const transformers = 203 | "identifier" in rest && rest.identifier != null 204 | ? di({typescript, compilerOptions, identifier: rest.identifier}) 205 | : Boolean(rest.useProgram) 206 | ? di({typescript, program}) 207 | : di({typescript, compilerOptions}); 208 | 209 | program.emit( 210 | undefined, 211 | (fileName, text) => { 212 | outputFiles.push({fileName, text}); 213 | }, 214 | undefined, 215 | undefined, 216 | transformers 217 | ); 218 | 219 | return outputFiles; 220 | } 221 | -------------------------------------------------------------------------------- /test/setup/setup-transform.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../src/type/type.js"; 2 | import type {DiIsolatedModulesOptions} from "../../src/transformer/di-options.js"; 3 | import {transform} from "../../src/transformer/transform.js"; 4 | import type {TransformResult} from "../../src/transformer/transform-options.js"; 5 | 6 | export interface ITestFile { 7 | fileName: string; 8 | text: string; 9 | } 10 | 11 | export type TestFile = ITestFile | string; 12 | 13 | interface GenerateTransformResultOptions { 14 | typescript: typeof TS; 15 | compilerOptions?: Partial; 16 | stackTraceLength?: number; 17 | identifier?: DiIsolatedModulesOptions["identifier"]; 18 | } 19 | 20 | interface TestTransformResult extends TransformResult { 21 | filename: string; 22 | } 23 | 24 | /** 25 | * Prepares a test 26 | */ 27 | export function generateTransformResult( 28 | input: TestFile, 29 | {typescript, compilerOptions: inputCompilerOptions, stackTraceLength, identifier}: GenerateTransformResultOptions 30 | ): TestTransformResult { 31 | // Optionally set the stack trace length limit 32 | if (stackTraceLength != null) { 33 | Error.stackTraceLimit = stackTraceLength; 34 | } 35 | 36 | const files: ITestFile[] = [input].map(file => 37 | typeof file === "string" 38 | ? { 39 | text: file, 40 | fileName: `auto-generated-${Math.floor(Math.random() * 100000)}.ts`, 41 | entry: true 42 | } 43 | : file 44 | ); 45 | 46 | const [entryFile] = files; 47 | if (entryFile == null) { 48 | throw new ReferenceError("No entry file provided"); 49 | } 50 | 51 | const compilerOptions: TS.CompilerOptions = { 52 | module: typescript.ModuleKind.ESNext, 53 | target: typescript.ScriptTarget.ESNext, 54 | sourceMap: false, 55 | // eslint-disable-next-line @typescript-eslint/no-deprecated, @typescript-eslint/naming-convention 56 | moduleResolution: (typescript.ModuleResolutionKind as {NodeNext?: TS.ModuleResolutionKind}).NodeNext ?? typescript.ModuleResolutionKind.NodeJs, 57 | ...inputCompilerOptions 58 | }; 59 | 60 | return { 61 | filename: entryFile.fileName, 62 | ...transform(entryFile.text, entryFile.fileName, { 63 | typescript, 64 | compilerOptions, 65 | identifier 66 | }) 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /test/transform.test.ts: -------------------------------------------------------------------------------- 1 | import path from "crosspath"; 2 | import {generateTransformResult} from "./setup/setup-transform.js"; 3 | import {formatCode} from "./util/format-code.js"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("The transform API goes from TypeScript to TypeScript. #1", "*", (_, {typescript}) => { 8 | const {code} = generateTransformResult( 9 | `import {DIContainer} from "@wessberg/di"; 10 | import Foo, {IFoo} from "./foo"; 11 | 12 | const container = new DIContainer(); 13 | container.registerSingleton();`, 14 | { 15 | typescript, 16 | compilerOptions: { 17 | sourceMap: false 18 | } 19 | } 20 | ); 21 | 22 | assert.deepEqual( 23 | formatCode(code), 24 | formatCode(`\ 25 | import { DIContainer } from "@wessberg/di"; 26 | import Foo, { IFoo } from "./foo"; 27 | const container = new DIContainer(); 28 | container.registerSingleton(undefined, { 29 | identifier: \`IFoo\`, 30 | implementation: Foo, 31 | });`) 32 | ); 33 | }); 34 | 35 | test("The transform API goes from TypeScript to TypeScript. #2", "*", (_, {typescript}) => { 36 | const {code, map, filename} = generateTransformResult( 37 | `import {DIContainer} from "@wessberg/di"; 38 | import Foo, {IFoo} from "./foo"; 39 | 40 | const container = new DIContainer(); 41 | container.registerSingleton();`, 42 | { 43 | typescript, 44 | compilerOptions: { 45 | sourceMap: true 46 | } 47 | } 48 | ); 49 | 50 | assert.deepEqual( 51 | formatCode(code), 52 | formatCode(`\ 53 | import { DIContainer } from "@wessberg/di"; 54 | import Foo, { IFoo } from "./foo"; 55 | const container = new DIContainer(); 56 | container.registerSingleton(undefined, { 57 | identifier: \`IFoo\`, 58 | implementation: Foo, 59 | }); 60 | //# sourceMappingURL=${filename}.map`) 61 | ); 62 | 63 | assert(map != null); 64 | }); 65 | 66 | test("The transform API goes from TypeScript to TypeScript. #3", "*", (_, {typescript}) => { 67 | const {code} = generateTransformResult( 68 | { 69 | fileName: "file.ts", 70 | text: `import {DIContainer} from "@wessberg/di"; 71 | import Foo, {IFoo} from "./foo"; 72 | 73 | const container = new DIContainer(); 74 | container.registerSingleton();` 75 | }, 76 | { 77 | typescript, 78 | compilerOptions: { 79 | sourceMap: true, 80 | inlineSourceMap: true 81 | } 82 | } 83 | ); 84 | 85 | assert.deepEqual( 86 | formatCode(code), 87 | formatCode(`\ 88 | import { DIContainer } from "@wessberg/di"; 89 | import Foo, { IFoo } from "./foo"; 90 | const container = new DIContainer(); 91 | container.registerSingleton(undefined, { 92 | identifier: \`IFoo\`, 93 | implementation: Foo, 94 | }); 95 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZS50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImZpbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFDLFdBQVcsRUFBQyxNQUFNLGNBQWMsQ0FBQztBQUN2QyxPQUFPLEdBQUcsRUFBRSxFQUFDLElBQUksRUFBQyxNQUFNLE9BQU8sQ0FBQztBQUVoQyxNQUFNLFNBQVMsR0FBRyxJQUFJLFdBQVcsRUFBRSxDQUFDO0FBQ3BDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLEVBQUUsR0FBRyx5REFBRyxDQUFDIn0=`) 96 | ); 97 | }); 98 | 99 | test("The transform API goes from TypeScript to TypeScript. #4", "*", (_, {typescript}) => { 100 | const {code, map, filename} = generateTransformResult( 101 | { 102 | fileName: `C:/foo/bar/baz.ts`, 103 | text: `import {DIContainer} from "@wessberg/di"; 104 | import Foo, {IFoo} from "./foo"; 105 | 106 | const container = new DIContainer(); 107 | container.registerSingleton();` 108 | }, 109 | { 110 | typescript, 111 | compilerOptions: { 112 | sourceMap: true 113 | } 114 | } 115 | ); 116 | 117 | assert.deepEqual( 118 | formatCode(code), 119 | formatCode(`\ 120 | import { DIContainer } from "@wessberg/di"; 121 | import Foo, { IFoo } from "./foo"; 122 | const container = new DIContainer(); 123 | container.registerSingleton(undefined, { 124 | identifier: \`IFoo\`, 125 | implementation: Foo, 126 | }); 127 | //# sourceMappingURL=${path.basename(filename)}.map`) 128 | ); 129 | 130 | assert(map != null); 131 | }); 132 | 133 | test("The transform API allows JSX code. #1", "*", (_, {typescript}) => { 134 | const {code} = generateTransformResult( 135 | { 136 | fileName: "file.tsx", 137 | text: `import {IFoo} from "./foo"; 138 | 139 | const foo = container.get(); 140 | 141 | return
{foo.name}
;` 142 | }, 143 | { 144 | typescript, 145 | identifier: "container" 146 | } 147 | ); 148 | 149 | assert.deepEqual( 150 | formatCode(code), 151 | formatCode(`\ 152 | import { IFoo } from "./foo"; 153 | const foo = container.get({ identifier: "IFoo" }); 154 | return
{foo.name}
;`) 155 | ); 156 | }); 157 | 158 | test("The transform API allows JSX code. #2", "*", (_, {typescript}) => { 159 | const {code} = generateTransformResult( 160 | { 161 | fileName: "file.ts", 162 | text: `import {IFoo} from "./foo"; 163 | 164 | const foo = container.get(); 165 | 166 | return
{foo.name}
;` 167 | }, 168 | { 169 | typescript, 170 | identifier: "container" 171 | } 172 | ); 173 | 174 | assert.throws(() => formatCode(code)); 175 | }); 176 | -------------------------------------------------------------------------------- /test/umd.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {formatCode} from "./util/format-code.js"; 3 | import {includeEmitHelper} from "./util/include-emit-helper.js"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "assert"; 6 | 7 | test("UMD => Preserves Type-only imports. #1", "*", (_, {typescript, useProgram}) => { 8 | const bundle = generateCustomTransformerResult( 9 | [ 10 | { 11 | entry: true, 12 | fileName: "index.ts", 13 | text: ` 14 | import {DIContainer} from "@wessberg/di"; 15 | import Foo, {IFoo} from "./foo"; 16 | console.log(Foo); 17 | 18 | const container = new DIContainer(); 19 | container.registerSingleton(); 20 | ` 21 | }, 22 | { 23 | entry: false, 24 | fileName: "foo.ts", 25 | text: ` 26 | export interface IFoo {} 27 | export default class Foo implements IFoo {} 28 | ` 29 | } 30 | ], 31 | { 32 | typescript, 33 | useProgram, 34 | compilerOptions: { 35 | module: typescript.ModuleKind.UMD 36 | } 37 | } 38 | ); 39 | 40 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 41 | 42 | assert.deepEqual( 43 | formatCode(file.text), 44 | formatCode(`\ 45 | (function (factory) { 46 | if (typeof module === "object" && typeof module.exports === "object") { 47 | var v = factory(require, exports); 48 | if (v !== undefined) module.exports = v; 49 | } 50 | else if (typeof define === "function" && define.amd) { 51 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 52 | } 53 | })(function (require, exports) { 54 | "use strict"; 55 | Object.defineProperty(exports, "__esModule", { value: true }); 56 | const Foo = require("./foo"); 57 | const di_1 = require("@wessberg/di"); 58 | const foo_1 = require("./foo"); 59 | console.log(foo_1.default); 60 | const container = new di_1.DIContainer(); 61 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 62 | }); 63 | 64 | `) 65 | ); 66 | }); 67 | 68 | test("UMD => Preserves type-only imports. #2", "*", (_, {typescript, useProgram}) => { 69 | const bundle = generateCustomTransformerResult( 70 | [ 71 | { 72 | entry: true, 73 | fileName: "index.ts", 74 | text: ` 75 | import {DIContainer} from "@wessberg/di"; 76 | import Foo, {IFoo} from "./foo"; 77 | 78 | const container = new DIContainer(); 79 | container.registerSingleton(); 80 | ` 81 | }, 82 | { 83 | entry: false, 84 | fileName: "foo.ts", 85 | text: ` 86 | export interface IFoo {} 87 | export default class Foo implements IFoo {} 88 | ` 89 | } 90 | ], 91 | { 92 | typescript, 93 | useProgram, 94 | compilerOptions: { 95 | module: typescript.ModuleKind.UMD 96 | } 97 | } 98 | ); 99 | 100 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 101 | 102 | assert.deepEqual( 103 | formatCode(file.text), 104 | formatCode(`\ 105 | (function (factory) { 106 | if (typeof module === "object" && typeof module.exports === "object") { 107 | var v = factory(require, exports); 108 | if (v !== undefined) module.exports = v; 109 | } 110 | else if (typeof define === "function" && define.amd) { 111 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 112 | } 113 | })(function (require, exports) { 114 | "use strict"; 115 | Object.defineProperty(exports, "__esModule", { value: true }); 116 | const Foo = require("./foo"); 117 | const di_1 = require("@wessberg/di"); 118 | const container = new di_1.DIContainer(); 119 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 120 | }); 121 | `) 122 | ); 123 | }); 124 | 125 | test("UMD => Preserves type-only imports. #3", "*", (_, {typescript, useProgram}) => { 126 | const bundle = generateCustomTransformerResult( 127 | [ 128 | { 129 | entry: true, 130 | fileName: "index.ts", 131 | text: ` 132 | import {DIContainer} from "@wessberg/di"; 133 | import {Foo, IFoo} from "./foo"; 134 | 135 | const container = new DIContainer(); 136 | container.registerSingleton(); 137 | ` 138 | }, 139 | { 140 | entry: false, 141 | fileName: "foo.ts", 142 | text: ` 143 | export interface IFoo {} 144 | export class Foo implements IFoo {} 145 | ` 146 | } 147 | ], 148 | { 149 | typescript, 150 | useProgram, 151 | compilerOptions: { 152 | module: typescript.ModuleKind.UMD 153 | } 154 | } 155 | ); 156 | 157 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 158 | 159 | assert.deepEqual( 160 | formatCode(file.text), 161 | formatCode(`\ 162 | (function (factory) { 163 | if (typeof module === "object" && typeof module.exports === "object") { 164 | var v = factory(require, exports); 165 | if (v !== undefined) module.exports = v; 166 | } 167 | else if (typeof define === "function" && define.amd) { 168 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 169 | } 170 | })(function (require, exports) { 171 | "use strict"; 172 | Object.defineProperty(exports, "__esModule", { value: true }); 173 | const Foo = require("./foo"); 174 | const di_1 = require("@wessberg/di"); 175 | const container = new di_1.DIContainer(); 176 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 177 | }); 178 | `) 179 | ); 180 | }); 181 | 182 | test("UMD => Preserves type-only imports. #4", "*", (_, {typescript, useProgram}) => { 183 | const bundle = generateCustomTransformerResult( 184 | [ 185 | { 186 | entry: true, 187 | fileName: "index.ts", 188 | text: ` 189 | import {DIContainer} from "@wessberg/di"; 190 | import * as Foo from "./foo"; 191 | import {IFoo} from "./foo"; 192 | 193 | const container = new DIContainer(); 194 | container.registerSingleton(); 195 | ` 196 | }, 197 | { 198 | entry: false, 199 | fileName: "foo.ts", 200 | text: ` 201 | export interface IFoo {} 202 | export class Foo implements IFoo {} 203 | ` 204 | } 205 | ], 206 | { 207 | typescript, 208 | useProgram, 209 | compilerOptions: { 210 | module: typescript.ModuleKind.UMD 211 | } 212 | } 213 | ); 214 | 215 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 216 | 217 | assert.deepEqual( 218 | formatCode(file.text), 219 | formatCode(`\ 220 | (function (factory) { 221 | if (typeof module === "object" && typeof module.exports === "object") { 222 | var v = factory(require, exports); 223 | if (v !== undefined) module.exports = v; 224 | } 225 | else if (typeof define === "function" && define.amd) { 226 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 227 | } 228 | })(function (require, exports) { 229 | "use strict"; 230 | Object.defineProperty(exports, "__esModule", { value: true }); 231 | const Foo = require("./foo"); 232 | const di_1 = require("@wessberg/di"); 233 | const container = new di_1.DIContainer(); 234 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 235 | }); 236 | `) 237 | ); 238 | }); 239 | 240 | test("UMD => Preserves type-only imports. #5", "*", (_, {typescript, useProgram}) => { 241 | const bundle = generateCustomTransformerResult( 242 | [ 243 | { 244 | entry: true, 245 | fileName: "index.ts", 246 | text: ` 247 | import {DIContainer} from "@wessberg/di"; 248 | import {Bar as Foo, IFoo} from "./foo"; 249 | 250 | const container = new DIContainer(); 251 | container.registerSingleton(); 252 | ` 253 | }, 254 | { 255 | entry: false, 256 | fileName: "foo.ts", 257 | text: ` 258 | export interface IFoo {} 259 | export class Bar implements IFoo {} 260 | ` 261 | } 262 | ], 263 | { 264 | typescript, 265 | useProgram, 266 | compilerOptions: { 267 | module: typescript.ModuleKind.UMD 268 | } 269 | } 270 | ); 271 | 272 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 273 | 274 | assert.deepEqual( 275 | formatCode(file.text), 276 | formatCode(`\ 277 | (function (factory) { 278 | if (typeof module === "object" && typeof module.exports === "object") { 279 | var v = factory(require, exports); 280 | if (v !== undefined) module.exports = v; 281 | } 282 | else if (typeof define === "function" && define.amd) { 283 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 284 | } 285 | })(function (require, exports) { 286 | "use strict"; 287 | Object.defineProperty(exports, "__esModule", { value: true }); 288 | const Foo = require("./foo"); 289 | const di_1 = require("@wessberg/di"); 290 | const container = new di_1.DIContainer(); 291 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Bar }); 292 | }); 293 | `) 294 | ); 295 | }); 296 | 297 | test("UMD => Preserves type-only imports. #6", "*", (_, {typescript, useProgram}) => { 298 | const bundle = generateCustomTransformerResult( 299 | [ 300 | { 301 | entry: true, 302 | fileName: "index.ts", 303 | text: ` 304 | import {DIContainer} from "@wessberg/di"; 305 | import {default as Foo, IFoo} from "./foo"; 306 | 307 | const container = new DIContainer(); 308 | container.registerSingleton(); 309 | ` 310 | }, 311 | { 312 | entry: false, 313 | fileName: "foo.ts", 314 | text: ` 315 | export interface IFoo {} 316 | export default class Bar implements IFoo {} 317 | ` 318 | } 319 | ], 320 | { 321 | typescript, 322 | useProgram, 323 | compilerOptions: { 324 | module: typescript.ModuleKind.UMD 325 | } 326 | } 327 | ); 328 | 329 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 330 | 331 | assert.deepEqual( 332 | formatCode(file.text), 333 | formatCode(`\ 334 | (function (factory) { 335 | if (typeof module === "object" && typeof module.exports === "object") { 336 | var v = factory(require, exports); 337 | if (v !== undefined) module.exports = v; 338 | } 339 | else if (typeof define === "function" && define.amd) { 340 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 341 | } 342 | })(function (require, exports) { 343 | "use strict"; 344 | Object.defineProperty(exports, "__esModule", { value: true }); 345 | const Foo = require("./foo"); 346 | const di_1 = require("@wessberg/di"); 347 | const container = new di_1.DIContainer(); 348 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 349 | }); 350 | `) 351 | ); 352 | }); 353 | 354 | test("UMD => Preserves type-only imports. #7", "*", (_, {typescript, useProgram}) => { 355 | const bundle = generateCustomTransformerResult( 356 | [ 357 | { 358 | entry: true, 359 | fileName: "index.ts", 360 | text: ` 361 | import {DIContainer} from "@wessberg/di"; 362 | import {Foo, Bar, IFoo} from "./foo"; 363 | console.log(Bar); 364 | 365 | const container = new DIContainer(); 366 | container.registerSingleton(); 367 | ` 368 | }, 369 | { 370 | entry: false, 371 | fileName: "foo.ts", 372 | text: ` 373 | export interface IFoo {} 374 | export class Foo implements IFoo {} 375 | export class Bar {} 376 | ` 377 | } 378 | ], 379 | { 380 | typescript, 381 | useProgram, 382 | compilerOptions: { 383 | module: typescript.ModuleKind.UMD 384 | } 385 | } 386 | ); 387 | 388 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 389 | 390 | assert.deepEqual( 391 | formatCode(file.text), 392 | formatCode(`\ 393 | (function (factory) { 394 | if (typeof module === "object" && typeof module.exports === "object") { 395 | var v = factory(require, exports); 396 | if (v !== undefined) module.exports = v; 397 | } 398 | else if (typeof define === "function" && define.amd) { 399 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 400 | } 401 | })(function (require, exports) { 402 | "use strict"; 403 | Object.defineProperty(exports, "__esModule", { value: true }); 404 | const Foo = require("./foo"); 405 | const di_1 = require("@wessberg/di"); 406 | const foo_1 = require("./foo"); 407 | console.log(foo_1.Bar); 408 | const container = new di_1.DIContainer(); 409 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.Foo }); 410 | }); 411 | `) 412 | ); 413 | }); 414 | 415 | test("UMD => Preserves type-only imports with esModuleInterop and importHelpers. #1", "*", (_, {typescript, useProgram}) => { 416 | const bundle = generateCustomTransformerResult( 417 | [ 418 | { 419 | entry: true, 420 | fileName: "index.ts", 421 | text: ` 422 | import {DIContainer} from "@wessberg/di"; 423 | import Foo, {IFoo} from "./foo"; 424 | 425 | const container = new DIContainer(); 426 | container.registerSingleton(); 427 | ` 428 | }, 429 | { 430 | entry: false, 431 | fileName: "foo.ts", 432 | text: ` 433 | export interface IFoo {} 434 | export default class Foo implements IFoo {} 435 | ` 436 | } 437 | ], 438 | { 439 | typescript, 440 | useProgram, 441 | compilerOptions: { 442 | esModuleInterop: true, 443 | importHelpers: true, 444 | module: typescript.ModuleKind.UMD 445 | } 446 | } 447 | ); 448 | 449 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 450 | 451 | assert.deepEqual( 452 | formatCode(file.text), 453 | formatCode(`\ 454 | (function (factory) { 455 | if (typeof module === "object" && typeof module.exports === "object") { 456 | var v = factory(require, exports); 457 | if (v !== undefined) module.exports = v; 458 | } 459 | else if (typeof define === "function" && define.amd) { 460 | define(["require", "exports", "@wessberg/di", "./foo", "tslib"], factory); 461 | } 462 | })(function (require, exports) { 463 | "use strict"; 464 | Object.defineProperty(exports, "__esModule", { value: true }); 465 | const Foo = require("tslib").__importDefault(require("./foo")); 466 | const di_1 = require("@wessberg/di"); 467 | const container = new di_1.DIContainer(); 468 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 469 | }); 470 | `) 471 | ); 472 | }); 473 | 474 | test("UMD => Preserves type-only imports with esModuleInterop. #1", "*", (_, {typescript, useProgram}) => { 475 | const bundle = generateCustomTransformerResult( 476 | [ 477 | { 478 | entry: true, 479 | fileName: "index.ts", 480 | text: ` 481 | import {DIContainer} from "@wessberg/di"; 482 | import Foo, {IFoo} from "./foo"; 483 | 484 | const container = new DIContainer(); 485 | container.registerSingleton(); 486 | ` 487 | }, 488 | { 489 | entry: false, 490 | fileName: "foo.ts", 491 | text: ` 492 | export interface IFoo {} 493 | export default class Foo implements IFoo {} 494 | ` 495 | } 496 | ], 497 | { 498 | typescript, 499 | useProgram, 500 | compilerOptions: { 501 | esModuleInterop: true, 502 | module: typescript.ModuleKind.UMD 503 | } 504 | } 505 | ); 506 | 507 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 508 | 509 | assert.deepEqual( 510 | formatCode(file.text), 511 | formatCode(`\ 512 | ${includeEmitHelper(typescript, "__importDefault")} 513 | (function (factory) { 514 | if (typeof module === "object" && typeof module.exports === "object") { 515 | var v = factory(require, exports); 516 | if (v !== undefined) module.exports = v; 517 | } 518 | else if (typeof define === "function" && define.amd) { 519 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 520 | } 521 | })(function (require, exports) { 522 | "use strict"; 523 | Object.defineProperty(exports, "__esModule", { value: true }); 524 | const Foo = __importDefault(require("./foo")); 525 | const di_1 = require("@wessberg/di"); 526 | const container = new di_1.DIContainer(); 527 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 528 | }); 529 | 530 | `) 531 | ); 532 | }); 533 | 534 | test("UMD => Preserves type-only imports with esModuleInterop. #2", "*", (_, {typescript, useProgram}) => { 535 | const bundle = generateCustomTransformerResult( 536 | [ 537 | { 538 | entry: true, 539 | fileName: "index.ts", 540 | text: ` 541 | import {DIContainer} from "@wessberg/di"; 542 | import Foo, {IFoo} from "./foo"; 543 | console.log(Foo); 544 | 545 | const container = new DIContainer(); 546 | container.registerSingleton(); 547 | ` 548 | }, 549 | { 550 | entry: false, 551 | fileName: "foo.ts", 552 | text: ` 553 | export interface IFoo {} 554 | export default class Foo implements IFoo {} 555 | ` 556 | } 557 | ], 558 | { 559 | typescript, 560 | useProgram, 561 | compilerOptions: { 562 | esModuleInterop: true, 563 | module: typescript.ModuleKind.UMD 564 | } 565 | } 566 | ); 567 | 568 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 569 | 570 | assert.deepEqual( 571 | formatCode(file.text), 572 | formatCode(`\ 573 | ${includeEmitHelper(typescript, "__importDefault")} 574 | (function (factory) { 575 | if (typeof module === "object" && typeof module.exports === "object") { 576 | var v = factory(require, exports); 577 | if (v !== undefined) module.exports = v; 578 | } 579 | else if (typeof define === "function" && define.amd) { 580 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 581 | } 582 | })(function (require, exports) { 583 | "use strict"; 584 | Object.defineProperty(exports, "__esModule", { value: true }); 585 | const Foo = __importDefault(require("./foo")); 586 | const di_1 = require("@wessberg/di"); 587 | const foo_1 = __importDefault(require("./foo")); 588 | console.log(foo_1.default); 589 | const container = new di_1.DIContainer(); 590 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo.default }); 591 | }); 592 | 593 | `) 594 | ); 595 | }); 596 | 597 | test("UMD => Preserves type-only imports with esModuleInterop. #3", "*", (_, {typescript, useProgram}) => { 598 | const bundle = generateCustomTransformerResult( 599 | [ 600 | { 601 | entry: true, 602 | fileName: "index.ts", 603 | text: ` 604 | import {DIContainer} from "@wessberg/di"; 605 | import * as Foo from "./foo"; 606 | import {IFoo} from "./foo"; 607 | 608 | const container = new DIContainer(); 609 | container.registerSingleton(); 610 | ` 611 | }, 612 | { 613 | entry: false, 614 | fileName: "foo.ts", 615 | text: ` 616 | export interface IFoo {} 617 | export class Foo implements IFoo {} 618 | ` 619 | } 620 | ], 621 | { 622 | typescript, 623 | useProgram, 624 | compilerOptions: { 625 | esModuleInterop: true, 626 | module: typescript.ModuleKind.UMD 627 | } 628 | } 629 | ); 630 | 631 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 632 | 633 | assert.deepEqual( 634 | formatCode(file.text), 635 | formatCode(`\ 636 | ${includeEmitHelper(typescript, "__importStar")} 637 | (function (factory) { 638 | if (typeof module === "object" && typeof module.exports === "object") { 639 | var v = factory(require, exports); 640 | if (v !== undefined) module.exports = v; 641 | } 642 | else if (typeof define === "function" && define.amd) { 643 | define(["require", "exports", "@wessberg/di", "./foo"], factory); 644 | } 645 | })(function (require, exports) { 646 | "use strict"; 647 | Object.defineProperty(exports, "__esModule", { value: true }); 648 | const Foo = __importStar(require("./foo")); 649 | const di_1 = require("@wessberg/di"); 650 | const container = new di_1.DIContainer(); 651 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 652 | }); 653 | `) 654 | ); 655 | }); 656 | -------------------------------------------------------------------------------- /test/util/format-code.ts: -------------------------------------------------------------------------------- 1 | import prettier from "@prettier/sync"; 2 | 3 | export function formatCode(code: string): string { 4 | return prettier.format(code, {parser: "typescript", endOfLine: "lf"}); 5 | } 6 | -------------------------------------------------------------------------------- /test/util/include-emit-helper.ts: -------------------------------------------------------------------------------- 1 | import type {TS} from "../../src/type/type.js"; 2 | import {getImportDefaultHelper, getImportStarHelper} from "../../src/util/ts-util.js"; 3 | 4 | export function includeEmitHelper(typescript: typeof TS, helperName: "__importStar" | "__importDefault"): string { 5 | const helper = helperName === "__importStar" ? getImportStarHelper(typescript) : getImportDefaultHelper(typescript); 6 | 7 | let str = ""; 8 | for (const dependency of helper.dependencies ?? []) { 9 | str += dependency.text as string; 10 | } 11 | str += helper.text as string; 12 | if (str.startsWith("\n")) { 13 | return str.slice(1); 14 | } else { 15 | return str; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/util/test-runner.ts: -------------------------------------------------------------------------------- 1 | import path from "crosspath"; 2 | import fs from "fs"; 3 | import semver from "semver"; 4 | import testModule, {type TestContext} from "node:test"; 5 | import type * as TS from "typescript"; 6 | 7 | function getNearestPackageJson(from = import.meta.url): Record | undefined { 8 | // There may be a file protocol in from of the path 9 | const normalizedFrom = path.urlToFilename(from); 10 | const currentDir = path.dirname(normalizedFrom); 11 | 12 | const pkgPath = path.join(currentDir, "package.json"); 13 | if (fs.existsSync(pkgPath)) { 14 | return JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record; 15 | } else if (currentDir !== normalizedFrom) { 16 | return getNearestPackageJson(currentDir); 17 | } else { 18 | return undefined; 19 | } 20 | } 21 | 22 | const pkg = getNearestPackageJson(); 23 | 24 | export interface ExecutionContextOptions { 25 | typescript: typeof TS; 26 | typescriptModuleSpecifier: string; 27 | typescriptVersion: string; 28 | useProgram: boolean; 29 | } 30 | 31 | export type ExtendedImplementation = (t: TestContext, options: ExecutionContextOptions) => void | Promise; 32 | 33 | const {devDependencies} = pkg as {devDependencies: Record}; 34 | 35 | // Set of all TypeScript versions parsed from package.json 36 | const availableTsVersions = new Set(); 37 | const TS_OPTIONS_RECORDS = new Map(); 38 | 39 | const tsRangeRegex = /(npm:typescript@)?[\^~]*(.+)$/; 40 | const filter = process.env.TS_VERSION; 41 | 42 | for (const [specifier, range] of Object.entries(devDependencies)) { 43 | const match = range.match(tsRangeRegex); 44 | if (match !== null) { 45 | const [, context, version] = match; 46 | if (version != null && (context === "npm:typescript@" || specifier === "typescript")) { 47 | availableTsVersions.add(version); 48 | if (filter === undefined || (filter.toUpperCase() === "CURRENT" && specifier === "typescript") || semver.satisfies(version, filter, {includePrerelease: true})) { 49 | const typescript = ((await import(specifier)) as {default: typeof TS}).default; 50 | TS_OPTIONS_RECORDS.set(version, { 51 | typescript, 52 | typescriptModuleSpecifier: specifier, 53 | typescriptVersion: version, 54 | useProgram: false 55 | }); 56 | } 57 | } 58 | } 59 | } 60 | 61 | if (availableTsVersions.size === 0) { 62 | throw new Error(`The TS_VERSION environment variable matches none of the available TypeScript versions. 63 | Filter: ${process.env.TS_VERSION} 64 | Available TypeScript versions: ${[...availableTsVersions].join(", ")}`); 65 | } 66 | 67 | interface TestRunOptions { 68 | only: boolean; 69 | } 70 | 71 | export function test(title: string, tsVersionGlob: string | undefined, impl: ExtendedImplementation, runOptions?: Partial): void { 72 | const allOptions = 73 | tsVersionGlob == null || tsVersionGlob === "*" 74 | ? [...TS_OPTIONS_RECORDS.values()] 75 | : [...TS_OPTIONS_RECORDS.entries()].filter(([version]) => semver.satisfies(version, tsVersionGlob, {includePrerelease: true})).map(([, options]) => options); 76 | 77 | for (const useProgram of [true, false]) { 78 | for (const currentOptions of allOptions) { 79 | const nextCurrentOptions = {...currentOptions, useProgram}; 80 | const fullTitle = `${title} (TypeScript v${nextCurrentOptions.typescriptVersion}) (Use Program: ${useProgram})`; 81 | 82 | if (Boolean(runOptions?.only)) { 83 | testModule(fullTitle, {only: true}, async t => impl(t, nextCurrentOptions)); 84 | } else { 85 | testModule(fullTitle, async t => impl(t, nextCurrentOptions)); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/verbatim-module-syntax.test.ts: -------------------------------------------------------------------------------- 1 | import {generateCustomTransformerResult} from "./setup/setup-custom-transformer.js"; 2 | import {generateTransformResult} from "./setup/setup-transform.js"; 3 | import {formatCode} from "./util/format-code.js"; 4 | import {test} from "./util/test-runner.js"; 5 | import assert from "node:assert"; 6 | 7 | test("Relevant type-only imports for DIContainer are preserved, but replaced, under verbatim module syntax. #1", ">3.7", (_, {typescript}) => { 8 | const {code} = generateTransformResult( 9 | `import {DIContainer} from "@wessberg/di"; 10 | import type {IFoo} from "./foo"; 11 | 12 | const container = new DIContainer(); 13 | container.get();`, 14 | { 15 | typescript, 16 | compilerOptions: { 17 | sourceMap: false, 18 | verbatimModuleSyntax: true 19 | } 20 | } 21 | ); 22 | 23 | assert.deepEqual( 24 | formatCode(code), 25 | formatCode(`\ 26 | import { DIContainer } from "@wessberg/di"; 27 | import type { IFoo } from "./foo"; 28 | const container = new DIContainer(); 29 | container.get({ identifier: "IFoo" }); 30 | `) 31 | ); 32 | }); 33 | 34 | test("Relevant type-only imports for DIContainer are preserved, but replaced, under verbatim module syntax. #2", ">3.7", (_, {typescript, useProgram}) => { 35 | const bundle = generateCustomTransformerResult( 36 | [ 37 | { 38 | entry: true, 39 | fileName: "index.ts", 40 | text: ` 41 | import {DIContainer} from "@wessberg/di"; 42 | import type {IFoo, Foo} from "./foo"; 43 | 44 | const container = new DIContainer(); 45 | container.registerSingleton(); 46 | ` 47 | }, 48 | { 49 | entry: false, 50 | fileName: "foo.ts", 51 | text: ` 52 | export interface IFoo {} 53 | export class Foo implements IFoo {} 54 | ` 55 | } 56 | ], 57 | { 58 | typescript, 59 | useProgram, 60 | compilerOptions: { 61 | sourceMap: false, 62 | verbatimModuleSyntax: true 63 | } 64 | } 65 | ); 66 | const file = bundle.find(({fileName}) => fileName.includes("index.js"))!; 67 | 68 | console.log(formatCode(file.text)); 69 | 70 | assert.deepEqual( 71 | formatCode(file.text), 72 | formatCode(`\ 73 | import { Foo } from "./foo"; 74 | import { DIContainer } from "@wessberg/di"; 75 | const container = new DIContainer(); 76 | container.registerSingleton(undefined, { identifier: \`IFoo\`, implementation: Foo }); 77 | `) 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@wessberg/ts-config/tsconfig.json", 3 | "include": ["src/**/*.*", "test/**/*.*", "loader.cjs", "sandhog.config.js"], 4 | "exclude": ["dist/*.*"], 5 | "compilerOptions": { 6 | "importHelpers": false 7 | } 8 | } 9 | --------------------------------------------------------------------------------