From fbf1652be846068fcf858b178d5fa5ba373e45af Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 26 Nov 2025 16:23:57 +0100 Subject: [PATCH 1/3] Add AGENTS.md --- AGENTS.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ddbd1d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS.md + +## Project Overview + +`create-rescript-app` is the official CLI tool for scaffolding new ReScript applications. It supports creating projects from templates and adding ReScript to existing JavaScript projects. The tool is written in ReScript and distributed as a bundled Node.js CLI. + +## Coding Style & Naming Conventions + +- Make sure to use modern ReScript and not Reason syntax! Read https://rescript-lang.org/llms/manual/llm-small.txt to learn the language syntax. +- Formatting is enforced by `rescript format`; keep 2-space indentation and prefer pattern matching over chained conditionals. +- Module files are PascalCase (`Templates.res`), values/functions camelCase, types/variants PascalCase, and records snake_case fields only when matching external JSON. +- Keep `.resi` signatures accurate and minimal; avoid exposing helpers that are template-specific. +- When touching templates, mirror upstream defaults and keep package scripts consistent with the chosen toolchain. + +## Package Manager Support + +- Detects and supports npm, yarn, pnpm, and bun +- Handles existing project detection based on presence of `package.json` +- Adapts installation commands based on detected package manager + +## Directory Structure + +- **`src/`**: ReScript source files +- **`lib/`**: Generated build artifacts from ReScript compiler (do not edit) +- **`out/`**: Production bundle (`create-rescript-app.cjs`) for distribution +- **`templates/`**: Project starter templates (keep self-contained) +- **`bindings/`**: External library bindings (ClackPrompts, CompareVersions) + +## Development Commands + +- **`npm start`** - Run CLI directly from source (`src/Main.res.mjs`) for interactive testing and development +- **`npm run dev`** - Watch ReScript sources and rebuild automatically to `lib/` directory +- **`npm run prepack`** - Compile ReScript and bundle with Rollup into `out/create-rescript-app.cjs` (production build) +- **`npm run format`** - Apply ReScript formatter across all source files + +## Testing and Validation + +- **Manual Testing**: No automated test suite - perform smoke tests by running the CLI into a temp directory +- **Template Validation**: After changes, test each template type (basic/Next.js/Vite) to ensure templates bootstrap cleanly +- **Build Verification**: Run `npm run prepack` to ensure the production bundle builds correctly + +## Build System + +- **ReScript Compiler**: Outputs ES modules in-source (`src/*.res.mjs`) with configuration in `rescript.json` +- **Rollup Bundler**: Creates `out/create-rescript-app.cjs` CommonJS bundle for distribution + +## Template Modification Guidelines + +When modifying templates: + +1. Maintain consistency with upstream toolchain defaults +2. Ensure package scripts match the chosen build tool (Vite, Next.js, etc.) +3. Keep templates self-contained with their own dependencies +4. Test template bootstrapping after modifications From b697a0b5501234f191d17c4b6235076e122b7e50 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 26 Nov 2025 17:24:52 +0100 Subject: [PATCH 2/3] Correctly handle yarn berry; remove package-lock.json if not using npm --- src/PackageManagers.res | 57 +++++++++++++++++++++++++++++++--------- src/PackageManagers.resi | 14 +++++++++- src/RescriptVersions.res | 43 +++++++++++++++++++++++------- src/bindings/Node.res | 3 +++ 4 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/PackageManagers.res b/src/PackageManagers.res index 5df28dc..655aaf4 100644 --- a/src/PackageManagers.res +++ b/src/PackageManagers.res @@ -1,21 +1,54 @@ open Node +type packageManager = + | Npm + | Yarn1 + | YarnBerry + | Pnpm + | Bun + +type packageManagerInfo = { + packageManager: packageManager, + command: string, +} + +let defaultPackagerInfo = {packageManager: Npm, command: "npm"} + @scope(("process", "env")) external npm_execpath: option = "npm_execpath" -let compatiblePackageManagers = ["pnpm", "npm", "yarn", "bun"] +let getPackageManagerInfo = async () => + switch npm_execpath { + | None => defaultPackagerInfo + | Some(execPath) => + // #58: Windows: packageManager may be something like + // "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js". + // + // Therefore, packageManager needs to be in quotes, and we need to prepend "node " + // if packageManager points to a JS file, otherwise the invocation will hang. + let maybeNode = execPath->String.endsWith("js") ? "node " : "" + let command = `${maybeNode}"${execPath}"` -let isCompatiblePackageManager = execPath => { - let filename = Path.parse(execPath).name + // Note: exec path may be something like + // /usr/local/lib/node_modules/npm/bin/npm-cli.js + // So we have to check for substrings here. + let filename = Path.parse(execPath).name->String.toLowerCase - // Note: exec path may be something like - // /usr/local/lib/node_modules/npm/bin/npm-cli.js - // So we have to check for substrings here. - compatiblePackageManagers->Array.some(pm => filename->String.includes(pm)) -} + let packageManager = switch () { + | _ if filename->String.includes("npm") => Some(Npm) + | _ if filename->String.includes("yarn") => + let versionResult = await Promisified.ChildProcess.exec(`${command} --version`) + let version = versionResult.stdout->String.trim + let isYarn1 = CompareVersions.compareVersions(version, "2.0.0")->Ordering.isLess -let getActivePackageManager = () => - switch npm_execpath { - | Some(execPath) if isCompatiblePackageManager(execPath) => execPath - | _ => "npm" + Some(isYarn1 ? Yarn1 : YarnBerry) + | _ if filename->String.includes("pnpm") => Some(Pnpm) + | _ if filename->String.includes("bun") => Some(Bun) + | _ => None + } + + switch packageManager { + | Some(packageManager) => {packageManager, command} + | None => defaultPackagerInfo + } } diff --git a/src/PackageManagers.resi b/src/PackageManagers.resi index 4531f22..05d9cac 100644 --- a/src/PackageManagers.resi +++ b/src/PackageManagers.resi @@ -1 +1,13 @@ -let getActivePackageManager: unit => string +type packageManager = + | Npm + | Yarn1 + | YarnBerry + | Pnpm + | Bun + +type packageManagerInfo = { + packageManager: packageManager, + command: string, +} + +let getPackageManagerInfo: unit => promise diff --git a/src/RescriptVersions.res b/src/RescriptVersions.res index d16598e..5eea60d 100644 --- a/src/RescriptVersions.res +++ b/src/RescriptVersions.res @@ -1,3 +1,5 @@ +open Node + module P = ClackPrompts let rescriptVersionRange = `11.x.x || 12.x.x` @@ -73,8 +75,29 @@ let promptVersions = async () => { {rescriptVersion, rescriptCoreVersion} } +let ensureYarnNodeModulesLinker = async () => { + let yarnRcPath = Path.join2(Process.cwd(), ".yarnrc.yml") + + if !Fs.existsSync(yarnRcPath) { + let nodeLinkerLine = "nodeLinker: node-modules" + let eol = Os.eol + + await Fs.Promises.writeFile(yarnRcPath, `${nodeLinkerLine}${eol}`) + } +} + +let removeNpmPackageLock = async () => { + let packageLockPath = Path.join2(Process.cwd(), "package-lock.json") + + if Fs.existsSync(packageLockPath) { + await Fs.Promises.unlink(packageLockPath) + } +} + let installVersions = async ({rescriptVersion, rescriptCoreVersion}) => { - let packageManager = PackageManagers.getActivePackageManager() + let packageManagerInfo = await PackageManagers.getPackageManagerInfo() + let {command: packageManagerCommand, packageManager} = packageManagerInfo + let packages = switch rescriptCoreVersion { | Some(rescriptCoreVersion) => [ `rescript@${rescriptVersion}`, @@ -83,15 +106,17 @@ let installVersions = async ({rescriptVersion, rescriptCoreVersion}) => { | None => [`rescript@${rescriptVersion}`] } - // #58: Windows: packageManager may be something like - // "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js". - // - // Therefore, packageManager needs to be in quotes, and we need to prepend "node " - // if packageManager points to a JS file, otherwise the invocation will hang. - let maybeNode = packageManager->String.endsWith("js") ? "node " : "" - let command = `${maybeNode}"${packageManager}" add ${packages->Array.join(" ")}` + if packageManager === YarnBerry { + await ensureYarnNodeModulesLinker() + } - let _ = await Node.Promisified.ChildProcess.exec(command) + let command = `${packageManagerCommand} add ${packages->Array.join(" ")}` + + let _ = await Promisified.ChildProcess.exec(command) + + if packageManager !== Npm { + await removeNpmPackageLock() + } } let esmModuleSystemName = ({rescriptVersion}) => diff --git a/src/bindings/Node.res b/src/bindings/Node.res index 14d14e9..80c9b5e 100644 --- a/src/bindings/Node.res +++ b/src/bindings/Node.res @@ -24,6 +24,9 @@ module Fs = { @module("node:fs") @scope("promises") external mkdir: string => promise = "mkdir" + + @module("node:fs") @scope("promises") + external unlink: string => promise = "unlink" } } From b90137d56732336d4d272398bdaea105c98cc068 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 26 Nov 2025 20:10:00 +0100 Subject: [PATCH 3/3] Set version to 1.12.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d3c677..6b41455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-rescript-app", - "version": "1.11.0", + "version": "1.12.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-rescript-app", - "version": "1.11.0", + "version": "1.12.0-beta.1", "license": "ISC", "bin": { "create-rescript-app": "out/create-rescript-app.cjs" diff --git a/package.json b/package.json index 1afa6f6..36e847b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-rescript-app", - "version": "1.11.0", + "version": "1.12.0-beta.1", "description": "Quickly create new ReScript apps from project templates.", "main": "out/create-rescript-app.cjs", "scripts": {