├── .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 |
--------------------------------------------------------------------------------