diff --git a/CHANGELOG.md b/CHANGELOG.md index b31db042c5..699b88bc72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ #### :rocket: Main New Feature - Add support for Dynamic import. https://github.com/rescript-lang/rescript-compiler/pull/5703 +- GenType: Add `moduleResolution` option to customize extensions on emitted import statements. This helps to adjust output compatibility with TypeScript projects using ESM. https://github.com/rescript-lang/rescript-compiler/pull/6182 + - `node` (default): Drop extensions. + - `node16`: Use TS output's extensions. Make it ESM-compatible. + - `bundler`: Use TS input's extensions. Make it ESM-compatible. #### :boom: Breaking Change diff --git a/jscomp/gentype/EmitType.ml b/jscomp/gentype/EmitType.ml index caa4756f77..7f6dbe7e27 100644 --- a/jscomp/gentype/EmitType.ml +++ b/jscomp/gentype/EmitType.ml @@ -13,21 +13,6 @@ let fileHeader ~sourceFile = ~lines:["TypeScript file generated from " ^ sourceFile ^ " by genType."] ^ "/* eslint-disable import/first */\n\n" -let generatedFilesExtension ~(config : Config.t) = - match config.generatedFileExtension with - | Some s -> - (* from .foo.bar to .foo *) - Filename.remove_extension s - | None -> ".gen" - -let outputFileSuffix ~(config : Config.t) = - match config.generatedFileExtension with - | Some s when Filename.extension s <> "" (* double extension *) -> s - | _ -> generatedFilesExtension ~config ^ ".tsx" - -let generatedModuleExtension ~config = generatedFilesExtension ~config -let shimExtension = ".shim.ts" - let interfaceName ~(config : Config.t) name = match config.exportInterfaces with | true -> "I" ^ name @@ -387,6 +372,13 @@ let emitRequire ~importedValueOrComponent ~early ~emitters ~(config : Config.t) | true -> "// tslint:disable-next-line:no-var-requires\n" | false -> "// @ts-ignore: Implicit any on import\n" in + let importPath = + match config.moduleResolution with + | Node -> + importPath + |> ImportPath.chopExtensionSafe (* for backward compatibility *) + | _ -> importPath + in match config.module_ with | ES6 when not importedValueOrComponent -> let moduleNameString = ModuleName.toString moduleName in diff --git a/jscomp/gentype/GenTypeConfig.ml b/jscomp/gentype/GenTypeConfig.ml index aa8e2af049..125d94fb5c 100644 --- a/jscomp/gentype/GenTypeConfig.ml +++ b/jscomp/gentype/GenTypeConfig.ml @@ -1,6 +1,15 @@ module ModuleNameMap = Map.Make (ModuleName) type module_ = CommonJS | ES6 + +(** Compatibility for `compilerOptions.moduleResolution` in TypeScript projects. *) +type moduleResolution = + | Node (** should drop extension on import statements *) + | Node16 + (** should use TS output's extension (e.g. `.gen.js`) on import statements *) + | Bundler + (** should use TS input's extension (e.g. `.gen.tsx`) on import statements *) + type bsVersion = int * int * int type t = { @@ -13,6 +22,7 @@ type t = { exportInterfaces: bool; generatedFileExtension: string option; module_: module_; + moduleResolution: moduleResolution; namespace: string option; platformLib: string; mutable projectRoot: string; @@ -32,12 +42,13 @@ let default = exportInterfaces = false; generatedFileExtension = None; module_ = ES6; + moduleResolution = Node; namespace = None; platformLib = ""; projectRoot = ""; shimsMap = ModuleNameMap.empty; sources = None; - suffix = ""; + suffix = ".bs.js"; } let bsPlatformLib ~config = @@ -112,6 +123,7 @@ let readConfig ~getBsConfigFile ~namespace = in let parseConfig ~bsconf ~gtconf = let moduleString = gtconf |> getStringOption "module" in + let moduleResolutionString = gtconf |> getStringOption "moduleResolution" in let exportInterfacesBool = gtconf |> getBool "exportInterfaces" in let generatedFileExtensionStringOption = gtconf |> getStringOption "generatedFileExtension" @@ -143,6 +155,13 @@ let readConfig ~getBsConfigFile ~namespace = | None, Some ("es6" | "es6-global") -> ES6 | _ -> default.module_ in + let moduleResolution = + match moduleResolutionString with + | Some "node" -> Node + | Some "node16" -> Node16 + | Some "bundler" -> Bundler + | _ -> default.moduleResolution + in let exportInterfaces = match exportInterfacesBool with | None -> default.exportInterfaces @@ -171,9 +190,8 @@ let readConfig ~getBsConfigFile ~namespace = in let suffix = match bsconf |> getStringOption "suffix" with - | Some ".bs.js" -> ".bs" | Some s -> s - | _ -> ".bs" + | _ -> default.suffix in let bsDependencies = match bsconf |> getOpt "bs-dependencies" with @@ -204,6 +222,7 @@ let readConfig ~getBsConfigFile ~namespace = exportInterfaces; generatedFileExtension; module_; + moduleResolution; namespace; platformLib; projectRoot; diff --git a/jscomp/gentype/GenTypeMain.ml b/jscomp/gentype/GenTypeMain.ml index 7269de6451..e4bcaf0346 100644 --- a/jscomp/gentype/GenTypeMain.ml +++ b/jscomp/gentype/GenTypeMain.ml @@ -103,8 +103,8 @@ let processCmtFile cmt = let fileName = cmt |> Paths.getModuleName in let isInterface = Filename.check_suffix cmtFile ".cmti" in let resolver = - ModuleResolver.createLazyResolver ~config - ~extensions:[".res"; EmitType.shimExtension] ~excludeFile:(fun fname -> + ModuleResolver.createLazyResolver ~config ~extensions:[".res"; ".shim.ts"] + ~excludeFile:(fun fname -> fname = "React.res" || fname = "ReasonReact.res") in let inputCMT, hasGenTypeAnnotations = diff --git a/jscomp/gentype/ImportPath.ml b/jscomp/gentype/ImportPath.ml index 6715b18b97..a1c11145d2 100644 --- a/jscomp/gentype/ImportPath.ml +++ b/jscomp/gentype/ImportPath.ml @@ -13,19 +13,17 @@ let fromModule ~dir ~importExtension moduleName = let fromStringUnsafe s = ("", s) -let chopExtensionSafe s = - try s |> Filename.chop_extension with Invalid_argument _ -> s +let chopExtensionSafe (dir, s) = + try (dir, s |> Filename.chop_extension) with Invalid_argument _ -> (dir, s) let dump (dir, s) = NodeFilename.concat dir s let toCmt ~(config : Config.t) ~outputFileRelative (dir, s) = let open Filename in - concat - (outputFileRelative |> dirname) - (((dir, s |> chopExtensionSafe) |> dump) - ^ (match config.namespace with - | None -> "" - | Some name -> "-" ^ name) - ^ ".cmt") + concat (outputFileRelative |> dirname) ((dir, s) |> chopExtensionSafe |> dump) + ^ (match config.namespace with + | None -> "" + | Some name -> "-" ^ name) + ^ ".cmt" let emit (dir, s) = (dir, s) |> dump diff --git a/jscomp/gentype/ImportPath.mli b/jscomp/gentype/ImportPath.mli index 54d72d4254..5f319d00bc 100644 --- a/jscomp/gentype/ImportPath.mli +++ b/jscomp/gentype/ImportPath.mli @@ -3,6 +3,7 @@ open GenTypeCommon type t val bsCurryPath : config:Config.t -> t +val chopExtensionSafe : t -> t val dump : t -> string val emit : t -> string val fromModule : dir:string -> importExtension:string -> ModuleName.t -> t diff --git a/jscomp/gentype/ModuleExtension.ml b/jscomp/gentype/ModuleExtension.ml new file mode 100644 index 0000000000..c983f4ec30 --- /dev/null +++ b/jscomp/gentype/ModuleExtension.ml @@ -0,0 +1,28 @@ +open GenTypeCommon + +let shimTsOutputFileExtension ~(config : Config.t) = + match config.moduleResolution with + | Node -> ".shim" + | Node16 -> ".shim.js" + | Bundler -> ".shim.ts" + +let generatedFilesExtension ~(config : Config.t) = + match config.generatedFileExtension with + | Some s -> + (* from .foo.bar to .foo *) + Filename.remove_extension s + | None -> ".gen" + +let tsInputFileSuffix ~(config : Config.t) = + match config.generatedFileExtension with + | Some s when Filename.extension s <> "" (* double extension *) -> s + | _ -> generatedFilesExtension ~config ^ ".tsx" + +let tsOutputFileSuffix ~(config : Config.t) = + generatedFilesExtension ~config ^ ".js" + +let generatedModuleExtension ~(config : Config.t) = + match config.moduleResolution with + | Node -> generatedFilesExtension ~config + | Node16 -> tsOutputFileSuffix ~config + | Bundler -> tsInputFileSuffix ~config diff --git a/jscomp/gentype/ModuleResolver.ml b/jscomp/gentype/ModuleResolver.ml index e7d5e26960..838ecca198 100644 --- a/jscomp/gentype/ModuleResolver.ml +++ b/jscomp/gentype/ModuleResolver.ml @@ -256,7 +256,7 @@ let resolveGeneratedModule ~config ~outputFileRelative ~resolver moduleName = (moduleName |> ModuleName.toString); let importPath = resolveModule ~config - ~importExtension:(EmitType.generatedModuleExtension ~config) + ~importExtension:(ModuleExtension.generatedModuleExtension ~config) ~outputFileRelative ~resolver ~useBsDependencies:true moduleName in if !Debug.moduleResolution then @@ -272,9 +272,10 @@ let importPathForReasonModuleName ~(config : Config.t) ~outputFileRelative | shimModuleName -> if !Debug.moduleResolution then Log_.item "ShimModuleName: %s\n" (shimModuleName |> ModuleName.toString); + let importExtension = ModuleExtension.shimTsOutputFileExtension ~config in let importPath = - resolveModule ~config ~importExtension:".shim" ~outputFileRelative - ~resolver ~useBsDependencies:false shimModuleName + resolveModule ~config ~importExtension ~outputFileRelative ~resolver + ~useBsDependencies:false shimModuleName in if !Debug.moduleResolution then Log_.item "Import Path: %s\n" (importPath |> ImportPath.dump); diff --git a/jscomp/gentype/Paths.ml b/jscomp/gentype/Paths.ml index c1c35b0c24..b64b5c0b36 100644 --- a/jscomp/gentype/Paths.ml +++ b/jscomp/gentype/Paths.ml @@ -29,7 +29,7 @@ let findNameSpace cmt = |> keepAfterDash let getOutputFileRelative ~config cmt = - (cmt |> handleNamespace) ^ EmitType.outputFileSuffix ~config + (cmt |> handleNamespace) ^ ModuleExtension.tsInputFileSuffix ~config let getOutputFile ~(config : Config.t) cmt = Filename.concat config.projectRoot (getOutputFileRelative ~config cmt) diff --git a/jscomp/gentype_tests/typescript-react-example/package-lock.json b/jscomp/gentype_tests/typescript-react-example/package-lock.json index 888a014276..ba9633e710 100644 --- a/jscomp/gentype_tests/typescript-react-example/package-lock.json +++ b/jscomp/gentype_tests/typescript-react-example/package-lock.json @@ -13,15 +13,15 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@types/node": "^13.13.4", - "@types/react-dom": "^16.9.7", + "@types/node": "^18.15.12", + "@types/react-dom": "^18.0.11", "rescript": "file:../../..", - "typescript": "3.9.2" + "typescript": "^5.0.4" } }, "../../..": { "name": "rescript", - "version": "11.0.0-alpha.3", + "version": "11.0.0-alpha.5", "dev": true, "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", @@ -47,9 +47,9 @@ } }, "node_modules/@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", + "version": "18.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.12.tgz", + "integrity": "sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==", "dev": true }, "node_modules/@types/prop-types": { @@ -70,12 +70,12 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.16", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.16.tgz", - "integrity": "sha512-Oqc0RY4fggGA3ltEgyPLc3IV9T73IGoWjkONbsyJ3ZBn+UPPCYpU2ec0i3cEbJuEdZtkqcCF2l1zf2pBdgUGSg==", + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", "dev": true, "dependencies": { - "@types/react": "^16" + "@types/react": "*" } }, "node_modules/@types/scheduler": { @@ -142,16 +142,16 @@ } }, "node_modules/typescript": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", - "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } } }, @@ -163,9 +163,9 @@ "requires": {} }, "@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", + "version": "18.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.12.tgz", + "integrity": "sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==", "dev": true }, "@types/prop-types": { @@ -186,12 +186,12 @@ } }, "@types/react-dom": { - "version": "16.9.16", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.16.tgz", - "integrity": "sha512-Oqc0RY4fggGA3ltEgyPLc3IV9T73IGoWjkONbsyJ3ZBn+UPPCYpU2ec0i3cEbJuEdZtkqcCF2l1zf2pBdgUGSg==", + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", "dev": true, "requires": { - "@types/react": "^16" + "@types/react": "*" } }, "@types/scheduler": { @@ -254,9 +254,9 @@ } }, "typescript": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", - "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true } } diff --git a/jscomp/gentype_tests/typescript-react-example/package.json b/jscomp/gentype_tests/typescript-react-example/package.json index a494cc27a5..b8a0cec5b8 100644 --- a/jscomp/gentype_tests/typescript-react-example/package.json +++ b/jscomp/gentype_tests/typescript-react-example/package.json @@ -14,9 +14,9 @@ "tsc": "tsc -p tsconfig.json" }, "devDependencies": { - "@types/node": "^13.13.4", - "@types/react-dom": "^16.9.7", + "@types/node": "^18.15.12", + "@types/react-dom": "^18.0.11", "rescript": "file:../../..", - "typescript": "3.9.2" + "typescript": "^5.0.4" } }