create-fff-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +21 -0
  2. package/src/index.ts +119 -0
  3. package/template/.config/dotnet-tools.json +5 -0
  4. package/template/dev-server.ts +415 -0
  5. package/template/index.html +23 -0
  6. package/template/migrate.ts +166 -0
  7. package/template/migrations/0001_init.sql +25 -0
  8. package/template/migrations/0002_employees.sql +16 -0
  9. package/template/package.json +29 -0
  10. package/template/public/app.js +156 -0
  11. package/template/public/runtime.js +149 -0
  12. package/template/public/style.css +769 -0
  13. package/template/schema.sql +25 -0
  14. package/template/src/client/App.fs +55 -0
  15. package/template/src/client/Client.fsproj +24 -0
  16. package/template/src/client/Nav.fs +75 -0
  17. package/template/src/server/Domain/Employees/Employee.fs +34 -0
  18. package/template/src/server/Endpoints/Employees/Routes.fs +35 -0
  19. package/template/src/server/Endpoints/Pages/About.fjsx +66 -0
  20. package/template/src/server/Endpoints/Pages/EmployeeEdit.fjsx +66 -0
  21. package/template/src/server/Endpoints/Pages/EmployeeList.fjsx +54 -0
  22. package/template/src/server/Endpoints/Pages/EmployeeNew.fjsx +55 -0
  23. package/template/src/server/Endpoints/Pages/Layout.fjsx +40 -0
  24. package/template/src/server/Endpoints/Pages/Routes.fs +117 -0
  25. package/template/src/server/Endpoints/Pages/Templates.fs +45 -0
  26. package/template/src/server/Server.fsproj +28 -0
  27. package/template/src/server/Worker.fs +45 -0
  28. package/template/src/server/Workflows/Employees/EmployeeWorkflows.fs +86 -0
  29. package/template/webpack.client.js +18 -0
  30. package/template/wrangler.toml +23 -0
@@ -0,0 +1,45 @@
1
+ module App.Server.Endpoints.Pages.Templates
2
+
3
+ open Fable.Core
4
+ open Fable.Core.JsInterop
5
+
6
+ // ── Compiled FJSX page template modules ──────────────────────────────────────
7
+ // These static imports are resolved at Worker startup.
8
+ // The FJSX files must be compiled first:
9
+ // fjsx --dir src/server/Endpoints/Pages --out dist/server/Endpoints/Pages
10
+
11
+ [<ImportAll("./Layout.js")>]
12
+ let private layoutMod : obj = jsNative
13
+
14
+ [<ImportAll("./EmployeeList.js")>]
15
+ let private employeeListMod : obj = jsNative
16
+
17
+ [<ImportAll("./EmployeeNew.js")>]
18
+ let private employeeNewMod : obj = jsNative
19
+
20
+ [<ImportAll("./About.js")>]
21
+ let private aboutMod : obj = jsNative
22
+
23
+
24
+ [<ImportAll("./EmployeeEdit.js")>]
25
+ let private employeeEditMod : obj = jsNative
26
+ // ── Render helpers ────────────────────────────────────────────────────────────
27
+
28
+ let renderLayout (data: obj) : string = layoutMod?renderToHtml $ data
29
+ let renderEmployeeList (data: obj) : string = employeeListMod?renderToHtml $ data
30
+ let renderEmployeeNew (data: obj) : string = employeeNewMod?renderToHtml $ data
31
+ let renderAbout () : string = aboutMod?renderToHtml $ (createObj [])
32
+
33
+ let renderEmployeeEdit (data: obj) : string = employeeEditMod?renderToHtml $ data
34
+
35
+ /// Full-page HTML shell.
36
+ /// title — page title shown in <title> tag
37
+ /// activePath — used to highlight the active nav link
38
+ /// body — raw HTML injected into <main>
39
+ let shell (title: string) (activePath: string) (body: string) : string =
40
+ renderLayout (createObj [
41
+ "title" ==> title
42
+ "homeActive" ==> (activePath = "/")
43
+ "aboutActive" ==> (activePath = "/about")
44
+ "body" ==> body
45
+ ])
@@ -0,0 +1,28 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <TargetFramework>net9.0</TargetFramework>
5
+ <RootNamespace>App.Server</RootNamespace>
6
+ </PropertyGroup>
7
+
8
+ <ItemGroup>
9
+ <!-- Domain — pure types, no side effects -->
10
+ <Compile Include="Domain/Employees/Employee.fs" />
11
+
12
+ <!-- Workflows — business logic (may use Infrastructure) -->
13
+ <Compile Include="Workflows/Employees/EmployeeWorkflows.fs" />
14
+
15
+ <!-- Endpoints — HTTP layer (routes + page templates) -->
16
+ <Compile Include="Endpoints/Employees/Routes.fs" />
17
+ <Compile Include="Endpoints/Pages/Templates.fs" />
18
+ <Compile Include="Endpoints/Pages/Routes.fs" />
19
+
20
+ <!-- Entry point -->
21
+ <Compile Include="Worker.fs" />
22
+ </ItemGroup>
23
+
24
+ <ItemGroup>
25
+ <PackageReference Include="FffStack.Server" Version="0.1.0" />
26
+ </ItemGroup>
27
+
28
+ </Project>
@@ -0,0 +1,45 @@
1
+ module App.Server.Worker
2
+
3
+ open FffStack.Server
4
+ open Fable.Core
5
+
6
+ let router = Router()
7
+
8
+ // ── API routes (JSON) ─────────────────────────────────────────────────────────
9
+ Endpoints.Employees.Routes.register router
10
+
11
+ // ── Page routes (HTML, MPA) ───────────────────────────────────────────────────
12
+ Endpoints.Pages.Routes.register router
13
+
14
+ // ── Error pages ───────────────────────────────────────────────────────────────
15
+
16
+ router.OnNotFound(fun _ctx ->
17
+ promise {
18
+ let body = """
19
+ <section class="card">
20
+ <div class="card-label">404</div>
21
+ <h2>Page not found</h2>
22
+ <p class="text-muted" style="margin-top:0.5rem">The page you were looking for doesn't exist.</p>
23
+ <a href="/" class="btn btn-primary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
24
+ </section>"""
25
+ return Response.html (Endpoints.Pages.Templates.shell "Not Found" "" body)
26
+ }) |> ignore
27
+
28
+ router.OnError(fun _ctx ex ->
29
+ let msg = ex.Message
30
+ let body =
31
+ sprintf """
32
+ <section class="card">
33
+ <div class="card-label">500</div>
34
+ <h2>Something went wrong</h2>
35
+ <p class="text-muted" style="margin-top:0.5rem">%s</p>
36
+ <a href="/" class="btn btn-secondary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
37
+ </section>"""
38
+ msg
39
+ promise {
40
+ return Response.html (Endpoints.Pages.Templates.shell "Error" "" body)
41
+ }) |> ignore
42
+
43
+ // ── Cloudflare Workers entry point ────────────────────────────────────────────
44
+
45
+ router.ExportAsWorkerDefault()
@@ -0,0 +1,86 @@
1
+ module App.Server.Workflows.Employees
2
+
3
+ open FffStack.Server
4
+ open Fable.Core
5
+ open Fable.Core.JsInterop
6
+ open App.Server.Domain.Employees
7
+
8
+ [<Emit("crypto.randomUUID()")>]
9
+ let private newId () : string = jsNative
10
+
11
+ let getAll (db: ID1Database) : JS.Promise<Employee list> =
12
+ queryAll<Employee> db
13
+ "SELECT id, name, email, department, role, status, joined_at as joinedAt \
14
+ FROM employees ORDER BY name ASC"
15
+ [||]
16
+
17
+ let getById (db: ID1Database) (id: string) : JS.Promise<Employee option> =
18
+ queryOne<Employee> db
19
+ "SELECT id, name, email, department, role, status, joined_at as joinedAt \
20
+ FROM employees WHERE id = ?"
21
+ [| id |]
22
+
23
+ let create
24
+ (db: ID1Database)
25
+ (rawName: string)
26
+ (rawEmail: string)
27
+ (department: string)
28
+ (role: string)
29
+ (status: string)
30
+ : JS.Promise<Result<Employee, string>> =
31
+ promise {
32
+ match EmployeeName.create rawName, WorkEmail.create rawEmail with
33
+ | Error e, _ -> return Error e
34
+ | _, Error e -> return Error e
35
+ | Ok name, Ok email ->
36
+ let id = newId ()
37
+ let n = EmployeeName.value name
38
+ let em = WorkEmail.value email
39
+ let dep = department.Trim()
40
+ let rol = role.Trim()
41
+ let st = if status = "inactive" then "inactive" else "active"
42
+ do! execute db
43
+ "INSERT INTO employees (id, name, email, department, role, status, joined_at) \
44
+ VALUES (?, ?, ?, ?, ?, ?, date('now'))"
45
+ [| id; n; em; dep; rol; st |]
46
+ |> Promise.map ignore
47
+ return Ok { id = id; name = n; email = em; department = dep
48
+ role = rol; status = st; joinedAt = "" }
49
+ }
50
+
51
+ let update
52
+ (db: ID1Database)
53
+ (id: string)
54
+ (rawName: string)
55
+ (rawEmail: string)
56
+ (department: string)
57
+ (role: string)
58
+ (status: string)
59
+ : JS.Promise<Result<Employee, string>> =
60
+ promise {
61
+ match EmployeeName.create rawName, WorkEmail.create rawEmail with
62
+ | Error e, _ -> return Error e
63
+ | _, Error e -> return Error e
64
+ | Ok name, Ok email ->
65
+ let n = EmployeeName.value name
66
+ let em = WorkEmail.value email
67
+ let dep = department.Trim()
68
+ let rol = role.Trim()
69
+ let st = if status = "inactive" then "inactive" else "active"
70
+ do! execute db
71
+ "UPDATE employees SET name = ?, email = ?, department = ?, role = ?, status = ? WHERE id = ?"
72
+ [| n; em; dep; rol; st; id |]
73
+ |> Promise.map ignore
74
+ return Ok { id = id; name = n; email = em; department = dep
75
+ role = rol; status = st; joinedAt = "" }
76
+ }
77
+
78
+ let delete (db: ID1Database) (id: string) : JS.Promise<bool> =
79
+ promise {
80
+ match! getById db id with
81
+ | None -> return false
82
+ | Some _ ->
83
+ do! execute db "DELETE FROM employees WHERE id = ?" [| id |]
84
+ |> Promise.map ignore
85
+ return true
86
+ }
@@ -0,0 +1,18 @@
1
+ import { createRequire } from 'module';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ export default {
8
+ entry: './dist/client/fable/App.js',
9
+ output: {
10
+ path: path.resolve(__dirname, 'dist/client'),
11
+ filename: 'bundle.js',
12
+ },
13
+ mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
14
+ devtool: 'source-map',
15
+ resolve: {
16
+ extensions: ['.js'],
17
+ },
18
+ };
@@ -0,0 +1,23 @@
1
+ name = "fff-stack-app"
2
+ main = "dist/server/Worker.js"
3
+ compatibility_date = "2024-09-23"
4
+
5
+ # Client assets served from Cloudflare Pages / Workers Static Assets
6
+ [assets]
7
+ directory = "./dist/client"
8
+ binding = "ASSETS"
9
+
10
+ # Cloudflare D1 (SQLite)
11
+ [[d1_databases]]
12
+ binding = "DB"
13
+ database_name = "fff-stack-db"
14
+ database_id = "YOUR_D1_DATABASE_ID" # replace after: wrangler d1 create fff-stack-db
15
+
16
+ # Cloudflare R2 object storage
17
+ [[r2_buckets]]
18
+ binding = "BUCKET"
19
+ bucket_name = "fff-stack-bucket" # replace with your bucket name
20
+ # preview_bucket_name = "fff-stack-bucket-dev" # optional local dev bucket
21
+
22
+ [build]
23
+ command = "bun run build"