Skip to content

Commit 8643d4c

Browse files
Add res-x template (#123)
* Add res-x template * Use stdlib String.replaceAll
1 parent ea6af06 commit 8643d4c

46 files changed

Lines changed: 2041 additions & 13 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/NewProject.res

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ open Node
33
module P = ClackPrompts
44

55
let packageNameRegExp = /^[a-z0-9-]+$/
6+
let resxTemplatePlaceholderName = "resx-template"
67

78
let validateProjectName = projectName =>
89
if projectName->String.trim->String.length === 0 {
@@ -109,6 +110,72 @@ let promptTemplateName = async () => {
109110
}
110111
}
111112

113+
let rec replaceFileContents = async (remainingFilePaths, ~replaceValue, ~withValue) =>
114+
switch remainingFilePaths {
115+
| list{} => ()
116+
| list{filePath, ...remainingFilePaths} =>
117+
let fileContents = await Fs.Promises.readFile(filePath)
118+
let updatedFileContents = fileContents->String.replaceAll(replaceValue, withValue)
119+
120+
await Fs.Promises.writeFile(filePath, updatedFileContents)
121+
await replaceFileContents(remainingFilePaths, ~replaceValue, ~withValue)
122+
}
123+
124+
let synchronizeResxTemplateFiles = async (~projectName) =>
125+
await replaceFileContents(
126+
list{
127+
"package.json",
128+
"rescript.json",
129+
"README.md",
130+
"Dockerfile",
131+
"bun.lock",
132+
Path.join(["scripts", "build-sfe.mjs"]),
133+
Path.join(["src", "data", "TemplateContent.res"]),
134+
},
135+
~replaceValue=resxTemplatePlaceholderName,
136+
~withValue=projectName,
137+
)
138+
139+
let getPackageManagerName = (packageManager: PackageManagers.packageManager) =>
140+
switch packageManager {
141+
| Npm => "npm"
142+
| Yarn1 | YarnBerry => "yarn"
143+
| Pnpm => "pnpm"
144+
| Bun => "bun"
145+
}
146+
147+
let showGetStartedNote = async (~templateName, ~projectName) => {
148+
if templateName === Templates.resXTemplateName {
149+
let packageManagerInfo = await PackageManagers.getPackageManagerInfo()
150+
151+
switch packageManagerInfo.packageManager {
152+
| Bun =>
153+
P.note(
154+
~title="Get started",
155+
~message=`cd ${projectName}
156+
157+
bun run dev`,
158+
)
159+
| packageManager =>
160+
P.note(
161+
~title="Bun recommended",
162+
~message=`This ResX template is Bun-centric. You created it with ${packageManager->getPackageManagerName}, but the generated project should use Bun.
163+
164+
cd ${projectName}
165+
bun install
166+
bun run dev`,
167+
)
168+
}
169+
} else {
170+
P.note(
171+
~title="Get started",
172+
~message=`cd ${projectName}
173+
174+
# See the project's README.md for more information.`,
175+
)
176+
}
177+
}
178+
112179
let createProject = async (~templateName, ~projectName, ~versions) => {
113180
let templatePath = CraPaths.getTemplatePath(~templateName)
114181
let projectPath = Path.join2(Process.cwd(), projectName)
@@ -127,19 +194,18 @@ let createProject = async (~templateName, ~projectName, ~versions) => {
127194
await updateRescriptJson(~projectName, ~versions)
128195
await updateViteConfig()
129196

197+
if templateName === Templates.resXTemplateName {
198+
await synchronizeResxTemplateFiles(~projectName)
199+
}
200+
130201
await RescriptVersions.installVersions(versions)
131202
let _ = await Promisified.ChildProcess.exec("git init")
132203

133204
if !CI.isRunningInCI {
134205
s->P.Spinner.stop("Project created.")
135206
}
136207

137-
P.note(
138-
~title="Get started",
139-
~message=`cd ${projectName}
140-
141-
# See the project's README.md for more information.`,
142-
)
208+
await showGetStartedNote(~templateName, ~projectName)
143209
}
144210

145211
let createNewProject = async () => {
@@ -153,7 +219,10 @@ let createNewProject = async () => {
153219
~versions={rescriptVersion: "11.1.1", rescriptCoreVersion: Some("1.5.0")},
154220
)
155221
} else {
156-
let commandLineArguments = CommandLineArguments.fromProcessArgv(Process.argv)->Result.getOrThrow
222+
let commandLineArguments = switch CommandLineArguments.fromProcessArgv(Process.argv) {
223+
| Ok(commandLineArguments) => commandLineArguments
224+
| Error(message) => JsError.throwWithMessage(message)
225+
}
157226
let useDefaultVersions = Option.isSome(commandLineArguments.templateName)
158227

159228
let projectName = switch commandLineArguments.projectName {

src/Templates.res

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ let viteTemplateName = "rescript-template-vite"
1616
let nextjsTemplateName = "rescript-template-nextjs"
1717
let xoteTemplateName = "rescript-template-xote"
1818
let xoteSsrTemplateName = "rescript-template-xote-ssr"
19+
let resXTemplateName = "rescript-template-res-x"
1920
let templateNamePrefix = "rescript-template-"
2021

21-
let supportedTemplateNames = ["vite", "nextjs", "xote", "xote-ssr", "basic"]
22+
let supportedTemplateNames = ["vite", "nextjs", "xote", "xote-ssr", "res-x", "basic"]
2223

2324
let getTemplateName = templateName => {
2425
let templateName = templateName->String.toLowerCase
@@ -58,6 +59,12 @@ let templates = [
5859
},
5960
]),
6061
},
62+
{
63+
name: resXTemplateName,
64+
displayName: "ResX",
65+
shortDescription: "Bun SSR with ResX, Vite and Tailwind 4",
66+
variants: None,
67+
},
6168
{
6269
name: basicTemplateName,
6370
displayName: "Basic",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
node_modules
2+
.git
3+
dist
4+
build
5+
lib
6+
.playwright-cli
7+
.DS_Store
8+
*.log
9+
.env
10+
.env.*
11+
!.env.example
12+
AGENTS.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PORT=5557
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
FROM oven/bun:1 AS build
2+
3+
ENV HUSKY=0
4+
ENV NODE_ENV=production
5+
6+
WORKDIR /app
7+
8+
COPY package.json bun.lock ./
9+
RUN bun install --frozen-lockfile
10+
11+
COPY . .
12+
RUN bun run build:sfe
13+
14+
FROM debian:bookworm-slim
15+
16+
RUN apt-get update && \
17+
apt-get install -y --no-install-recommends ca-certificates tzdata && \
18+
rm -rf /var/lib/apt/lists/* && \
19+
groupadd --system app && \
20+
useradd --system --gid app --home-dir /app --create-home app
21+
22+
ENV NODE_ENV=production
23+
ENV PORT=5555
24+
ENV TZ=Europe/Stockholm
25+
26+
WORKDIR /app
27+
28+
COPY --from=build --chown=app:app /app/build/resx-template ./resx-template
29+
30+
USER app
31+
32+
EXPOSE 5555
33+
34+
CMD ["./resx-template"]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# resx-template
2+
3+
Generic ResX + ReScript starter.
4+
5+
Included in the base template:
6+
7+
- Bun server rendering with ResX
8+
- Vite + Tailwind asset pipeline
9+
- Bun single-file executable build path
10+
- Page metadata, sitemap, robots, and health endpoints
11+
- HTMX-backed server-rendered filter example
12+
- `assets/rescript-logo.svg` wired through `ResXAssets.assets`
13+
- `public/favicon.ico` wired through the document head
14+
- Static content module that is easy to rename and replace
15+
16+
## Quick start
17+
18+
1. `bun install`
19+
2. `bun run dev`
20+
3. Open `http://localhost:9201`
21+
22+
The dev script does one initial ReScript compile before starting the long-running watchers so the Bun server can boot on a fresh clone.
23+
24+
## Useful commands
25+
26+
- `bun run dev`
27+
- `bun run build`
28+
- `bun run start`
29+
- `bun run build:sfe`
30+
- `bun run start:sfe`
31+
- `bun run clean:res`
32+
33+
## Docker
34+
35+
Build and run the containerized executable:
36+
37+
1. `docker build -t resx-template .`
38+
2. `docker run --rm -p 5555:5555 resx-template`
39+
40+
The image runs the same standalone Bun executable produced by `build:sfe`.
41+
The page still loads HTMX from `unpkg`, so browsers opening the app need internet access to that CDN.
42+
43+
## Standalone executable
44+
45+
Build the standalone Bun single-file executable:
46+
47+
1. `bun run build:sfe`
48+
2. `cd build`
49+
3. `PORT=5557 NODE_ENV=production ./resx-template`
50+
51+
This follows the upstream ResX single-file executable path.
52+
53+
## Where to edit first
54+
55+
- `src/data/TemplateContent.res` for starter copy and example data
56+
- `src/pages/` for routes
57+
- `src/components/` for layout and reusable UI
58+
- `src/Server.res` for route matching, health checks, and response policy
59+
- `assets/` for transformed assets that should go through `ResXAssets.assets`
60+
- `public/` for direct top-level files like `/favicon.ico`
61+
62+
## Notes
63+
64+
- The base branch intentionally does not include Postgres, pgtyped, migrations, or auth.
65+
- Add database integration in a dedicated branch once the schema and deployment shape are real.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules
2+
.env
3+
dist
4+
build
5+
lib
6+
.DS_Store
7+
src/**/*.res.mjs
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
@import "tailwindcss" source(none);
2+
3+
@source "../src";
4+
5+
@theme inline {
6+
--font-sans:
7+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
8+
sans-serif;
9+
--font-mono:
10+
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
11+
--color-background: var(--background);
12+
--color-foreground: var(--foreground);
13+
--color-card: var(--card);
14+
--color-muted: var(--muted);
15+
--color-muted-foreground: var(--muted-foreground);
16+
--color-border: var(--border);
17+
--color-brand: var(--brand);
18+
--color-brand-foreground: var(--brand-foreground);
19+
--color-nav: var(--nav);
20+
--color-nav-foreground: var(--nav-foreground);
21+
--color-accent: var(--accent);
22+
--color-destructive: var(--destructive);
23+
}
24+
25+
:root {
26+
--background: #f7f8fb;
27+
--foreground: #10131a;
28+
--card: #ffffff;
29+
--muted: #eef1f5;
30+
--muted-foreground: #5c6779;
31+
--border: #d8dee8;
32+
--brand: #ef4444;
33+
--brand-foreground: #ffffff;
34+
--nav: #101827;
35+
--nav-foreground: #f4f7fb;
36+
--accent: #e8eef9;
37+
--destructive: #b91c1c;
38+
}
39+
40+
@layer base {
41+
* {
42+
border-color: var(--color-border);
43+
}
44+
45+
html {
46+
font-family: var(--font-sans);
47+
color-scheme: light;
48+
}
49+
50+
body {
51+
background-color: var(--color-background);
52+
color: var(--color-foreground);
53+
margin: 0;
54+
min-height: 100vh;
55+
text-rendering: optimizeLegibility;
56+
}
57+
58+
a {
59+
color: inherit;
60+
text-decoration: none;
61+
}
62+
}
63+
64+
@layer utilities {
65+
.grid-bg {
66+
background-image:
67+
linear-gradient(to right, rgb(16 24 39 / 0.06) 1px, transparent 1px),
68+
linear-gradient(to bottom, rgb(16 24 39 / 0.06) 1px, transparent 1px);
69+
background-size: 48px 48px;
70+
}
71+
}

0 commit comments

Comments
 (0)