Skip to content

Commit e56c84d

Browse files
GenType tsconfig mapping (#129)
* Document genType tsconfig mapping rules * Infer genType setup from tsconfig * Fix tsconfig path boundaries and preserve JSONC updates
1 parent 2264f5e commit e56c84d

9 files changed

Lines changed: 1032 additions & 11 deletions

‎docs/gentype-tsconfig-mapping.md‎

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# genType tsconfig Mapping Rules
2+
3+
This note defines the rules for issue #44: when adding ReScript to an
4+
existing TypeScript project, infer the ReScript and genType configuration from
5+
the project's effective `tsconfig.json` instead of asking the user to choose a
6+
module system manually.
7+
8+
## Research Summary
9+
10+
- ReScript v12 genType is configured through top-level `gentypeconfig` in
11+
`rescript.json`.
12+
- genType supports `gentypeconfig.module` values `esmodule` and `commonjs`.
13+
- genType supports `gentypeconfig.moduleResolution` values `node`, `node16`,
14+
and `bundler`.
15+
- ReScript's TypeScript integration currently requires `"in-source": true` and
16+
generated JS suffixes ending in `.js`, for example `.res.js`.
17+
- ReScript's TypeScript integration requires TypeScript `allowJs: true`.
18+
- ReScript's `bundler` genType mode requires TypeScript 5.0+ and
19+
`allowImportingTsExtensions: true`.
20+
- TypeScript `allowImportingTsExtensions` is only valid when `noEmit` or
21+
`emitDeclarationOnly` is enabled.
22+
- TypeScript `node16`, `node18`, `node20`, and `nodenext` module modes can emit
23+
CommonJS or ESM per file. For `.ts` and `.tsx` files, package.json `"type"`
24+
decides the format: `"module"` means ESM, otherwise CommonJS.
25+
- TypeScript `moduleResolution: "bundler"` models bundlers and does not require
26+
file extensions on relative imports.
27+
28+
## Effective Input
29+
30+
Read the effective TypeScript configuration, not only the raw root
31+
`tsconfig.json`.
32+
33+
1. Find `tsconfig.json` in the current project root.
34+
2. Resolve `extends` using TypeScript semantics before mapping values.
35+
3. Preserve the root project's `package.json` context.
36+
4. Read at least these fields:
37+
- `compilerOptions.module`
38+
- `compilerOptions.moduleResolution`
39+
- `compilerOptions.allowJs`
40+
- `compilerOptions.allowImportingTsExtensions`
41+
- `compilerOptions.noEmit`
42+
- `compilerOptions.emitDeclarationOnly`
43+
- `compilerOptions.jsx`
44+
- package.json `type`
45+
46+
Use a JSONC-aware parser or TypeScript's own config parser when implementing
47+
this. `tsconfig.json` can contain comments, trailing commas, and inherited
48+
settings.
49+
50+
If there is no `tsconfig.json`, or the effective config cannot be read, keep the
51+
current manual module prompt.
52+
53+
## ReScript Output Baseline
54+
55+
When genType is enabled for an existing TypeScript project, set:
56+
57+
```json
58+
{
59+
"package-specs": {
60+
"module": "<mapped module>",
61+
"in-source": true
62+
},
63+
"suffix": ".res.js",
64+
"gentypeconfig": {
65+
"module": "<mapped module>",
66+
"moduleResolution": "<mapped module resolution>",
67+
"generatedFileExtension": "<mapped generated file extension>"
68+
}
69+
}
70+
```
71+
72+
Use `.res.js` for both ESM and CommonJS genType setups. The current CLI behavior
73+
of using `.res.mjs` for ESM is fine for plain ReScript output, but it conflicts
74+
with the documented genType limitation that TypeScript integration currently
75+
supports suffixes ending in `.js`.
76+
77+
The mapped `gentypeconfig.module` should match `package-specs.module`.
78+
79+
## Module Format Mapping
80+
81+
Normalize `compilerOptions.module` and `package.json.type` to lowercase before
82+
mapping.
83+
84+
| TypeScript input | package.json `type` | ReScript `package-specs.module` | genType `module` | Notes |
85+
| --- | --- | --- | --- | --- |
86+
| `commonjs` | any | `commonjs` | `commonjs` | Straight CommonJS mapping. |
87+
| `es2015`, `es6`, `es2020`, `es2022`, `esnext` | any | `esmodule` | `esmodule` | Runtime-agnostic ESM output. |
88+
| `preserve` | any | `esmodule` | `esmodule` | Best fit for bundlers. Warn if the project relies on statement-level CommonJS preservation. |
89+
| `node16`, `node18`, `node20`, `nodenext` | `module` | `esmodule` | `esmodule` | `.ts` and `.tsx` files emit as ESM in this package scope. |
90+
| `node16`, `node18`, `node20`, `nodenext` | absent or `commonjs` | `commonjs` | `commonjs` | `.ts` and `.tsx` files emit as CommonJS in this package scope. |
91+
| `amd`, `umd`, `system`, `none` | any | none | none | Unsupported by ReScript's two genType module formats. Fall back to manual prompt or abort genType setup. |
92+
| missing or unknown | any | none | none | Do not guess. Fall back to the current manual prompt. |
93+
94+
For Node dual-format projects, warn when the project contains `.mts` or `.cts`
95+
files, or package subdirectories with their own package.json `type`. ReScript
96+
has one project-level module output setting, so it cannot exactly mirror a mixed
97+
per-file TypeScript module graph.
98+
99+
## Module Resolution Mapping
100+
101+
Normalize `compilerOptions.moduleResolution` to lowercase. If it is missing,
102+
infer the effective TypeScript default only when the `module` setting makes the
103+
default unambiguous.
104+
105+
| Effective TypeScript module resolution | genType `moduleResolution` | Notes |
106+
| --- | --- | --- |
107+
| `bundler` | `bundler` | Also requires `allowImportingTsExtensions: true`. |
108+
| `node16` | `node16` | Exact documented genType mapping. |
109+
| `nodenext` | `node16` | ReScript v12 documents NodeNext as a use case but only exposes `node16`; warn that this is an approximation. |
110+
| `node`, `node10` | `node` | Legacy Node/CommonJS resolver. |
111+
| `classic` | none | Unsupported for genType setup. Fall back to manual prompt or abort genType setup. |
112+
| missing with `module: "preserve"` | `bundler` | TypeScript uses bundler-style resolution for preserve-mode bundled projects. |
113+
| missing with `module: "node16"`, `node18`, `node20`, or `nodenext` | `node16` | Use the Node ESM-compatible genType mode. Warn for `nodenext` as above. |
114+
| missing with `commonjs` | `node` | Legacy CommonJS default. |
115+
| missing with ESM-family module values | none | Do not invent `node`; use TypeScript's effective value if the parser provides one, otherwise prompt. |
116+
| unknown | none | Do not guess. Fall back to manual prompt. |
117+
118+
## TypeScript Config Edits And Warnings
119+
120+
### `allowJs`
121+
122+
If `compilerOptions.allowJs` is not `true`, warn and offer to set it to `true`.
123+
ReScript's TypeScript integration requires this so TypeScript can accept the JS
124+
files emitted by ReScript and imported by generated genType files.
125+
126+
### `allowImportingTsExtensions`
127+
128+
If the mapped genType module resolution is `bundler`:
129+
130+
1. If `allowImportingTsExtensions` is already `true`, no action is needed.
131+
2. If `noEmit: true` or `emitDeclarationOnly: true`, offer to set
132+
`allowImportingTsExtensions: true`.
133+
3. Otherwise, warn that TypeScript does not allow
134+
`allowImportingTsExtensions` unless `noEmit` or `emitDeclarationOnly` is
135+
enabled. Continue only after confirmation, or fall back to manual setup.
136+
137+
### Generated File Extension
138+
139+
Use:
140+
141+
| Signal | `gentypeconfig.generatedFileExtension` |
142+
| --- | --- |
143+
| `compilerOptions.jsx` is present | `.gen.tsx` |
144+
| React, Next.js, or another JSX framework is detected in package.json dependencies | `.gen.tsx` |
145+
| No JSX signal | `.gen.ts` |
146+
147+
Set `.gen.ts` explicitly when no JSX signal is available. That differs from the
148+
documented genType default of `.gen.tsx`, but it is less surprising for existing
149+
non-React TypeScript projects.
150+
151+
## Fallback Rules
152+
153+
Fall back to the current manual module prompt when:
154+
155+
- There is no readable `tsconfig.json`.
156+
- The effective TypeScript config cannot be resolved.
157+
- `module` is missing or unsupported.
158+
- `moduleResolution` is `classic`, unknown, or conflicts with the module mode.
159+
- The project is a mixed Node dual-format project that cannot be represented by
160+
one ReScript project-level module setting, such as a CommonJS package with
161+
`.mts` inputs, an ESM package with `.cts` inputs, or included source files
162+
under nested package.json files with a different `type`.
163+
- The project uses `module: "preserve"` and package contents suggest meaningful
164+
CommonJS-style exports that ReScript cannot preserve statement-by-statement.
165+
166+
When falling back, still surface the detected values in the prompt so the user
167+
can make an informed choice.
168+
169+
## Implementation Notes
170+
171+
- Prefer loading the target project's `typescript` package and using its config
172+
parser when available. That handles JSONC and `extends` correctly.
173+
- If TypeScript is not available yet, use a JSONC parser and implement only the
174+
documented `extends` behavior needed for `compilerOptions`, or ask the user to
175+
install dependencies first.
176+
- Do not copy the existing Next.js template's legacy `gentypeconfig` shape
177+
(`language`, `shims`) into the add-to-existing flow. The v12 manual documents
178+
the current compiler-integrated fields used above.
179+
- Keep generated JS ignore behavior tied to the selected suffix. For genType,
180+
the suffix is always `.res.js`, so the gitignore prompt should refer to
181+
generated `.res.js` files.
182+
183+
## Sources
184+
185+
- ReScript build configuration, especially `package-specs`, `suffix`, and
186+
`gentypeconfig`: https://rescript-lang.org/docs/manual/build-configuration/
187+
- ReScript TypeScript integration setup, `allowJs`, module resolution, and
188+
genType limitations: https://rescript-lang.org/docs/manual/typescript-integration/
189+
- TypeScript `module` option and Node dual-format behavior:
190+
https://www.typescriptlang.org/tsconfig/module.html
191+
- TypeScript module resolution reference:
192+
https://www.typescriptlang.org/tsconfig/moduleResolution.html
193+
- TypeScript module reference for Node and bundler resolution:
194+
https://www.typescriptlang.org/docs/handbook/modules/reference.html
195+
- TypeScript `allowImportingTsExtensions` constraints:
196+
https://www.typescriptlang.org/tsconfig/allowImportingTsExtensions.html
197+
- TypeScript `extends` behavior:
198+
https://www.typescriptlang.org/tsconfig/extends.html

‎package-lock.json‎

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"start": "node src/Main.res.mjs",
88
"prepack": "rescript && rollup -c",
9-
"test": "rescript && node --test test/*Test.res.mjs",
9+
"test": "rescript && node --test test/*Test.res.mjs test/*Test.mjs",
1010
"format": "rescript format",
1111
"dev": "rescript -w"
1212
},
@@ -43,8 +43,10 @@
4343
"@rollup/plugin-terser": "^0.4.4",
4444
"@vitejs/plugin-react": "^6.0.1",
4545
"compare-versions": "^6.1.0",
46+
"jsonc-parser": "^3.3.1",
4647
"rescript": "^12.0.0",
4748
"rollup": "^4.53.3",
49+
"typescript": "^5.9.3",
4850
"vite": "^8.0.10"
4951
}
5052
}

0 commit comments

Comments
 (0)