Skip to content

Commit 99e0604

Browse files
Add res-x template
1 parent 88d90d9 commit 99e0604

46 files changed

Lines changed: 2043 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: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
open Node
22

33
module P = ClackPrompts
4+
module StringPrototype = {
5+
@send external replaceAll: (string, string, string) => string = "replaceAll"
6+
}
47

58
let packageNameRegExp = /^[a-z0-9-]+$/
9+
let resxTemplatePlaceholderName = "resx-template"
610

711
let validateProjectName = projectName =>
812
if projectName->String.trim->String.length === 0 {
@@ -86,6 +90,72 @@ let getTemplateOptions = () =>
8690
hint: shortDescription,
8791
})
8892

93+
let rec replaceFileContents = async (remainingFilePaths, ~replaceValue, ~withValue) =>
94+
switch remainingFilePaths {
95+
| list{} => ()
96+
| list{filePath, ...remainingFilePaths} =>
97+
let fileContents = await Fs.Promises.readFile(filePath)
98+
let updatedFileContents = fileContents->StringPrototype.replaceAll(replaceValue, withValue)
99+
100+
await Fs.Promises.writeFile(filePath, updatedFileContents)
101+
await replaceFileContents(remainingFilePaths, ~replaceValue, ~withValue)
102+
}
103+
104+
let synchronizeResxTemplateFiles = async (~projectName) =>
105+
await replaceFileContents(
106+
list{
107+
"package.json",
108+
"rescript.json",
109+
"README.md",
110+
"Dockerfile",
111+
"bun.lock",
112+
Path.join(["scripts", "build-sfe.mjs"]),
113+
Path.join(["src", "data", "TemplateContent.res"]),
114+
},
115+
~replaceValue=resxTemplatePlaceholderName,
116+
~withValue=projectName,
117+
)
118+
119+
let getPackageManagerName = (packageManager: PackageManagers.packageManager) =>
120+
switch packageManager {
121+
| Npm => "npm"
122+
| Yarn1 | YarnBerry => "yarn"
123+
| Pnpm => "pnpm"
124+
| Bun => "bun"
125+
}
126+
127+
let showGetStartedNote = async (~templateName, ~projectName) => {
128+
if templateName === Templates.resXTemplateName {
129+
let packageManagerInfo = await PackageManagers.getPackageManagerInfo()
130+
131+
switch packageManagerInfo.packageManager {
132+
| Bun =>
133+
P.note(
134+
~title="Get started",
135+
~message=`cd ${projectName}
136+
137+
bun run dev`,
138+
)
139+
| packageManager =>
140+
P.note(
141+
~title="Bun recommended",
142+
~message=`This ResX template is Bun-centric. You created it with ${packageManager->getPackageManagerName}, but the generated project should use Bun.
143+
144+
cd ${projectName}
145+
bun install
146+
bun run dev`,
147+
)
148+
}
149+
} else {
150+
P.note(
151+
~title="Get started",
152+
~message=`cd ${projectName}
153+
154+
# See the project's README.md for more information.`,
155+
)
156+
}
157+
}
158+
89159
let createProject = async (~templateName, ~projectName, ~versions) => {
90160
let templatePath = CraPaths.getTemplatePath(~templateName)
91161
let projectPath = Path.join2(Process.cwd(), projectName)
@@ -104,19 +174,18 @@ let createProject = async (~templateName, ~projectName, ~versions) => {
104174
await updateRescriptJson(~projectName, ~versions)
105175
await updateViteConfig()
106176

177+
if templateName === Templates.resXTemplateName {
178+
await synchronizeResxTemplateFiles(~projectName)
179+
}
180+
107181
await RescriptVersions.installVersions(versions)
108182
let _ = await Promisified.ChildProcess.exec("git init")
109183

110184
if !CI.isRunningInCI {
111185
s->P.Spinner.stop("Project created.")
112186
}
113187

114-
P.note(
115-
~title="Get started",
116-
~message=`cd ${projectName}
117-
118-
# See the project's README.md for more information.`,
119-
)
188+
await showGetStartedNote(~templateName, ~projectName)
120189
}
121190

122191
let createNewProject = async () => {
@@ -130,7 +199,10 @@ let createNewProject = async () => {
130199
~versions={rescriptVersion: "11.1.1", rescriptCoreVersion: Some("1.5.0")},
131200
)
132201
} else {
133-
let commandLineArguments = CommandLineArguments.fromProcessArgv(Process.argv)->Result.getOrThrow
202+
let commandLineArguments = switch CommandLineArguments.fromProcessArgv(Process.argv) {
203+
| Ok(commandLineArguments) => commandLineArguments
204+
| Error(message) => JsError.throwWithMessage(message)
205+
}
134206
let useDefaultVersions = Option.isSome(commandLineArguments.templateName)
135207

136208
let projectName = switch commandLineArguments.projectName {

‎src/Templates.res‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ type t = {
77
let basicTemplateName = "rescript-template-basic"
88
let viteTemplateName = "rescript-template-vite"
99
let nextjsTemplateName = "rescript-template-nextjs"
10+
let resXTemplateName = "rescript-template-res-x"
1011
let templateNamePrefix = "rescript-template-"
1112

12-
let supportedTemplateNames = ["vite", "nextjs", "basic"]
13+
let supportedTemplateNames = ["vite", "nextjs", "res-x", "basic"]
1314

1415
let getTemplateName = templateName => {
1516
let templateName = templateName->String.toLowerCase
@@ -30,6 +31,11 @@ let templates = [
3031
displayName: "Next.js",
3132
shortDescription: "Next.js 15 with static export and Tailwind 3",
3233
},
34+
{
35+
name: resXTemplateName,
36+
displayName: "ResX",
37+
shortDescription: "Bun SSR with ResX, Vite and Tailwind 4",
38+
},
3339
{
3440
name: basicTemplateName,
3541
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)