Skip to content

Commit 5dd0a7d

Browse files
Add res-x template
1 parent c2878d5 commit 5dd0a7d

46 files changed

Lines changed: 2038 additions & 12 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: 74 additions & 6 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 {
@@ -70,6 +74,72 @@ let getTemplateOptions = () =>
7074
hint: shortDescription,
7175
})
7276

77+
let rec replaceFileContents = async (remainingFilePaths, ~replaceValue, ~withValue) =>
78+
switch remainingFilePaths {
79+
| list{} => ()
80+
| list{filePath, ...remainingFilePaths} =>
81+
let fileContents = await Fs.Promises.readFile(filePath)
82+
let updatedFileContents = fileContents->StringPrototype.replaceAll(replaceValue, withValue)
83+
84+
await Fs.Promises.writeFile(filePath, updatedFileContents)
85+
await replaceFileContents(remainingFilePaths, ~replaceValue, ~withValue)
86+
}
87+
88+
let synchronizeResxTemplateFiles = async (~projectName) =>
89+
await replaceFileContents(
90+
list{
91+
"package.json",
92+
"rescript.json",
93+
"README.md",
94+
"Dockerfile",
95+
"bun.lock",
96+
Path.join(["scripts", "build-sfe.mjs"]),
97+
Path.join(["src", "data", "TemplateContent.res"]),
98+
},
99+
~replaceValue=resxTemplatePlaceholderName,
100+
~withValue=projectName,
101+
)
102+
103+
let getPackageManagerName = (packageManager: PackageManagers.packageManager) =>
104+
switch packageManager {
105+
| Npm => "npm"
106+
| Yarn1 | YarnBerry => "yarn"
107+
| Pnpm => "pnpm"
108+
| Bun => "bun"
109+
}
110+
111+
let showGetStartedNote = async (~templateName, ~projectName) => {
112+
if templateName === Templates.resxTemplateName {
113+
let packageManagerInfo = await PackageManagers.getPackageManagerInfo()
114+
115+
switch packageManagerInfo.packageManager {
116+
| Bun =>
117+
P.note(
118+
~title="Get started",
119+
~message=`cd ${projectName}
120+
121+
bun run dev`,
122+
)
123+
| packageManager =>
124+
P.note(
125+
~title="Bun recommended",
126+
~message=`This ResX template is Bun-centric. You created it with ${packageManager->getPackageManagerName}, but the generated project should use Bun.
127+
128+
cd ${projectName}
129+
bun install
130+
bun run dev`,
131+
)
132+
}
133+
} else {
134+
P.note(
135+
~title="Get started",
136+
~message=`cd ${projectName}
137+
138+
# See the project's README.md for more information.`,
139+
)
140+
}
141+
}
142+
73143
let createProject = async (~templateName, ~projectName, ~versions) => {
74144
let templatePath = CraPaths.getTemplatePath(~templateName)
75145
let projectPath = Path.join2(Process.cwd(), projectName)
@@ -86,6 +156,9 @@ let createProject = async (~templateName, ~projectName, ~versions) => {
86156
await Fs.Promises.rename("_gitignore", ".gitignore")
87157
await updatePackageJson(~projectName, ~versions)
88158
await updateRescriptJson(~projectName, ~versions)
159+
if templateName === Templates.resxTemplateName {
160+
await synchronizeResxTemplateFiles(~projectName)
161+
}
89162

90163
await RescriptVersions.installVersions(versions)
91164
let _ = await Promisified.ChildProcess.exec("git init")
@@ -94,12 +167,7 @@ let createProject = async (~templateName, ~projectName, ~versions) => {
94167
s->P.Spinner.stop("Project created.")
95168
}
96169

97-
P.note(
98-
~title="Get started",
99-
~message=`cd ${projectName}
100-
101-
# See the project's README.md for more information.`,
102-
)
170+
await showGetStartedNote(~templateName, ~projectName)
103171
}
104172

105173
let createNewProject = async () => {

‎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-resx"
1011
let templateNamePrefix = "rescript-template-"
1112

12-
let supportedTemplateNames = ["vite", "nextjs", "basic"]
13+
let supportedTemplateNames = ["vite", "nextjs", "resx", "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)