@@ -2,6 +2,21 @@ open Node
22
33module P = ClackPrompts
44
5+ type projectModuleConfig = {
6+ moduleSystem : string ,
7+ suffix : string ,
8+ gentypeConfig : option <TsConfigMapping .inferredConfig >,
9+ }
10+
11+ let getOrCreateJsonObject = (config : Dict .t <JSON .t >, ~fieldName ) =>
12+ switch config -> Dict .get (fieldName ) {
13+ | Some (Object (object )) => object
14+ | _ =>
15+ let object = Dict .make ()
16+ config -> Dict .set (fieldName , Object (object ))
17+ object
18+ }
19+
520let updatePackageJson = async (~versions ) =>
621 await JsonUtils .updateJsonFile ("package.json" , json =>
722 switch json {
@@ -25,7 +40,14 @@ let updatePackageJson = async (~versions) =>
2540 }
2641 )
2742
28- let updateRescriptJson = async (~projectName , ~sourceDir , ~moduleSystem , ~suffix , ~versions ) =>
43+ let updateRescriptJson = async (
44+ ~projectName ,
45+ ~sourceDir ,
46+ ~moduleSystem ,
47+ ~suffix ,
48+ ~gentypeConfig : option <TsConfigMapping .inferredConfig >,
49+ ~versions ,
50+ ) =>
2951 await JsonUtils .updateJsonFile ("rescript.json" , json =>
3052 switch json {
3153 | Object (config ) =>
@@ -39,6 +61,21 @@ let updateRescriptJson = async (~projectName, ~sourceDir, ~moduleSystem, ~suffix
3961 | Some (Object (sources )) => sources -> Dict .set ("module" , String (moduleSystem ))
4062 | _ => ()
4163 }
64+ switch gentypeConfig {
65+ | Some (gentypeConfig ) =>
66+ let gentypeConfigJson : Dict .t <JSON .t > = Dict .make ()
67+ gentypeConfigJson -> Dict .set ("module" , String (gentypeConfig .moduleSystem ))
68+ gentypeConfigJson -> Dict .set (
69+ "moduleResolution" ,
70+ String (gentypeConfig .gentypeModuleResolution ),
71+ )
72+ gentypeConfigJson -> Dict .set (
73+ "generatedFileExtension" ,
74+ String (gentypeConfig .generatedFileExtension ),
75+ )
76+ config -> Dict .set ("gentypeconfig" , Object (gentypeConfigJson ))
77+ | None => ()
78+ }
4279
4380 if Option .isNone (versions .RescriptVersions .rescriptCoreVersion ) {
4481 RescriptJsonUtils .removeRescriptCore (config )
@@ -60,8 +97,183 @@ let getModuleSystemOptions = () => [
6097 },
6198]
6299
100+ let getPackageJson = async () => await JsonUtils .readJsonFile ("package.json" )
101+
102+ let getPackageType = (packageJson : JSON .t ) =>
103+ switch packageJson {
104+ | Object (config ) =>
105+ switch config -> Dict .get ("type" ) {
106+ | Some (String (packageType )) => Some (packageType -> String .toLowerCase )
107+ | _ => None
108+ }
109+ | _ => None
110+ }
111+
112+ let packageJsonHasDependency = (packageJson : JSON .t , dependencyNames ) => {
113+ let dependencyFields = [
114+ "dependencies" ,
115+ "devDependencies" ,
116+ "peerDependencies" ,
117+ "optionalDependencies" ,
118+ ]
119+
120+ switch packageJson {
121+ | Object (config ) =>
122+ dependencyFields -> Array .some (fieldName =>
123+ switch config -> Dict .get (fieldName ) {
124+ | Some (Object (dependencies )) =>
125+ dependencyNames -> Array .some (dependencyName =>
126+ dependencies -> Dict .get (dependencyName )-> Option .isSome
127+ )
128+ | _ => false
129+ }
130+ )
131+ | _ => false
132+ }
133+ }
134+
135+ let hasJsxDependency = (packageJson : JSON .t ) =>
136+ packageJson -> packageJsonHasDependency ([
137+ "react" ,
138+ "react-dom" ,
139+ "next" ,
140+ "preact" ,
141+ "solid-js" ,
142+ "@vitejs/plugin-react" ,
143+ ])
144+
145+ let getManualModuleConfig = async () => {
146+ let moduleSystem = await P .select ({
147+ message : "What module system will you use?" ,
148+ options : getModuleSystemOptions (),
149+ })-> P .resultOrRaise
150+
151+ {
152+ moduleSystem ,
153+ suffix : moduleSystem === "esmodule" ? ".res.mjs" : ".res.js" ,
154+ gentypeConfig : None ,
155+ }
156+ }
157+
158+ let getTsConfigModuleConfig = async packageJson => {
159+ let projectPath = Process .cwd ()
160+ let tsConfig = TsConfigMapping .read (projectPath )
161+
162+ switch tsConfig .status {
163+ | "found" =>
164+ switch TsConfigMapping .infer (
165+ tsConfig ,
166+ ~packageType = getPackageType (packageJson ),
167+ ~hasJsxDependency = hasJsxDependency (packageJson ),
168+ ) {
169+ | Ok (gentypeConfig ) =>
170+ P .Log .info (
171+ ` Detected tsconfig.json. ReScript will use ${gentypeConfig.moduleSystem} output, ${gentypeConfig.gentypeModuleResolution} genType module resolution, and ${gentypeConfig.suffix} generated JS files.` ,
172+ )
173+
174+ gentypeConfig .warnings -> Array .forEach (P .Log .warn )
175+
176+ Some ({
177+ moduleSystem : gentypeConfig .moduleSystem ,
178+ suffix : gentypeConfig .suffix ,
179+ gentypeConfig : Some (gentypeConfig ),
180+ })
181+ | Error (message ) =>
182+ P .Log .warn (` ${message} Falling back to manual ReScript module setup.` )
183+ None
184+ }
185+ | "not_found" => None
186+ | "typescript_missing" =>
187+ P .Log .warn (
188+ "Found tsconfig.json, but could not resolve the project's TypeScript package. Falling back to manual ReScript module setup." ,
189+ )
190+ None
191+ | _ =>
192+ let message = tsConfig .message -> Option .getOr ("Could not read the effective tsconfig.json." )
193+ P .Log .warn (` ${message} Falling back to manual ReScript module setup.` )
194+ None
195+ }
196+ }
197+
198+ let getProjectModuleConfig = async packageJson =>
199+ switch await getTsConfigModuleConfig (packageJson ) {
200+ | Some (config ) => config
201+ | None => await getManualModuleConfig ()
202+ }
203+
204+ let updateTsConfig = async (~setAllowJs , ~setAllowImportingTsExtensions ) =>
205+ if setAllowJs || setAllowImportingTsExtensions {
206+ await JsonUtils .updateJsonFile ("tsconfig.json" , json =>
207+ switch json {
208+ | Object (config ) =>
209+ let compilerOptions = config -> getOrCreateJsonObject (~fieldName = "compilerOptions" )
210+
211+ if setAllowJs {
212+ compilerOptions -> Dict .set ("allowJs" , Boolean (true ))
213+ }
214+
215+ if setAllowImportingTsExtensions {
216+ compilerOptions -> Dict .set ("allowImportingTsExtensions" , Boolean (true ))
217+ }
218+ | _ => ()
219+ }
220+ )
221+ }
222+
223+ let promptTsConfigUpdates = async (gentypeConfig : option <TsConfigMapping .inferredConfig >) => {
224+ switch gentypeConfig {
225+ | None => ()
226+ | Some (gentypeConfig ) =>
227+ let setAllowJs = if gentypeConfig .needsAllowJs {
228+ P .Log .warn (
229+ "TypeScript allowJs is not enabled. genType imports ReScript's generated JS files, so TypeScript needs allowJs: true to type-check the setup." ,
230+ )
231+
232+ await P .confirm ({
233+ message : "Set compilerOptions.allowJs to true in tsconfig.json?" ,
234+ })-> P .resultOrRaise
235+ } else {
236+ false
237+ }
238+
239+ let setAllowImportingTsExtensions = if gentypeConfig .needsAllowImportingTsExtensions {
240+ P .Log .warn (
241+ "genType bundler module resolution requires TypeScript allowImportingTsExtensions: true." ,
242+ )
243+
244+ await P .confirm ({
245+ message : "Set compilerOptions.allowImportingTsExtensions to true in tsconfig.json?" ,
246+ })-> P .resultOrRaise
247+ } else {
248+ false
249+ }
250+
251+ if gentypeConfig .cannotSetAllowImportingTsExtensions {
252+ P .Log .warn (
253+ "genType bundler module resolution requires allowImportingTsExtensions: true, but TypeScript only allows that option when noEmit or emitDeclarationOnly is enabled." ,
254+ )
255+
256+ let shouldContinue = await P .confirm ({
257+ message : "Continue with the inferred genType bundler configuration anyway?" ,
258+ })-> P .resultOrRaise
259+
260+ if ! shouldContinue {
261+ JsError .throwWithMessage ("genType bundler setup requires manual tsconfig changes." )
262+ }
263+ }
264+
265+ try await updateTsConfig (~setAllowJs , ~setAllowImportingTsExtensions ) catch {
266+ | JsExn (error ) =>
267+ P .Log .warn (
268+ ` Could not update tsconfig.json automatically: ${error-> ErrorUtils.getErrorMessage}` ,
269+ )
270+ }
271+ }
272+ }
273+
63274let addToExistingProject = async (~projectName ) => {
64275 let versions = await RescriptVersions .promptVersions ()
276+ let packageJson = await getPackageJson ()
65277
66278 let sourceDir = await P .text ({
67279 message : "Where will you put your ReScript source files?" ,
@@ -70,15 +282,11 @@ let addToExistingProject = async (~projectName) => {
70282 initialValue : "src" ,
71283 })-> P .resultOrRaise
72284
73- let moduleSystem = await P .select ({
74- message : "What module system will you use?" ,
75- options : getModuleSystemOptions (),
76- })-> P .resultOrRaise
77-
78- let suffix = moduleSystem === "esmodule" ? ".res.mjs" : ".res.js"
285+ let moduleConfig = await getProjectModuleConfig (packageJson )
286+ await promptTsConfigUpdates (moduleConfig .gentypeConfig )
79287
80288 let shouldCheckJsFilesIntoGit = await P .confirm ({
81- message : ` Do you want to check generated ${suffix} files into git?` ,
289+ message : ` Do you want to check generated ${moduleConfig. suffix} files into git?` ,
82290 })-> P .resultOrRaise
83291
84292 let templatePath = CraPaths .getTemplatePath (~templateName = Templates .basicTemplateName )
@@ -103,11 +311,18 @@ let addToExistingProject = async (~projectName) => {
103311 }
104312
105313 if ! shouldCheckJsFilesIntoGit {
106- await Fs .Promises .appendFile (gitignorePath , ` **/*${suffix}${Os.eol}` )
314+ await Fs .Promises .appendFile (gitignorePath , ` **/*${moduleConfig. suffix}${Os.eol}` )
107315 }
108316
109317 await updatePackageJson (~versions )
110- await updateRescriptJson (~projectName , ~sourceDir , ~moduleSystem , ~suffix , ~versions )
318+ await updateRescriptJson (
319+ ~projectName ,
320+ ~sourceDir ,
321+ ~moduleSystem = moduleConfig .moduleSystem ,
322+ ~suffix = moduleConfig .suffix ,
323+ ~gentypeConfig = moduleConfig .gentypeConfig ,
324+ ~versions ,
325+ )
111326
112327 if ! Fs .existsSync (sourceDirPath ) {
113328 await Fs .Promises .mkdir (sourceDirPath )
0 commit comments