Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 78 additions & 7 deletions src/NewProject.res
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
open Node

module P = ClackPrompts
module StringPrototype = {
@send external replaceAll: (string, string, string) => string = "replaceAll"
}

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

let validateProjectName = projectName =>
if projectName->String.trim->String.length === 0 {
Expand Down Expand Up @@ -70,6 +74,72 @@ let getTemplateOptions = () =>
hint: shortDescription,
})

let rec replaceFileContents = async (remainingFilePaths, ~replaceValue, ~withValue) =>
switch remainingFilePaths {
| list{} => ()
| list{filePath, ...remainingFilePaths} =>
let fileContents = await Fs.Promises.readFile(filePath)
let updatedFileContents = fileContents->StringPrototype.replaceAll(replaceValue, withValue)

await Fs.Promises.writeFile(filePath, updatedFileContents)
await replaceFileContents(remainingFilePaths, ~replaceValue, ~withValue)
}

let synchronizeResxTemplateFiles = async (~projectName) =>
await replaceFileContents(
list{
"package.json",
"rescript.json",
"README.md",
"Dockerfile",
"bun.lock",
Path.join(["scripts", "build-sfe.mjs"]),
Path.join(["src", "data", "TemplateContent.res"]),
},
~replaceValue=resxTemplatePlaceholderName,
~withValue=projectName,
)

let getPackageManagerName = (packageManager: PackageManagers.packageManager) =>
switch packageManager {
| Npm => "npm"
| Yarn1 | YarnBerry => "yarn"
| Pnpm => "pnpm"
| Bun => "bun"
}

let showGetStartedNote = async (~templateName, ~projectName) => {
if templateName === Templates.resXTemplateName {
let packageManagerInfo = await PackageManagers.getPackageManagerInfo()

switch packageManagerInfo.packageManager {
| Bun =>
P.note(
~title="Get started",
~message=`cd ${projectName}

bun run dev`,
)
| packageManager =>
P.note(
~title="Bun recommended",
~message=`This ResX template is Bun-centric. You created it with ${packageManager->getPackageManagerName}, but the generated project should use Bun.

cd ${projectName}
bun install
bun run dev`,
)
}
} else {
P.note(
~title="Get started",
~message=`cd ${projectName}

# See the project's README.md for more information.`,
)
}
}

let createProject = async (~templateName, ~projectName, ~versions) => {
let templatePath = CraPaths.getTemplatePath(~templateName)
let projectPath = Path.join2(Process.cwd(), projectName)
Expand All @@ -86,6 +156,9 @@ let createProject = async (~templateName, ~projectName, ~versions) => {
await Fs.Promises.rename("_gitignore", ".gitignore")
await updatePackageJson(~projectName, ~versions)
await updateRescriptJson(~projectName, ~versions)
if templateName === Templates.resXTemplateName {
await synchronizeResxTemplateFiles(~projectName)
}

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

P.note(
~title="Get started",
~message=`cd ${projectName}

# See the project's README.md for more information.`,
)
await showGetStartedNote(~templateName, ~projectName)
}

let createNewProject = async () => {
Expand All @@ -113,7 +181,10 @@ let createNewProject = async () => {
~versions={rescriptVersion: "11.1.1", rescriptCoreVersion: Some("1.5.0")},
)
} else {
let commandLineArguments = CommandLineArguments.fromProcessArgv(Process.argv)->Result.getOrThrow
let commandLineArguments = switch CommandLineArguments.fromProcessArgv(Process.argv) {
| Ok(commandLineArguments) => commandLineArguments
| Error(message) => JsError.throwWithMessage(message)
}
let useDefaultVersions = Option.isSome(commandLineArguments.templateName)

let projectName = switch commandLineArguments.projectName {
Expand Down
8 changes: 7 additions & 1 deletion src/Templates.res
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ type t = {
let basicTemplateName = "rescript-template-basic"
let viteTemplateName = "rescript-template-vite"
let nextjsTemplateName = "rescript-template-nextjs"
let resXTemplateName = "rescript-template-res-x"
let templateNamePrefix = "rescript-template-"

let supportedTemplateNames = ["vite", "nextjs", "basic"]
let supportedTemplateNames = ["vite", "nextjs", "res-x", "basic"]

let getTemplateName = templateName => {
let templateName = templateName->String.toLowerCase
Expand All @@ -30,6 +31,11 @@ let templates = [
displayName: "Next.js",
shortDescription: "Next.js 15 with static export and Tailwind 3",
},
{
name: resXTemplateName,
displayName: "ResX",
shortDescription: "Bun SSR with ResX, Vite and Tailwind 4",
},
{
name: basicTemplateName,
displayName: "Basic",
Expand Down
12 changes: 12 additions & 0 deletions templates/rescript-template-res-x/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
.git
dist
build
lib
.playwright-cli
.DS_Store
*.log
.env
.env.*
!.env.example
AGENTS.md
1 change: 1 addition & 0 deletions templates/rescript-template-res-x/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PORT=5557
34 changes: 34 additions & 0 deletions templates/rescript-template-res-x/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM oven/bun:1 AS build

ENV HUSKY=0
ENV NODE_ENV=production

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build:sfe

FROM debian:bookworm-slim

RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates tzdata && \
rm -rf /var/lib/apt/lists/* && \
groupadd --system app && \
useradd --system --gid app --home-dir /app --create-home app

ENV NODE_ENV=production
ENV PORT=5555
ENV TZ=Europe/Stockholm

WORKDIR /app

COPY --from=build --chown=app:app /app/build/resx-template ./resx-template

USER app

EXPOSE 5555

CMD ["./resx-template"]
65 changes: 65 additions & 0 deletions templates/rescript-template-res-x/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# resx-template

Generic ResX + ReScript starter.

Included in the base template:

- Bun server rendering with ResX
- Vite + Tailwind asset pipeline
- Bun single-file executable build path
- Page metadata, sitemap, robots, and health endpoints
- HTMX-backed server-rendered filter example
- `assets/rescript-logo.svg` wired through `ResXAssets.assets`
- `public/favicon.ico` wired through the document head
- Static content module that is easy to rename and replace

## Quick start

1. `bun install`
2. `bun run dev`
3. Open `http://localhost:9201`

The dev script does one initial ReScript compile before starting the long-running watchers so the Bun server can boot on a fresh clone.

## Useful commands

- `bun run dev`
- `bun run build`
- `bun run start`
- `bun run build:sfe`
- `bun run start:sfe`
- `bun run clean:res`

## Docker

Build and run the containerized executable:

1. `docker build -t resx-template .`
2. `docker run --rm -p 5555:5555 resx-template`

The image runs the same standalone Bun executable produced by `build:sfe`.
The page still loads HTMX from `unpkg`, so browsers opening the app need internet access to that CDN.

## Standalone executable

Build the standalone Bun single-file executable:

1. `bun run build:sfe`
2. `cd build`
3. `PORT=5557 NODE_ENV=production ./resx-template`

This follows the upstream ResX single-file executable path.

## Where to edit first

- `src/data/TemplateContent.res` for starter copy and example data
- `src/pages/` for routes
- `src/components/` for layout and reusable UI
- `src/Server.res` for route matching, health checks, and response policy
- `assets/` for transformed assets that should go through `ResXAssets.assets`
- `public/` for direct top-level files like `/favicon.ico`

## Notes

- The base branch intentionally does not include Postgres, pgtyped, migrations, or auth.
- Add database integration in a dedicated branch once the schema and deployment shape are real.
7 changes: 7 additions & 0 deletions templates/rescript-template-res-x/_gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.env
dist
build
lib
.DS_Store
src/**/*.res.mjs
1 change: 1 addition & 0 deletions templates/rescript-template-res-x/assets/rescript-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions templates/rescript-template-res-x/assets/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
@import "tailwindcss" source(none);

@source "../src";

@theme inline {
--font-sans:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
--font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-nav: var(--nav);
--color-nav-foreground: var(--nav-foreground);
--color-accent: var(--accent);
--color-destructive: var(--destructive);
}

:root {
--background: #f7f8fb;
--foreground: #10131a;
--card: #ffffff;
--muted: #eef1f5;
--muted-foreground: #5c6779;
--border: #d8dee8;
--brand: #ef4444;
--brand-foreground: #ffffff;
--nav: #101827;
--nav-foreground: #f4f7fb;
--accent: #e8eef9;
--destructive: #b91c1c;
}

@layer base {
* {
border-color: var(--color-border);
}

html {
font-family: var(--font-sans);
color-scheme: light;
}

body {
background-color: var(--color-background);
color: var(--color-foreground);
margin: 0;
min-height: 100vh;
text-rendering: optimizeLegibility;
}

a {
color: inherit;
text-decoration: none;
}
}

@layer utilities {
.grid-bg {
background-image:
linear-gradient(to right, rgb(16 24 39 / 0.06) 1px, transparent 1px),
linear-gradient(to bottom, rgb(16 24 39 / 0.06) 1px, transparent 1px);
background-size: 48px 48px;
}
}
Loading
Loading