Skip to content

Commit 0465eee

Browse files
Template arguments (#121)
1 parent 63a213b commit 0465eee

13 files changed

Lines changed: 334 additions & 24 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: NPM install
3333
run: npm ci
3434

35+
- name: Run tests
36+
run: npm test
37+
3538
- name: Pack (includes rescript build)
3639
run: npm pack
3740

AGENTS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
- 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.
1010
- Formatting is enforced by `rescript format`; keep 2-space indentation and prefer pattern matching over chained conditionals.
11+
- Prefer `result` values over exceptions for expected failure paths; only raise or throw at clear integration boundaries where the surrounding API requires it.
12+
- Prefer pattern matching over `if`/`else` chains when branching on the shape or state of the same value; plain comparisons across different values are fine with `if`/`else`.
13+
- Do not run `rescript`, `npm test`, or `npm run prepack` in parallel; ReScript compiler artifacts are not safe for concurrent builds in this repo.
1114
- Module files are PascalCase (`Templates.res`), values/functions camelCase, types/variants PascalCase, and records snake_case fields only when matching external JSON.
1215
- Keep `.resi` signatures accurate and minimal; avoid exposing helpers that are template-specific.
1316
- When touching templates, mirror upstream defaults and keep package scripts consistent with the chosen toolchain.
@@ -31,11 +34,13 @@
3134
- **`npm start`** - Run CLI directly from source (`src/Main.res.mjs`) for interactive testing and development
3235
- **`npm run dev`** - Watch ReScript sources and rebuild automatically to `lib/` directory
3336
- **`npm run prepack`** - Compile ReScript and bundle with Rollup into `out/create-rescript-app.cjs` (production build)
37+
- **`npm test`** - Compile ReScript sources and run the Node.js regression tests
3438
- **`npm run format`** - Apply ReScript formatter across all source files
3539

3640
## Testing and Validation
3741

38-
- **Manual Testing**: No automated test suite - perform smoke tests by running the CLI into a temp directory
42+
- **Automated Tests**: Run `npm test` for automated coverage of CLI parsing and related helpers
43+
- **Manual Testing**: Perform smoke tests by running the CLI into a temp directory
3944
- **Template Validation**: After changes, test each template type (basic/Next.js/Vite) to ensure templates bootstrap cleanly
4045
- **Build Verification**: Run `npm run prepack` to ensure the production bundle builds correctly
4146

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ or
2424
bun create rescript-app
2525
```
2626

27+
You can also skip the interactive prompts by passing a project name and template flag.
28+
Supported templates are defined [`here`](./src/Templates.res).
29+
30+
With npm, pass the template flag after `--`:
31+
32+
```sh
33+
npm create rescript-app@latest my-app -- --template vite
34+
```
35+
36+
With Yarn, pnpm, and Bun, you can pass the template flag directly:
37+
38+
```sh
39+
yarn create rescript-app my-app --template vite
40+
```
41+
2742
## Add to existing project
2843

2944
If you have an existing JavaScript project containing a `package.json`, you can execute one of the above commands directly in your project's directory to add ReScript to your project.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +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",
910
"format": "rescript format",
1011
"dev": "rescript -w"
1112
},

rescript.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
{
22
"name": "create-rescript-app",
3-
"sources": {
4-
"dir": "src",
5-
"subdirs": true
6-
},
3+
"sources": [
4+
{
5+
"dir": "src",
6+
"subdirs": true
7+
},
8+
{
9+
"dir": "test",
10+
"subdirs": true,
11+
"type": "dev"
12+
}
13+
],
714
"package-specs": {
815
"module": "esmodule",
916
"in-source": true

src/CommandLineArguments.res

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
type t = {
2+
projectName: option<string>,
3+
templateName: option<string>,
4+
}
5+
6+
let supportedOptionsHint = `Supported options: --template <${Templates.supportedTemplateNames->Array.join(
7+
"|",
8+
)}> or -t <${Templates.supportedTemplateNames->Array.join("|")}>.`
9+
10+
let getTemplateName = templateName =>
11+
switch Templates.getTemplateName(templateName) {
12+
| Some(templateName) => Ok(templateName)
13+
| None =>
14+
Error(
15+
`Unknown template "${templateName}". Available templates: ${Templates.supportedTemplateNames->Array.join(
16+
", ",
17+
)}.`,
18+
)
19+
}
20+
21+
let parseError = message => Error(`${message} ${supportedOptionsHint}`)
22+
23+
let rec parseRemainingArguments = (remainingArguments, commandLineArguments) =>
24+
switch remainingArguments {
25+
| list{} => Ok(commandLineArguments)
26+
| list{"-t", templateName, ...remainingArguments}
27+
| list{"--template", templateName, ...remainingArguments} =>
28+
switch getTemplateName(templateName) {
29+
| Ok(templateName) =>
30+
parseRemainingArguments(
31+
remainingArguments,
32+
{
33+
...commandLineArguments,
34+
templateName: Some(templateName),
35+
},
36+
)
37+
| Error(message) => Error(message)
38+
}
39+
| list{"-t"} | list{"--template"} => parseError("Missing value for --template.")
40+
| list{argument, ...remainingArguments} if argument->String.startsWith("--template=") =>
41+
switch argument->String.split("=") {
42+
| [_, templateName] =>
43+
switch getTemplateName(templateName) {
44+
| Ok(templateName) =>
45+
parseRemainingArguments(
46+
remainingArguments,
47+
{
48+
...commandLineArguments,
49+
templateName: Some(templateName),
50+
},
51+
)
52+
| Error(message) => Error(message)
53+
}
54+
| _ => parseError("Missing value for --template.")
55+
}
56+
| list{argument, ..._remainingArguments} if argument->String.startsWith("-") =>
57+
parseError(`Unknown option "${argument}".`)
58+
| list{argument, ...remainingArguments} =>
59+
switch commandLineArguments.projectName {
60+
| None =>
61+
parseRemainingArguments(
62+
remainingArguments,
63+
{...commandLineArguments, projectName: Some(argument)},
64+
)
65+
| Some(_) => parseError(`Unexpected argument "${argument}".`)
66+
}
67+
}
68+
69+
let parse = remainingArguments =>
70+
parseRemainingArguments(remainingArguments, {projectName: None, templateName: None})
71+
72+
let fromProcessArgv = argv =>
73+
switch List.fromArray(argv) {
74+
| list{_, _, ...remainingArguments} => parse(remainingArguments)
75+
| _ => Ok({projectName: None, templateName: None})
76+
}

src/CommandLineArguments.resi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type t = {
2+
projectName: option<string>,
3+
templateName: option<string>,
4+
}
5+
6+
let parse: list<string> => result<t, string>
7+
let fromProcessArgv: array<string> => result<t, string>

src/NewProject.res

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,33 @@ let createNewProject = async () => {
113113
~versions={rescriptVersion: "11.1.1", rescriptCoreVersion: Some("1.5.0")},
114114
)
115115
} else {
116-
let projectName = await P.text({
117-
message: "What is the name of your new ReScript project?",
118-
placeholder: "my-rescript-app",
119-
initialValue: ?Process.argv[2],
120-
validate: validateProjectName,
121-
})->P.resultOrRaise
122-
123-
let templateName = await P.select({
124-
message: "Select a template",
125-
options: getTemplateOptions(),
126-
})->P.resultOrRaise
127-
128-
let versions = await RescriptVersions.promptVersions()
116+
let commandLineArguments = CommandLineArguments.fromProcessArgv(Process.argv)->Result.getOrThrow
117+
let useDefaultVersions = Option.isSome(commandLineArguments.templateName)
118+
119+
let projectName = switch commandLineArguments.projectName {
120+
| Some(projectName) if useDefaultVersions => projectName->validateProjectName->Option.getOrThrow
121+
122+
| initialValue =>
123+
await P.text({
124+
message: "What is the name of your new ReScript project?",
125+
placeholder: "my-rescript-app",
126+
?initialValue,
127+
validate: validateProjectName,
128+
})->P.resultOrRaise
129+
}
130+
131+
let templateName = switch commandLineArguments.templateName {
132+
| Some(templateName) => templateName
133+
| None =>
134+
await P.select({
135+
message: "Select a template",
136+
options: getTemplateOptions(),
137+
})->P.resultOrRaise
138+
}
139+
140+
let versions = useDefaultVersions
141+
? await RescriptVersions.getDefaultVersions()
142+
: await RescriptVersions.promptVersions()
129143

130144
await createProject(~templateName, ~projectName, ~versions)
131145
}

src/RescriptVersions.res

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,42 @@ type versions = {rescriptVersion: string, rescriptCoreVersion: option<string>}
1111

1212
let spinnerMessage = "Loading available versions..."
1313

14+
let makeVersions = rescriptVersion => {
15+
let includesStdlib = CompareVersions.satisfies(rescriptVersion, includesStdlibVersionRange)
16+
let rescriptCoreVersion = includesStdlib ? None : Some(finalRescriptCoreVersion)
17+
18+
{rescriptVersion, rescriptCoreVersion}
19+
}
20+
21+
let getDefaultVersions = async () => {
22+
let s = P.spinner()
23+
24+
s->P.Spinner.start(spinnerMessage)
25+
26+
let rescriptVersionsResult = await NpmRegistry.getPackageVersions(
27+
"rescript",
28+
rescriptVersionRange,
29+
)
30+
31+
switch rescriptVersionsResult {
32+
| Ok(_) => s->P.Spinner.stop("Versions loaded.")
33+
| Error(_) => s->P.Spinner.stop(spinnerMessage)
34+
}
35+
36+
let rescriptVersion = switch rescriptVersionsResult {
37+
| Ok([]) => JsError.throwWithMessage("No supported ReScript versions were found.")
38+
| Ok([version]) => version
39+
| Ok(rescriptVersions) =>
40+
switch rescriptVersions->Array.find(version => !(version->String.includes("-"))) {
41+
| Some(version) => version
42+
| None => rescriptVersions[0]->Option.getOrThrow
43+
}
44+
| Error(error) => error->NpmRegistry.getFetchErrorMessage->JsError.throwWithMessage
45+
}
46+
47+
makeVersions(rescriptVersion)
48+
}
49+
1450
let promptVersions = async () => {
1551
let s = P.spinner()
1652

@@ -41,10 +77,7 @@ let promptVersions = async () => {
4177
| Error(error) => error->NpmRegistry.getFetchErrorMessage->JsError.throwWithMessage
4278
}
4379

44-
let includesStdlib = CompareVersions.satisfies(rescriptVersion, includesStdlibVersionRange)
45-
let rescriptCoreVersion = includesStdlib ? None : Some(finalRescriptCoreVersion)
46-
47-
{rescriptVersion, rescriptCoreVersion}
80+
makeVersions(rescriptVersion)
4881
}
4982

5083
let ensureYarnNodeModulesLinker = async () => {

src/RescriptVersions.resi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
type versions = {rescriptVersion: string, rescriptCoreVersion: option<string>}
22

3+
let getDefaultVersions: unit => promise<versions>
4+
35
let promptVersions: unit => promise<versions>
46

57
let installVersions: versions => promise<unit>

0 commit comments

Comments
 (0)