elm-ssr 1.0.5 → 1.0.7

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.
package/README.md CHANGED
@@ -48,12 +48,13 @@ Creates a new app under `<workspace>/<name>/` (or `<workspace>/<subdir>/<name>/`
48
48
  - `--db`: Configures SQLite database support and an initial migration.
49
49
  - `--auth <betterAuth|auth0>`: Scaffolds authentication views, cookies, and callback handlers.
50
50
 
51
- ### `route <path> [--app <name>] [--api] [--ws] [--sse]`
52
- Scaffolds a new route or endpoint:
51
+ ### `route <path> [--app <name>] [--api] [--ws] [--sse] [--resource]`
52
+ Scaffolds a new route or endpoint (uses `ElmSsr.Form` for modern full-stack examples):
53
53
  - `--app <app-name>`: Required if multiple apps are configured in the workspace.
54
54
  - `--api`: Scaffolds a JSON API page/action route instead of an HTML page.
55
55
  - `--ws` or `--websocket`: Scaffolds a TypeScript WebSocket handler in `src/Endpoints/<route>.ts`.
56
56
  - `--sse`: Scaffolds a TypeScript Server-Sent Events (SSE) stream handler in `src/Endpoints/<route>.ts`.
57
+ - `--resource`: Generates a richer CRUD-style example with `Form` validation + Elmto hints.
57
58
 
58
59
  ### `query [--app <name>] [--dir <path>] [--output <path>]`
59
60
  Generates type-safe Elm database schema and query helpers directly from raw SQL files in the migrations directory.
package/bin/elm-ssr.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { readFile, stat } from "node:fs/promises";
4
4
  import { watch } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { createAppScaffold, createRouteScaffold } from "../lib/scaffold.mjs";
6
+ import { createAppScaffold, createRouteScaffold, addAuthProvider, listAuthProviders, ensureScaffoldCodegen } from "../lib/scaffold.mjs";
7
7
  import { readWorkspaceConfig } from "../lib/workspace.mjs";
8
8
  import { build } from "../lib/build.mjs";
9
9
  import { migrate } from "../lib/migrate.mjs";
@@ -13,6 +13,12 @@ const defaultRootPath = process.cwd();
13
13
  const args = process.argv.slice(2);
14
14
  const command = args[0] ?? "help";
15
15
 
16
+ // Ensure Elm-based scaffold generator is available (hybrid Elm/JS for better debuggability).
17
+ // Also ensure on "build" so editing the Elm generator is reflected in dev/CI flows.
18
+ if (["new", "init", "route", "build"].includes(command)) {
19
+ await ensureScaffoldCodegen();
20
+ }
21
+
16
22
  const ownPkg = JSON.parse(
17
23
  await readFile(new URL("../package.json", import.meta.url), "utf8")
18
24
  );
@@ -45,7 +51,7 @@ const userRoot = findFlagValue("--root");
45
51
  let rootPath;
46
52
  if (userRoot) {
47
53
  rootPath = resolve(userRoot);
48
- } else if (["new", "init", "migrate", "help"].includes(command)) {
54
+ } else if (["new", "init", "migrate", "help", "version", "--version", "-v"].includes(command)) {
49
55
  rootPath = defaultRootPath;
50
56
  } else {
51
57
  rootPath = resolve(await findWorkspaceRoot(defaultRootPath));
@@ -91,6 +97,8 @@ const printHelp = () => {
91
97
  info Print current workspace package and configured app names
92
98
  migrate ... Apply / revert / inspect SQL migrations (see: elm-ssr migrate --help)
93
99
  version Print the elm-ssr version
100
+ auth add <provider> [--app <name>] Add an auth provider to an app (betterAuth | auth0)
101
+ auth list [--app <name>] List configured auth providers
94
102
  `);
95
103
  };
96
104
 
@@ -276,7 +284,8 @@ switch (command) {
276
284
  requireConfig();
277
285
  const routePath = args[1];
278
286
  if (!routePath) {
279
- console.error("Usage: elm-ssr route <path> [--app <app-name>] [--api] [--ws] [--sse]");
287
+ console.error("Usage: elm-ssr route <path> [--app <app-name>] [--api] [--ws] [--sse] [--resource]");
288
+ console.error(" --resource Scaffold a richer CRUD-ish page using Form (recommended for full-stack examples)");
280
289
  process.exit(1);
281
290
  }
282
291
 
@@ -301,8 +310,9 @@ switch (command) {
301
310
  const isApi = args.includes("--api");
302
311
  const isWs = args.includes("--ws") || args.includes("--websocket");
303
312
  const isSse = args.includes("--sse");
313
+ const isResource = args.includes("--resource");
304
314
 
305
- const result = await createRouteScaffold(rootPath, appConfig, routePath, { isApi, isWs, isSse });
315
+ const result = await createRouteScaffold(rootPath, appConfig, routePath, { isApi, isWs, isSse, isResource });
306
316
  console.log(`Scaffolded ${result.type} route at ${result.path}`);
307
317
  if (result.instructions) {
308
318
  console.log("\nInstructions to wire it up:\n" + result.instructions);
@@ -348,6 +358,84 @@ switch (command) {
348
358
  break;
349
359
  }
350
360
 
361
+ case "auth": {
362
+ const subcommand = args[1];
363
+
364
+ if (subcommand === "list") {
365
+ requireConfig();
366
+ const appName = findFlagValue("--app");
367
+ const apps = appName
368
+ ? config.apps.filter(a => a.name === appName)
369
+ : config.apps;
370
+ if (apps.length === 0) {
371
+ console.error(appName
372
+ ? `Error: App "${appName}" not found in elm-ssr.config.json`
373
+ : "Error: No apps found in elm-ssr.config.json");
374
+ process.exit(1);
375
+ }
376
+ for (const app of apps) {
377
+ const providers = await listAuthProviders(rootPath, app);
378
+ if (providers.length === 0) {
379
+ console.log(`${app.name}: no auth providers configured`);
380
+ } else {
381
+ console.log(`${app.name}: ${providers.join(", ")}`);
382
+ }
383
+ }
384
+ break;
385
+ }
386
+
387
+ if (subcommand === "add") {
388
+ requireConfig();
389
+ const providerArg = args[2];
390
+ if (!providerArg) {
391
+ console.error("Usage: elm-ssr auth add <provider> [--app <name>]");
392
+ console.error(" Providers: betterAuth, auth0");
393
+ process.exit(1);
394
+ }
395
+ const appName = findFlagValue("--app");
396
+ let appConfig;
397
+ if (appName) {
398
+ appConfig = config.apps.find(a => a.name === appName);
399
+ if (!appConfig) {
400
+ console.error(`Error: App "${appName}" not found in elm-ssr.config.json`);
401
+ process.exit(1);
402
+ }
403
+ } else if (config.apps.length === 1) {
404
+ appConfig = config.apps[0];
405
+ } else {
406
+ console.error("Error: Multiple apps found. Specify one with --app <name>");
407
+ console.error("Available:", config.apps.map(a => a.name).join(", "));
408
+ process.exit(1);
409
+ }
410
+ try {
411
+ const result = await addAuthProvider(rootPath, appConfig, providerArg);
412
+ console.log(`Added ${result.name} to ${appConfig.name}`);
413
+ if (result.warnings && result.warnings.length > 0) {
414
+ console.log(`\nWarnings:`);
415
+ for (const warning of result.warnings) console.log(` - ${warning}`);
416
+ }
417
+ console.log(`\nNext steps:`);
418
+ if (result.provider === "better-auth") {
419
+ console.log(` 1. Run: bun install`);
420
+ console.log(` 2. Run: elm-ssr migrate`);
421
+ console.log(` 3. Set BETTER_AUTH_SECRET in ${appConfig.root}/.dev.vars`);
422
+ } else {
423
+ console.log(` 1. Set AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET in ${appConfig.root}/.dev.vars`);
424
+ }
425
+ console.log(` 4. Run: bun run build`);
426
+ } catch (err) {
427
+ console.error(`Error: ${err.message}`);
428
+ process.exit(1);
429
+ }
430
+ break;
431
+ }
432
+
433
+ console.error("Usage: elm-ssr auth <add|list> [...]");
434
+ console.error(" elm-ssr auth add betterAuth --app my-app");
435
+ console.error(" elm-ssr auth list --app my-app");
436
+ process.exit(1);
437
+ }
438
+
351
439
  case "migrate":
352
440
  await migrate(args.slice(1));
353
441
  break;
@@ -13,6 +13,10 @@ module ElmSsr.Db.Dsl exposing
13
13
  , compileQuery
14
14
  )
15
15
 
16
+ {-| LEGACY. Elmto is canonical (ElmSsr.Db.Elmto + Repo + Query).
17
+ Generator no longer produces Dsl modules. This is for porting only.
18
+ -}
19
+
16
20
  import Json.Decode as Decode exposing (Decoder)
17
21
  import Json.Encode as Encode
18
22
  import ElmSsr.Loader as Loader exposing (Loader)
@@ -1,6 +1,11 @@
1
1
  module ElmSsr.Document.Events exposing (EventRef, decodeEventRef, encodeEventRef, findMessage)
2
2
 
3
- {-| Browser events are bridged without serializing `Msg`. An event handler is
3
+ {-| LEGACY / vestigial.
4
+
5
+ Only used to support very old page event wiring if any; islands should use
6
+ stock `elm/html` `Html.Events` + the client runtime.
7
+
8
+ Browser events are bridged without serializing `Msg`. An event handler is
4
9
  rendered as an `EventRef` (a DOM path + event name); when the browser reports an
5
10
  event, the runtime looks the message back up against the current view with
6
11
  [`findMessage`](#findMessage).
@@ -0,0 +1,470 @@
1
+ module ElmSsr.Form exposing
2
+ ( Decoder, Error, FieldDecoder
3
+ , required, optional, optionalWithDefault
4
+ , string, int, float, bool
5
+ , validate, custom
6
+ , succeed, fail, map, map2, map3, map4, map5, map6, map7, map8, andThen
7
+ , decode
8
+ , email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength
9
+ , errorFor, hasError, errorsFor
10
+ )
11
+
12
+ {-| Type-safe form and request validation.
13
+
14
+ Decoders are pure and can be used on the server (via Request data) and in islands
15
+ (via plain key/value pairs from model state). Errors accumulate.
16
+
17
+ Use on the server with `decode` + `Request.formData` (or the thin wrappers in
18
+ `ElmSsr.Request.Decode`).
19
+
20
+ # Core types
21
+ @docs Decoder, Error
22
+
23
+ # Building decoders
24
+ @docs required, optional, optionalWithDefault
25
+ @docs string, int, float, bool
26
+ @docs validate, custom
27
+ @docs succeed, fail, map, map2, map3, map4, map5, map6, map7, map8, andThen
28
+
29
+ # Running
30
+ @docs decode
31
+
32
+ # Validators
33
+ @docs email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength
34
+
35
+ # Error helpers (usable on server and client)
36
+ @docs errorFor, hasError, errorsFor
37
+ -}
38
+
39
+ import Json.Decode as JD
40
+ import Json.Encode as JE
41
+
42
+
43
+ type alias Error =
44
+ { field : String
45
+ , message : String
46
+ }
47
+
48
+
49
+ type Decoder a
50
+ = Decoder (List ( String, String ) -> Result (List Error) a)
51
+
52
+
53
+ type FieldDecoder a
54
+ = FieldDecoder (Maybe String -> Result String a)
55
+
56
+
57
+ decode : Decoder a -> List ( String, String ) -> Result (List Error) a
58
+ decode (Decoder dec) input =
59
+ dec input
60
+
61
+
62
+ decodeField : FieldDecoder a -> Maybe String -> Result Error a
63
+ decodeField (FieldDecoder fd) raw =
64
+ case fd raw of
65
+ Ok v ->
66
+ Ok v
67
+
68
+ Err msg ->
69
+ Err { field = "", message = msg }
70
+
71
+
72
+ required : String -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
73
+ required name (FieldDecoder fieldDec) (Decoder decFn) =
74
+ Decoder (\input ->
75
+ let
76
+ rawValue =
77
+ lookup name input
78
+
79
+ resFn =
80
+ decFn input
81
+
82
+ resVal =
83
+ case fieldDec rawValue of
84
+ Ok val ->
85
+ Ok val
86
+
87
+ Err msg ->
88
+ Err [ { field = name, message = msg } ]
89
+ in
90
+ combine resFn resVal
91
+ )
92
+
93
+
94
+ optional : String -> FieldDecoder b -> Decoder (Maybe b -> a) -> Decoder a
95
+ optional name (FieldDecoder fieldDec) (Decoder decFn) =
96
+ Decoder (\input ->
97
+ let
98
+ rawValue =
99
+ lookup name input
100
+
101
+ resFn =
102
+ decFn input
103
+
104
+ resVal =
105
+ case rawValue of
106
+ Nothing ->
107
+ Ok Nothing
108
+
109
+ Just "" ->
110
+ Ok Nothing
111
+
112
+ Just val ->
113
+ case fieldDec (Just val) of
114
+ Ok decoded ->
115
+ Ok (Just decoded)
116
+
117
+ Err msg ->
118
+ Err [ { field = name, message = msg } ]
119
+ in
120
+ combine resFn resVal
121
+ )
122
+
123
+
124
+ optionalWithDefault : String -> b -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
125
+ optionalWithDefault name defaultVal (FieldDecoder fieldDec) (Decoder decFn) =
126
+ Decoder (\input ->
127
+ let
128
+ rawValue =
129
+ lookup name input
130
+
131
+ resFn =
132
+ decFn input
133
+
134
+ resVal =
135
+ case rawValue of
136
+ Nothing ->
137
+ Ok defaultVal
138
+
139
+ Just "" ->
140
+ Ok defaultVal
141
+
142
+ Just val ->
143
+ case fieldDec (Just val) of
144
+ Ok decoded ->
145
+ Ok decoded
146
+
147
+ Err msg ->
148
+ Err [ { field = name, message = msg } ]
149
+ in
150
+ combine resFn resVal
151
+ )
152
+
153
+
154
+ string : FieldDecoder String
155
+ string =
156
+ FieldDecoder (\rawValue ->
157
+ case rawValue of
158
+ Just val ->
159
+ Ok val
160
+
161
+ Nothing ->
162
+ Err "Field is required"
163
+ )
164
+
165
+
166
+ int : FieldDecoder Int
167
+ int =
168
+ FieldDecoder (\rawValue ->
169
+ case rawValue of
170
+ Just val ->
171
+ case String.toInt val of
172
+ Just num ->
173
+ Ok num
174
+
175
+ Nothing ->
176
+ Err "Must be a valid integer"
177
+
178
+ Nothing ->
179
+ Err "Field is required"
180
+ )
181
+
182
+
183
+ float : FieldDecoder Float
184
+ float =
185
+ FieldDecoder (\rawValue ->
186
+ case rawValue of
187
+ Just val ->
188
+ case String.toFloat val of
189
+ Just num ->
190
+ Ok num
191
+
192
+ Nothing ->
193
+ Err "Must be a valid number"
194
+
195
+ Nothing ->
196
+ Err "Field is required"
197
+ )
198
+
199
+
200
+ bool : FieldDecoder Bool
201
+ bool =
202
+ FieldDecoder (\rawValue ->
203
+ case rawValue of
204
+ Just "true" ->
205
+ Ok True
206
+
207
+ Just "false" ->
208
+ Ok False
209
+
210
+ Just "on" ->
211
+ Ok True
212
+
213
+ Just "" ->
214
+ Ok False
215
+
216
+ Just _ ->
217
+ Ok False
218
+
219
+ Nothing ->
220
+ Ok False
221
+ )
222
+
223
+
224
+ validate : (a -> Result String a) -> FieldDecoder a -> FieldDecoder a
225
+ validate validator (FieldDecoder fieldDec) =
226
+ FieldDecoder (\rawValue ->
227
+ fieldDec rawValue
228
+ |> Result.andThen validator
229
+ )
230
+
231
+
232
+ custom : (a -> Result String b) -> FieldDecoder a -> FieldDecoder b
233
+ custom f (FieldDecoder fieldDec) =
234
+ FieldDecoder (\rawValue ->
235
+ fieldDec rawValue
236
+ |> Result.andThen f
237
+ )
238
+
239
+
240
+ succeed : a -> Decoder a
241
+ succeed val =
242
+ Decoder (\_ -> Ok val)
243
+
244
+
245
+ fail : String -> String -> Decoder a
246
+ fail fieldName msg =
247
+ Decoder (\_ -> Err [ { field = fieldName, message = msg } ])
248
+
249
+
250
+ map : (a -> b) -> Decoder a -> Decoder b
251
+ map f (Decoder dec) =
252
+ Decoder (\input -> Result.map f (dec input))
253
+
254
+
255
+ apply : Decoder a -> Decoder (a -> b) -> Decoder b
256
+ apply (Decoder decVal) (Decoder decFn) =
257
+ Decoder (\input ->
258
+ combine (decFn input) (decVal input)
259
+ )
260
+
261
+
262
+ map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
263
+ map2 f decA decB =
264
+ succeed f
265
+ |> apply decA
266
+ |> apply decB
267
+
268
+
269
+ map3 : (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d
270
+ map3 f decA decB decC =
271
+ succeed f
272
+ |> apply decA
273
+ |> apply decB
274
+ |> apply decC
275
+
276
+
277
+ map4 : (a -> b -> c -> d -> e) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e
278
+ map4 f decA decB decC decD =
279
+ succeed f
280
+ |> apply decA
281
+ |> apply decB
282
+ |> apply decC
283
+ |> apply decD
284
+
285
+
286
+ map5 : (a -> b -> c -> d -> e -> f) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f
287
+ map5 f decA decB decC decD decE =
288
+ succeed f
289
+ |> apply decA
290
+ |> apply decB
291
+ |> apply decC
292
+ |> apply decD
293
+ |> apply decE
294
+
295
+
296
+ map6 : (a -> b -> c -> d -> e -> f -> g) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g
297
+ map6 f decA decB decC decD decE decF =
298
+ succeed f
299
+ |> apply decA
300
+ |> apply decB
301
+ |> apply decC
302
+ |> apply decD
303
+ |> apply decE
304
+ |> apply decF
305
+
306
+
307
+ map7 : (a -> b -> c -> d -> e -> f -> g -> h) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h
308
+ map7 f decA decB decC decD decE decF decG =
309
+ succeed f
310
+ |> apply decA
311
+ |> apply decB
312
+ |> apply decC
313
+ |> apply decD
314
+ |> apply decE
315
+ |> apply decF
316
+ |> apply decG
317
+
318
+
319
+ map8 : (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h -> Decoder i
320
+ map8 f decA decB decC decD decE decF decG decH =
321
+ succeed f
322
+ |> apply decA
323
+ |> apply decB
324
+ |> apply decC
325
+ |> apply decD
326
+ |> apply decE
327
+ |> apply decF
328
+ |> apply decG
329
+ |> apply decH
330
+
331
+
332
+ andThen : (a -> Decoder b) -> Decoder a -> Decoder b
333
+ andThen f (Decoder decA) =
334
+ Decoder (\input ->
335
+ case decA input of
336
+ Ok a ->
337
+ let
338
+ (Decoder decB) =
339
+ f a
340
+ in
341
+ decB input
342
+
343
+ Err errs ->
344
+ Err errs
345
+ )
346
+
347
+
348
+ email : String -> Result String String
349
+ email val =
350
+ let
351
+ parts =
352
+ String.split "@" val
353
+ in
354
+ case parts of
355
+ [ _, domain ] ->
356
+ if String.contains "." domain then
357
+ Ok val
358
+
359
+ else
360
+ Err "Invalid email address"
361
+
362
+ _ ->
363
+ Err "Invalid email address"
364
+
365
+
366
+ nonEmpty : String -> Result String String
367
+ nonEmpty val =
368
+ if String.isEmpty (String.trim val) then
369
+ Err "Cannot be empty"
370
+
371
+ else
372
+ Ok val
373
+
374
+
375
+ minInt : Int -> Int -> Result String Int
376
+ minInt min val =
377
+ if val >= min then
378
+ Ok val
379
+
380
+ else
381
+ Err ("Must be at least " ++ String.fromInt min)
382
+
383
+
384
+ maxInt : Int -> Int -> Result String Int
385
+ maxInt max val =
386
+ if val <= max then
387
+ Ok val
388
+
389
+ else
390
+ Err ("Must be at most " ++ String.fromInt max)
391
+
392
+
393
+ minFloat : Float -> Float -> Result String Float
394
+ minFloat min val =
395
+ if val >= min then
396
+ Ok val
397
+
398
+ else
399
+ Err ("Must be at least " ++ String.fromFloat min)
400
+
401
+
402
+ maxFloat : Float -> Float -> Result String Float
403
+ maxFloat max val =
404
+ if val <= max then
405
+ Ok val
406
+
407
+ else
408
+ Err ("Must be at most " ++ String.fromFloat max)
409
+
410
+
411
+ minLength : Int -> String -> Result String String
412
+ minLength len val =
413
+ if String.length val >= len then
414
+ Ok val
415
+
416
+ else
417
+ Err ("Must be at least " ++ String.fromInt len ++ " characters")
418
+
419
+
420
+ maxLength : Int -> String -> Result String String
421
+ maxLength len val =
422
+ if String.length val <= len then
423
+ Ok val
424
+
425
+ else
426
+ Err ("Must be at most " ++ String.fromInt len ++ " characters")
427
+
428
+
429
+ errorFor : String -> List Error -> Maybe String
430
+ errorFor field errors =
431
+ errors
432
+ |> List.filter (\e -> e.field == field)
433
+ |> List.head
434
+ |> Maybe.map .message
435
+
436
+
437
+ hasError : String -> List Error -> Bool
438
+ hasError field errors =
439
+ List.any (\e -> e.field == field) errors
440
+
441
+
442
+ errorsFor : String -> List Error -> List String
443
+ errorsFor field errors =
444
+ errors
445
+ |> List.filter (\e -> e.field == field)
446
+ |> List.map .message
447
+
448
+
449
+ lookup : String -> List ( String, String ) -> Maybe String
450
+ lookup key pairs =
451
+ pairs
452
+ |> List.filter (\( name, _ ) -> name == key)
453
+ |> List.head
454
+ |> Maybe.map Tuple.second
455
+
456
+
457
+ combine : Result (List Error) (a -> b) -> Result (List Error) a -> Result (List Error) b
458
+ combine resFn resVal =
459
+ case ( resFn, resVal ) of
460
+ ( Ok fn, Ok val ) ->
461
+ Ok (fn val)
462
+
463
+ ( Err errsFn, Err errsVal ) ->
464
+ Err (errsFn ++ errsVal)
465
+
466
+ ( Err errs, Ok _ ) ->
467
+ Err errs
468
+
469
+ ( Ok _, Err errs ) ->
470
+ Err errs
@@ -5,6 +5,10 @@ module ElmSsr.Html.Events exposing
5
5
  , onBlur, onFocus
6
6
  )
7
7
 
8
+ {-| LEGACY / vestigial. Pages are static SSR; events belong in islands using stock elm/html Html.Events.
9
+ Kept only if Document.Events still references for legacy findMessage.
10
+ -}
11
+
8
12
  {-| Event handlers. Mirrors `elm/html`'s `Html.Events`.
9
13
 
10
14
  Handlers are bridged by DOM path and event name, not by serializing `Msg`, so