elm-ssr 0.6.2 → 0.90.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.
package/lib/query.mjs ADDED
@@ -0,0 +1,464 @@
1
+ import { readdir, readFile, writeFile, mkdir, stat } from "node:fs/promises";
2
+ import { resolve, dirname, basename } from "node:path";
3
+
4
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
5
+
6
+ const snakeToCamel = (str) => {
7
+ return str.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
8
+ };
9
+
10
+ const singularize = (str) => {
11
+ if (str.endsWith("ies")) return str.slice(0, -3) + "y";
12
+ if (str.endsWith("es") && (str.endsWith("shes") || str.endsWith("ches") || str.endsWith("xes"))) return str.slice(0, -2);
13
+ if (str.endsWith("s") && !str.endsWith("ss")) return str.slice(0, -1);
14
+ return str;
15
+ };
16
+
17
+ const scanCreateTables = (sqlText) => {
18
+ const tables = [];
19
+ // Case-insensitive match for CREATE TABLE table_name (
20
+ const regex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s*\(/gi;
21
+ let match;
22
+
23
+ while ((match = regex.exec(sqlText)) !== null) {
24
+ const tableName = match[1];
25
+ const startIndex = regex.lastIndex; // index after the opening '('
26
+ let parenDepth = 1;
27
+ let endIndex = -1;
28
+
29
+ for (let i = startIndex; i < sqlText.length; i++) {
30
+ const char = sqlText[i];
31
+ if (char === "(") parenDepth++;
32
+ if (char === ")") parenDepth--;
33
+ if (parenDepth === 0) {
34
+ endIndex = i;
35
+ break;
36
+ }
37
+ }
38
+
39
+ if (endIndex !== -1) {
40
+ const body = sqlText.slice(startIndex, endIndex);
41
+ tables.push({ name: tableName, body });
42
+ regex.lastIndex = endIndex + 1;
43
+ }
44
+ }
45
+ return tables;
46
+ };
47
+
48
+ const splitColumns = (body) => {
49
+ const cols = [];
50
+ let current = "";
51
+ let parenDepth = 0;
52
+ for (let i = 0; i < body.length; i++) {
53
+ const char = body[i];
54
+ if (char === "(") parenDepth++;
55
+ if (char === ")") parenDepth--;
56
+ if (char === "," && parenDepth === 0) {
57
+ cols.push(current.trim());
58
+ current = "";
59
+ } else {
60
+ current += char;
61
+ }
62
+ }
63
+ if (current.trim()) {
64
+ cols.push(current.trim());
65
+ }
66
+ return cols;
67
+ };
68
+
69
+ const mapSqlTypeToElm = (sqlType) => {
70
+ switch (sqlType) {
71
+ case "INT":
72
+ case "INTEGER":
73
+ case "SERIAL":
74
+ case "BIGINT":
75
+ case "SMALLINT":
76
+ case "TINYINT":
77
+ return "Int";
78
+ case "REAL":
79
+ case "FLOAT":
80
+ case "DOUBLE":
81
+ case "DECIMAL":
82
+ case "NUMERIC":
83
+ return "Float";
84
+ case "BOOLEAN":
85
+ case "BOOL":
86
+ return "Bool";
87
+ case "TEXT":
88
+ case "VARCHAR":
89
+ case "CHAR":
90
+ case "UUID":
91
+ case "TIMESTAMP":
92
+ case "DATE":
93
+ case "TIME":
94
+ return "String";
95
+ default:
96
+ console.warn(`[elm-ssr query] Unknown SQL type "${sqlType}", falling back to String.`);
97
+ return "String";
98
+ }
99
+ };
100
+
101
+ const parseColumnDefinition = (colStr) => {
102
+ const trimmed = colStr.replace(/\s+/g, " ").trim();
103
+
104
+ // Skip constraints
105
+ if (/^(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CONSTRAINT|CHECK)\b/i.test(trimmed)) {
106
+ return null;
107
+ }
108
+
109
+ const tokens = trimmed.split(" ");
110
+ if (tokens.length < 2) return null;
111
+
112
+ const rawName = tokens[0];
113
+ const dbName = rawName.replace(/[`"]/g, "");
114
+ const elmName = snakeToCamel(dbName);
115
+
116
+ const rawType = tokens[1].replace(/\([\s\S]*?\)/g, "").toUpperCase();
117
+ const elmType = mapSqlTypeToElm(rawType);
118
+
119
+ const isPrimaryKey = /\bPRIMARY\s+KEY\b/i.test(trimmed);
120
+ const isNullable = !/\bNOT\s+NULL\b/i.test(trimmed) && !isPrimaryKey;
121
+ const hasDefault = /\bDEFAULT\b/i.test(trimmed);
122
+
123
+ const isAutoIncrement = /\bAUTOINCREMENT\b/i.test(trimmed) ||
124
+ (isPrimaryKey && (rawType === "INTEGER" || rawType === "INT")) ||
125
+ rawType === "SERIAL";
126
+
127
+ return {
128
+ dbName,
129
+ elmName,
130
+ elmType,
131
+ isNullable,
132
+ isPrimaryKey,
133
+ hasDefault,
134
+ isAutoIncrement
135
+ };
136
+ };
137
+
138
+ const getFieldDecoder = (c) => {
139
+ let baseDec = "";
140
+ if (c.elmType === "Int") baseDec = "Decode.int";
141
+ else if (c.elmType === "Float") baseDec = "Decode.float";
142
+ else if (c.elmType === "Bool") baseDec = "boolDecoder";
143
+ else baseDec = "Decode.string";
144
+
145
+ if (c.isNullable) {
146
+ return `(Decode.field "${c.dbName}" (Decode.nullable ${baseDec}))`;
147
+ } else {
148
+ return `(Decode.field "${c.dbName}" ${baseDec})`;
149
+ }
150
+ };
151
+
152
+ const getEncoderExpr = (c, valName) => {
153
+ let baseEnc = "";
154
+ if (c.elmType === "Int") baseEnc = "Encode.int";
155
+ else if (c.elmType === "Float") baseEnc = "Encode.float";
156
+ else if (c.elmType === "Bool") baseEnc = "Encode.bool";
157
+ else baseEnc = "Encode.string";
158
+
159
+ if (c.isNullable) {
160
+ return `encodeNullable ${baseEnc} ${valName}`;
161
+ } else {
162
+ return `${baseEnc} ${valName}`;
163
+ }
164
+ };
165
+
166
+ const generatePipelineDecoder = (recordName, columns) => {
167
+ let code = "";
168
+ let indent = "";
169
+ for (let i = 0; i < columns.length; i++) {
170
+ const c = columns[i];
171
+ const dec = getFieldDecoder(c);
172
+ code += `${indent}${dec}\n${indent} |> Decode.andThen (\\${c.elmName} ->\n`;
173
+ indent += " ";
174
+ }
175
+
176
+ const fieldsAssignment = columns.map(c => `${c.elmName} = ${c.elmName}`).join(", ");
177
+ code += `${indent}Decode.succeed { ${fieldsAssignment} }\n`;
178
+
179
+ for (let i = 0; i < columns.length; i++) {
180
+ code += `)`;
181
+ }
182
+ return code;
183
+ };
184
+
185
+ const getDslEncoder = (elmType) => {
186
+ if (elmType === "Int") return "Encode.int";
187
+ if (elmType === "Float") return "Encode.float";
188
+ if (elmType === "Bool") return "Encode.bool";
189
+ return "Encode.string";
190
+ };
191
+
192
+ const generateElmModule = (namespace, table) => {
193
+ const { moduleName, recordName, name: tableName, columns } = table;
194
+ const pk = columns.find(c => c.isPrimaryKey);
195
+ const hasBool = columns.some(c => c.elmType === "Bool");
196
+ const hasNullable = columns.some(c => c.isNullable);
197
+
198
+ let exports = [recordName, moduleName + "Table", "table", ...columns.map(c => c.elmName), "decoder", "all", "insert"];
199
+ if (pk) {
200
+ exports.push("byId", "delete", "update");
201
+ }
202
+
203
+ let code = `module ${namespace}.Db.${moduleName} exposing (${exports.join(", ")})
204
+
205
+ -- This module was automatically generated by elm-ssr query.
206
+ -- Do not edit this file manually.
207
+
208
+ import ElmSsr.Db.Dsl as Dsl exposing (Table, Column)
209
+ import Json.Decode as Decode exposing (Decoder)
210
+ import Json.Encode as Encode
211
+ import ElmSsr.Loader as Loader exposing (Loader)
212
+
213
+ type ${moduleName}Table
214
+ = ${moduleName}Table
215
+
216
+
217
+ table : Table ${moduleName}Table
218
+ table =
219
+ Dsl.table "${tableName}"
220
+
221
+ `;
222
+
223
+ // Column descriptors
224
+ for (const c of columns) {
225
+ code += `
226
+ ${c.elmName} : Column ${moduleName}Table ${c.elmType}
227
+ ${c.elmName} =
228
+ Dsl.column "${c.dbName}" ${getDslEncoder(c.elmType)}
229
+
230
+ `;
231
+ }
232
+
233
+ code += `\n`;
234
+
235
+ // Type alias
236
+ code += `type alias ${recordName} =\n { `;
237
+ const recordFields = columns.map(c => {
238
+ const typeStr = c.isNullable ? `Maybe ${c.elmType}` : c.elmType;
239
+ return `${c.elmName} : ${typeStr}`;
240
+ });
241
+ code += recordFields.join("\n , ") + "\n }\n\n";
242
+
243
+ // Local helpers
244
+ if (hasBool) {
245
+ code += `boolDecoder : Decoder Bool
246
+ boolDecoder =
247
+ Decode.oneOf
248
+ [ Decode.bool
249
+ , Decode.int |> Decode.map (\\val -> val /= 0)
250
+ ]
251
+
252
+
253
+ `;
254
+ }
255
+
256
+ if (hasNullable) {
257
+ code += `encodeNullable : (a -> Encode.Value) -> Maybe a -> Encode.Value
258
+ encodeNullable encoder maybeVal =
259
+ case maybeVal of
260
+ Just val ->
261
+ encoder val
262
+
263
+ Nothing ->
264
+ Encode.null
265
+
266
+
267
+ `;
268
+ }
269
+
270
+ // Decoder
271
+ code += `decoder : Decoder ${recordName}
272
+ decoder =
273
+ `;
274
+
275
+ if (columns.length === 1) {
276
+ const c = columns[0];
277
+ const fieldDec = getFieldDecoder(c);
278
+ code += `Decode.map ${recordName}\n (${fieldDec})\n\n`;
279
+ } else if (columns.length <= 8) {
280
+ code += `Decode.map${columns.length} ${recordName}\n `;
281
+ const fields = columns.map(c => getFieldDecoder(c));
282
+ code += fields.join("\n ") + "\n\n";
283
+ } else {
284
+ code += generatePipelineDecoder(recordName, columns) + "\n\n";
285
+ }
286
+
287
+ // Helpers: all
288
+ const selectFields = columns.map(c => c.dbName).join(", ");
289
+ code += `all : Loader (List ${recordName})
290
+ all =
291
+ Loader.query
292
+ { sql = "SELECT ${selectFields} FROM ${tableName}"
293
+ , params = []
294
+ , decoder = decoder
295
+ }
296
+
297
+
298
+ `;
299
+
300
+ // Helpers: byId
301
+ if (pk) {
302
+ const pkEncoder = getEncoderExpr(pk, pk.elmName + "Val");
303
+ code += `byId : ${pk.elmType} -> Loader (Maybe ${recordName})
304
+ byId ${pk.elmName}Val =
305
+ Loader.queryOne
306
+ { sql = "SELECT ${selectFields} FROM ${tableName} WHERE ${pk.dbName} = ?"
307
+ , params = [ ${pkEncoder} ]
308
+ , decoder = decoder
309
+ }
310
+
311
+
312
+ `;
313
+ }
314
+
315
+ // Helpers: insert
316
+ const insertParams = columns.filter(c => !c.isAutoIncrement && !c.hasDefault);
317
+ const insertSqlFields = insertParams.map(c => c.dbName).join(", ");
318
+ const insertPlaceholders = insertParams.map(() => "?").join(", ");
319
+
320
+ if (insertParams.length > 0) {
321
+ const paramTypes = insertParams.map(c => {
322
+ const typeStr = c.isNullable ? `Maybe ${c.elmType}` : c.elmType;
323
+ return `${c.elmName} : ${typeStr}`;
324
+ });
325
+ const paramEncoders = insertParams.map(c => getEncoderExpr(c, `params.${c.elmName}`));
326
+ const encoderLines = paramEncoders.map((e, idx) => {
327
+ const prefix = idx === 0 ? "[ " : ", ";
328
+ return ` ${prefix}${e}`;
329
+ });
330
+
331
+ code += `insert : { ${paramTypes.join(", ")} } -> Loader { rowsAffected : Int }
332
+ insert params =
333
+ Loader.execute
334
+ { sql = "INSERT INTO ${tableName} (${insertSqlFields}) VALUES (${insertPlaceholders})"
335
+ , params =
336
+ ${encoderLines.join("\n")}
337
+ ]
338
+ }
339
+
340
+
341
+ `;
342
+ } else {
343
+ code += `insert : Loader { rowsAffected : Int }
344
+ insert =
345
+ Loader.execute
346
+ { sql = "INSERT INTO ${tableName} DEFAULT VALUES"
347
+ , params = []
348
+ }
349
+
350
+
351
+ `;
352
+ }
353
+
354
+ // Helpers: delete
355
+ if (pk) {
356
+ const pkEncoder = getEncoderExpr(pk, pk.elmName + "Val");
357
+ code += `delete : ${pk.elmType} -> Loader { rowsAffected : Int }
358
+ delete ${pk.elmName}Val =
359
+ Loader.execute
360
+ { sql = "DELETE FROM ${tableName} WHERE ${pk.dbName} = ?"
361
+ , params = [ ${pkEncoder} ]
362
+ }
363
+
364
+
365
+ `;
366
+ }
367
+
368
+ // Helpers: update
369
+ if (pk) {
370
+ const updateParams = columns.filter(c => !c.isPrimaryKey);
371
+ if (updateParams.length > 0) {
372
+ const updateTypes = updateParams.map(c => {
373
+ const typeStr = c.isNullable ? `Maybe ${c.elmType}` : c.elmType;
374
+ return `${c.elmName} : ${typeStr}`;
375
+ });
376
+ const updateSets = updateParams.map(c => `${c.dbName} = ?`).join(", ");
377
+ const updateEncoders = updateParams.map(c => getEncoderExpr(c, `params.${c.elmName}`));
378
+ const pkEncoder = getEncoderExpr(pk, pk.elmName + "Val");
379
+
380
+ const encoderLines = [
381
+ ...updateEncoders,
382
+ pkEncoder
383
+ ].map((e, idx) => {
384
+ const prefix = idx === 0 ? "[ " : ", ";
385
+ return ` ${prefix}${e}`;
386
+ });
387
+
388
+ code += `update : ${pk.elmType} -> { ${updateTypes.join(", ")} } -> Loader { rowsAffected : Int }
389
+ update ${pk.elmName}Val params =
390
+ Loader.execute
391
+ { sql = "UPDATE ${tableName} SET ${updateSets} WHERE ${pk.dbName} = ?"
392
+ , params =
393
+ ${encoderLines.join("\n")}
394
+ ]
395
+ }
396
+
397
+
398
+ `;
399
+ }
400
+ }
401
+
402
+ return code;
403
+ };
404
+
405
+ export const generateQueries = async ({ rootPath, appConfig, migrationsDir, outputDir }) => {
406
+ const resolvedMigrationsDir = migrationsDir ? resolve(rootPath, migrationsDir) : resolve(rootPath, appConfig.root, "migrations");
407
+ const resolvedOutputDir = outputDir ? resolve(rootPath, outputDir) : resolve(rootPath, appConfig.root, "src", appConfig.module.split(".").join("/"), "Db");
408
+
409
+ try {
410
+ await stat(resolvedMigrationsDir);
411
+ } catch {
412
+ throw new Error(`Migrations directory not found: ${resolvedMigrationsDir}`);
413
+ }
414
+
415
+ const files = await readdir(resolvedMigrationsDir);
416
+ const sqlFiles = files
417
+ .filter(f => f.endsWith(".sql") && !f.endsWith(".down.sql"))
418
+ .sort();
419
+
420
+ if (sqlFiles.length === 0) {
421
+ console.log(`[elm-ssr query] No SQL migration files found in ${resolvedMigrationsDir}`);
422
+ return;
423
+ }
424
+
425
+ // Map to store tables to prevent duplicates and keep the latest definition
426
+ const tablesMap = new Map();
427
+
428
+ for (const file of sqlFiles) {
429
+ const content = await readFile(resolve(resolvedMigrationsDir, file), "utf8");
430
+ const parsedTables = scanCreateTables(content);
431
+
432
+ for (const table of parsedTables) {
433
+ const columns = splitColumns(table.body)
434
+ .map(parseColumnDefinition)
435
+ .filter(Boolean);
436
+
437
+ if (columns.length === 0) continue;
438
+
439
+ const moduleName = capitalize(snakeToCamel(table.name));
440
+ const recordName = capitalize(singularize(snakeToCamel(table.name)));
441
+
442
+ tablesMap.set(table.name, {
443
+ name: table.name,
444
+ moduleName,
445
+ recordName,
446
+ columns
447
+ });
448
+ }
449
+ }
450
+
451
+ if (tablesMap.size === 0) {
452
+ console.log(`[elm-ssr query] No valid CREATE TABLE statements found in migrations.`);
453
+ return;
454
+ }
455
+
456
+ await mkdir(resolvedOutputDir, { recursive: true });
457
+
458
+ for (const table of tablesMap.values()) {
459
+ const fileContent = generateElmModule(appConfig.module, table);
460
+ const targetFile = resolve(resolvedOutputDir, `${table.moduleName}.elm`);
461
+ await writeFile(targetFile, fileContent, "utf8");
462
+ console.log(`[elm-ssr query] Generated Db helper at ${targetFile}`);
463
+ }
464
+ };
package/lib/scaffold.mjs CHANGED
@@ -541,3 +541,159 @@ export const createAppScaffold = async (rootPath, name, options = {}) => {
541
541
 
542
542
  /** @deprecated Use `createAppScaffold`. */
543
543
  export const createExampleScaffold = createAppScaffold;
544
+
545
+ const parseRoutePath = (routePath) => {
546
+ const parts = routePath.split("/").map(p => p.trim()).filter(Boolean);
547
+ if (parts.length === 0) {
548
+ throw new Error(`Invalid route path: "${routePath}"`);
549
+ }
550
+
551
+ return parts.map(part => {
552
+ const clean = part.replace(/_+$/, "");
553
+ const underscores = part.slice(clean.length);
554
+ return toPascalCase(clean) + underscores;
555
+ });
556
+ };
557
+
558
+ export const createRouteScaffold = async (rootPath, appConfig, routePath, options = {}) => {
559
+ const parts = parseRoutePath(routePath);
560
+ const namespace = appConfig.module;
561
+
562
+ if (options.isWs || options.isSse) {
563
+ const endpointName = parts.join("");
564
+ const fileSubpath = `src/Endpoints/${parts.join("/")}.ts`;
565
+ const filePath = resolve(rootPath, appConfig.root, fileSubpath);
566
+
567
+ let content = "";
568
+ let type = "";
569
+ let instructions = "";
570
+
571
+ if (options.isWs) {
572
+ type = "WebSocket";
573
+ content = `export const handleWebSocket = (request: Request): Response => {
574
+ const upgradeHeader = request.headers.get("Upgrade");
575
+ if (!upgradeHeader || upgradeHeader !== "websocket") {
576
+ return new Response("Expected Upgrade: websocket", { status: 426 });
577
+ }
578
+
579
+ const webSocketPair = new WebSocketPair();
580
+ const [client, server] = Object.values(webSocketPair);
581
+
582
+ server.accept();
583
+ server.addEventListener("message", (event) => {
584
+ console.log("WS received:", event.data);
585
+ server.send(JSON.stringify({ echo: event.data, time: new Date().toISOString() }));
586
+ });
587
+
588
+ server.addEventListener("close", () => {
589
+ console.log("WS connection closed");
590
+ });
591
+
592
+ return new Response(null, {
593
+ status: 101,
594
+ webSocket: client
595
+ });
596
+ };
597
+ `;
598
+ instructions = `1. Import this handler in your worker entrypoint (${appConfig.root}/worker.ts or runtime.ts):
599
+ import { handleWebSocket } from "./src/Endpoints/${parts.join("/")}";
600
+
601
+ 2. Intercept the request in your fetch handler:
602
+ if (url.pathname === "/${routePath}") {
603
+ return handleWebSocket(request);
604
+ }`;
605
+ } else {
606
+ type = "Server-Sent Events (SSE)";
607
+ content = `import { createSseStream } from "elm-ssr/sse";
608
+
609
+ export const handleSse = (request: Request): Response => {
610
+ return createSseStream(request, async (send, signal) => {
611
+ let count = 0;
612
+ while (!signal.aborted && count < 100) {
613
+ count += 1;
614
+ send(JSON.stringify({ event: "tick", count, time: new Date().toISOString() }));
615
+ await new Promise((resolve) => setTimeout(resolve, 1000));
616
+ }
617
+ });
618
+ };
619
+ `;
620
+ instructions = `1. Import this handler in your worker entrypoint (${appConfig.root}/worker.ts or runtime.ts):
621
+ import { handleSse } from "./src/Endpoints/${parts.join("/")}";
622
+
623
+ 2. Intercept the request in your fetch handler:
624
+ if (url.pathname === "/${routePath}") {
625
+ return handleSse(request);
626
+ }`;
627
+ }
628
+
629
+ await mkdir(dirname(filePath), { recursive: true });
630
+ await writeFile(filePath, content, "utf8");
631
+
632
+ return { type, path: fileSubpath, instructions };
633
+ } else {
634
+ const moduleName = parts.join(".");
635
+ const fileSubpath = `src/${namespace.split(".").join("/")}/Routes/${parts.join("/")}.elm`;
636
+ const filePath = resolve(rootPath, appConfig.root, fileSubpath);
637
+
638
+ let content = "";
639
+ let type = "";
640
+
641
+ if (options.isApi) {
642
+ type = "Elm JSON API";
643
+ content = `module ${namespace}.Routes.${moduleName} exposing (page, action)
644
+
645
+ import ElmSsr.Action as Action exposing (Action)
646
+ import ElmSsr.Document exposing (Document)
647
+ import ElmSsr.Loader as Loader exposing (Loader)
648
+ import ElmSsr.Route exposing (Request)
649
+ import Json.Encode as Encode
650
+
651
+ page : Request -> Loader (Document Never)
652
+ page _ =
653
+ Loader.fail 405 "GET not allowed on this API route"
654
+
655
+ action : Request -> Action (Document Never)
656
+ action request =
657
+ -- Process request and return JSON response
658
+ Action.json <|
659
+ Encode.object
660
+ [ ( "ok", Encode.bool True )
661
+ , ( "message", Encode.string "Hello from ${routePath} API route!" )
662
+ ]
663
+ `;
664
+ } else {
665
+ type = "Elm Page";
666
+ content = `module ${namespace}.Routes.${moduleName} exposing (page, action)
667
+
668
+ import ElmSsr.Action as Action exposing (Action)
669
+ import ElmSsr.Document exposing (Document)
670
+ import ElmSsr.Html exposing (div, text)
671
+ import ElmSsr.Loader as Loader exposing (Loader)
672
+ import ElmSsr.Page as Page
673
+ import ElmSsr.Route exposing (Request)
674
+ import ${namespace}.View.Shared as Shared
675
+
676
+ page : Request -> Loader (Document Never)
677
+ page _ =
678
+ Loader.succeed view
679
+
680
+ action : Request -> Action (Document Never)
681
+ action _ =
682
+ Action.fail 405 "Method not allowed"
683
+
684
+ view : Document Never
685
+ view =
686
+ Page.page
687
+ { title = "${parts[parts.length - 1]}"
688
+ , head = Shared.head
689
+ , body = [ div [] [ text "Hello from ${routePath}!" ] ]
690
+ }
691
+ `;
692
+ }
693
+
694
+ await mkdir(dirname(filePath), { recursive: true });
695
+ await writeFile(filePath, content, "utf8");
696
+
697
+ return { type, path: fileSubpath };
698
+ }
699
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-ssr",
3
- "version": "0.6.2",
3
+ "version": "0.90.0",
4
4
  "description": "Elm-first SSR library and framework for Cloudflare Workers (and Bun): file-based routes/islands, backend-neutral effect adapters (KV/D1/Redis/Postgres), background tasks (waitUntil/Queues), SQL-file migrations, CLI scaffold + build.",
5
5
  "license": "MIT",
6
6
  "author": "Michał Majchrzak <michmajchrzak@gmail.com>",
package/src/app.ts CHANGED
@@ -50,6 +50,7 @@ export interface WorkerAppOptions {
50
50
  * skip paths.
51
51
  */
52
52
  csrf?: CsrfMiddlewareOptions | boolean;
53
+ debug?: boolean;
53
54
  }
54
55
 
55
56
  export const createWorkerApp = ({
@@ -62,8 +63,12 @@ export const createWorkerApp = ({
62
63
  effects,
63
64
  log,
64
65
  sessions,
65
- csrf
66
+ csrf,
67
+ debug
66
68
  }: WorkerAppOptions): WorkerHandler => {
69
+ const isDev = typeof process !== "undefined" && process.env ? (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") : false;
70
+ const enableDebug = debug ?? isDev;
71
+
67
72
  const runner: EffectRunner = effects ?? defaultEffectRunner;
68
73
  const effectsWithSessions = sessions ? sessionEffects(runner) : runner;
69
74
 
@@ -74,7 +79,8 @@ export const createWorkerApp = ({
74
79
  stylesheet,
75
80
  routes,
76
81
  createFlags,
77
- effects: effectsWithSessions
82
+ effects: effectsWithSessions,
83
+ debug: enableDebug
78
84
  });
79
85
 
80
86
  const middlewares = [