Skip to content

Commit bc26539

Browse files
committed
init
0 parents  commit bc26539

32 files changed

Lines changed: 1807 additions & 0 deletions

README.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Vite TS Monorepo Setup Comparison
2+
3+
This repo contains two different approaches to setup up a TypeScript monorepo with Vite: one using [tsconfig paths](https://www.typescriptlang.org/tsconfig#paths), and one using [custom conditions](https://nodejs.org/api/packages.html#community-conditions-definitions) in `package.json` exports.
4+
5+
> ⚠️ Note: the setups in this repo are only for comparing / discussing different approaches and are not meant to be production-ready references.
6+
7+
Both setups simulate a simple case of a Vite + TS monorepo where:
8+
9+
- `packages/lib` is a component / utility library that also published to internal or public registries.
10+
- `packages/app` is a Vite application that uses `lib` as a dependency.
11+
12+
`lib` is linked into `app` via pnpm workspace.
13+
14+
As users, our goal is to get:
15+
16+
- Type inference without having to rebuild the `d.ts` file of `lib`.
17+
- Vite HMR when working on `app`, but editing TS source files in `lib`.
18+
19+
Because `lib` is a published package, its `exports` conditions point to the bundled `.js` and `.d.ts` files in its `dist` directory. In order to achieve our goals, we want both TS and Vite to resolve to the source TS files of `lib` instead of its dist files.
20+
21+
## Approach 1: Monorepo setup using tsconfig paths
22+
23+
In `tsconfig.json`:
24+
25+
```json
26+
{
27+
"compilerOptions": {
28+
// ...
29+
"paths": {
30+
"@test/lib": ["packages/lib/src"],
31+
// if need deep imports:
32+
"@test/lib/*": ["packages/lib/src/*"]
33+
}
34+
}
35+
}
36+
```
37+
38+
In `app`'s Vite config:
39+
40+
```js
41+
export default defineConfig({
42+
resolve: {
43+
alias: {
44+
'@test/lib': fileURLToPath(new URL('../lib/src', import.meta.url))
45+
}
46+
}
47+
})
48+
```
49+
50+
### Pros
51+
52+
- Centralized config: each package's `package.json` is only concerned with its public-facing exports.
53+
54+
- Flexible match: can resolve arbitrary extension types - e.g. an extension-less import can resolve to both `.ts` and `.tsx` files. Also covers non-TS types like Vue / Svelte SFCs.
55+
56+
### Cons
57+
58+
- Currently requires duplicated Vite `resolve.alias` config. This config needs to repeated in every package that consumes `lib`, and all of them needs to be updated when new packages need to be aliased.
59+
60+
Many users are using [vite-tsconfig-paths](https://www.npmjs.com/package/vite-tsconfig-paths) to simplify this. This plugin has 1.74m weekly downloads (~15% of Vite downloads). Remix uses this plugin by default in its scaffolded projects. Nx also uses a similar [nxViteTsPaths plugin](https://nx.dev/recipes/vite/configure-vite#typescript-paths) by default.
61+
62+
If Vite provides built-in support that automatically respect tsconfig paths as aliases, then `app` doesn't even need a Vite config file in this case.
63+
64+
- By making `tsconfig` paths the source of the truth, we expect other tools that need to work with source files also use it as the source of truth, or there will still be duplicated alias configurations required.
65+
66+
## Approach 2: Monorepo setup using custom `source` exports condition
67+
68+
In `tsconfig.json`:
69+
70+
```json
71+
{
72+
"compilerOptions": {
73+
"customConditions": ["source"]
74+
}
75+
}
76+
```
77+
78+
In `app`'s Vite config:
79+
80+
```js
81+
export default defineConfig({
82+
resolve: {
83+
conditions: ['source']
84+
}
85+
})
86+
```
87+
88+
### Pros
89+
90+
- Leverages `package.json` exports, which is widely supported across tools and runtimes. Compatible with any tool that supports configuring additional resolve conditions.
91+
92+
- Avoids letting `tsconfig` becoming the source of truth, which conflicts with its original intention.
93+
94+
- Same Vite config for every package.
95+
96+
### Cons
97+
98+
1. For published packages, it might be desirable to avoid exposing the source files to reduce package size. To achieve this, users will need to strip the `source` condition from `package.json` before publishing.
99+
100+
2. To support arbitrary deep imports, [subpath patterns](https://nodejs.org/api/packages.html#subpath-patterns) can be used:
101+
102+
```json
103+
{
104+
"exports": {
105+
// ...
106+
"./*": {
107+
"source": "./src/*.ts"
108+
}
109+
}
110+
}
111+
```
112+
113+
**The problem arises when the user has a codebase with mixed `.ts` and `.tsx` files, and want to use extension-less imports for both.**
114+
115+
TypeScript (when using `moduleResolution: "bundler"`) and webpack does support using an array for extension fallbacks:
116+
117+
```json
118+
{
119+
"exports": {
120+
// this works as fallbacks in TS & webpack but NOT in Node.js
121+
"./*": {
122+
"source": ["./src/*.ts", "./src/*.tsx"]
123+
}
124+
}
125+
}
126+
```
127+
128+
In this case, both TS and webpack will try the entries in the array until a match is found. However, Node.js **stops at the first valid filename regardless of whether the file exists or not**. [More context here](https://github.com/bluwy/publint/issues/92).
129+
130+
The reason for Node.js' behavior is explained [here](https://github.com/nodejs/node/issues/37928#issuecomment-808833604) and [here](https://github.com/nodejs/node/issues/44282#issuecomment-1220151715) - TL;DR - Node wants to avoid hitting the file system when resolving exports.
131+
132+
Vite currently [aligns with Node.js behavior](https://github.com/vitejs/vite/issues/4439#issuecomment-1465224035) and will only use the first valid path and error if the file does not exist. This poses a blocker for users who want to use exports conditions with deep, extension-less ts/tsx imports.
133+
134+
There are two workarounds:
135+
136+
1. Using a separate subpath pattern for tsx:
137+
138+
```json
139+
{
140+
"exports": {
141+
"./*.tsx": {
142+
"source": "./src/*.tsx"
143+
},
144+
"./*": {
145+
"source": "./src/*.ts"
146+
}
147+
}
148+
}
149+
```
150+
151+
This would require using explicit extensions when importing tsx files:
152+
153+
```diff
154+
- import { bar } from 'lib/src/bar'
155+
+ import { bar } from 'lib/src/bar.tsx'
156+
```
157+
158+
2. Manually list all tsx subpath exports:
159+
160+
```json
161+
{
162+
"exports": {
163+
"./bar": {
164+
"source": "./src/bar.tsx"
165+
},
166+
"./*": {
167+
"source": "./src/*.ts"
168+
}
169+
}
170+
}
171+
```
172+
173+
This is tedious and error-prone.
174+
175+
Neither workaround is ideal.
176+
177+
3. For deep imports of other file extensions, users will also have to specify exports for all additionally exported file types. This is a less common use case, but when needed, it can be cumbersome and need to be repeated in every package:
178+
179+
```json
180+
{
181+
"exports": {
182+
"./*.tsx": {
183+
"source": "./src/*.tsx"
184+
},
185+
"./*.vue": {
186+
"source": "./src/*.vue"
187+
},
188+
"./*.css": {
189+
"source": "./src/*.css"
190+
},
191+
"./*": {
192+
"source": "./src/*.ts"
193+
}
194+
}
195+
}
196+
```
197+
198+
When using tsconfig paths, this is handled automatically.
199+
200+
## Action Paths for Vite
201+
202+
Ideally, we want to provide the users with a "recommended" approach for setting up TS monorepos, but currently both approaches have some DX paper cuts that need to be addressed.
203+
204+
1. If we were to recommend custom condition in `exports` field:
205+
206+
- We either make Vite support array fallbacks for conditional exports (This aligns with TS but deviates from Node.js), or users will have to use explicit extensions for deep imports of `.tsx` files.
207+
208+
- Users who don't want to expose source files in public packages will need an easy way to strip the source conditions from `package.json` before publishing. Currently users have to do this via `prepublish` and `prepublish` scripts. Ideally this can be something that package managers can support via `.npmrc`.
209+
210+
2. If we were to recommend `tsconfig` paths:
211+
212+
- We should provide built-in support to automatically generate aliases based on `tsconfig` paths.
213+
214+
- Performance concerns
215+
216+
There are performance concerns for this, but Vite already has to perform tsconfig resolution internally, so having it built-in should result in better performance than using external plugins like `vite-tsconfig-paths`.
217+
218+
In the future, Vite will also likely rely on `oxc_resolver` and Rolldown which provides built-in tsconfig paths support with native performance.
219+
220+
- Alignment concerns
221+
222+
TS team has expressed that they do not like how tsconfig paths are being widely used because it creates misalignment between TS resolution and standard Node.js behavior.
223+
224+
However, in my opinion this concern only applies in cases where TS is used for transpilation output, and the output code is intended to be run directly with Node.js. It becomes irrelevant for web applications where the source code is almost always processed by a bundler. After bundling, there is no longer resolution happening when the code is executed.

0 commit comments

Comments
 (0)