elm-ssr 0.91.3 → 0.91.6
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/bin/elm-ssr.mjs +8 -6
- package/lib/build.mjs +49 -27
- package/lib/scaffold.mjs +61 -13
- package/package.json +3 -2
- package/src/app.ts +11 -5
- package/src/sessions/index.ts +1 -0
- package/src/sessions/middleware.ts +16 -9
- package/src/sessions/store.ts +2 -1
package/bin/elm-ssr.mjs
CHANGED
|
@@ -77,9 +77,9 @@ const printHelp = () => {
|
|
|
77
77
|
compress Pre-compress island and app bundles using Gzip for faster edge delivery
|
|
78
78
|
dev Build and start wrangler dev using the current workspace config
|
|
79
79
|
init <name> Initialize a self-contained single-app project in the current directory
|
|
80
|
-
(use --db to wire SQLite/migrations, --auth betterAuth|auth0 for auth guards)
|
|
80
|
+
(use --db to wire SQLite/migrations, --auth betterAuth|auth0 for auth guards, --tailwind for Tailwind CSS)
|
|
81
81
|
new <name> Create a new app at <workspace>/<name>/ and register it in elm-ssr.config.json
|
|
82
|
-
(use --in <subdir> to group, --db for SQLite, --auth betterAuth|auth0 for auth)
|
|
82
|
+
(use --in <subdir> to group, --db for SQLite, --auth betterAuth|auth0 for auth, --tailwind for Tailwind)
|
|
83
83
|
routes Print configured apps and their public modules
|
|
84
84
|
route <path> Scaffold a new route (standard HTML, --api JSON, --ws WebSocket, or --sse EventSource)
|
|
85
85
|
query Generate type-safe Elm Db modules from SQL table definitions
|
|
@@ -196,11 +196,12 @@ switch (command) {
|
|
|
196
196
|
const name = args[1];
|
|
197
197
|
|
|
198
198
|
if (!name) {
|
|
199
|
-
console.error("Usage: elm-ssr init <name> [--db] [--auth betterAuth|auth0]");
|
|
199
|
+
console.error("Usage: elm-ssr init <name> [--db] [--auth betterAuth|auth0] [--tailwind]");
|
|
200
200
|
process.exit(1);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
const db = args.includes("--db");
|
|
204
|
+
const tailwind = args.includes("--tailwind");
|
|
204
205
|
let auth = findFlagValue("--auth");
|
|
205
206
|
if (auth) {
|
|
206
207
|
if (auth === "better-auth" || auth === "betterAuth") {
|
|
@@ -211,7 +212,7 @@ switch (command) {
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
const created = await createAppScaffold(rootPath, name, { root: ".", db, auth });
|
|
215
|
+
const created = await createAppScaffold(rootPath, name, { root: ".", db, auth, tailwind });
|
|
215
216
|
console.log(`Initialized ${created.name} in current directory`);
|
|
216
217
|
break;
|
|
217
218
|
}
|
|
@@ -220,7 +221,7 @@ switch (command) {
|
|
|
220
221
|
const name = args[1];
|
|
221
222
|
|
|
222
223
|
if (!name) {
|
|
223
|
-
console.error("Usage: elm-ssr new <name> [--in <subdir>] [--db] [--auth betterAuth|auth0]");
|
|
224
|
+
console.error("Usage: elm-ssr new <name> [--in <subdir>] [--db] [--auth betterAuth|auth0] [--tailwind]");
|
|
224
225
|
console.error(" Default location: <workspace>/<name>/");
|
|
225
226
|
console.error(" Use --in apps to place it under <workspace>/apps/<name>/, etc.");
|
|
226
227
|
process.exit(1);
|
|
@@ -230,6 +231,7 @@ switch (command) {
|
|
|
230
231
|
const appRoot = subdir ? `${subdir.replace(/\/+$/, "")}/${name}` : name;
|
|
231
232
|
|
|
232
233
|
const db = args.includes("--db");
|
|
234
|
+
const tailwind = args.includes("--tailwind");
|
|
233
235
|
let auth = findFlagValue("--auth");
|
|
234
236
|
if (auth) {
|
|
235
237
|
if (auth === "better-auth" || auth === "betterAuth") {
|
|
@@ -240,7 +242,7 @@ switch (command) {
|
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
const created = await createAppScaffold(rootPath, name, { root: appRoot, db, auth });
|
|
245
|
+
const created = await createAppScaffold(rootPath, name, { root: appRoot, db, auth, tailwind });
|
|
244
246
|
console.log(`Created ${created.name} at ${created.root}`);
|
|
245
247
|
break;
|
|
246
248
|
}
|
package/lib/build.mjs
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, writeFile, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
|
-
import { exec } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
4
|
|
|
9
5
|
// This lib is part of elm-ssr. It handles generating Main.elm and
|
|
10
6
|
// compiling the route apps and island bundles.
|
|
@@ -203,21 +199,50 @@ main =
|
|
|
203
199
|
await rm(rawOutputPath, { force: true });
|
|
204
200
|
};
|
|
205
201
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
const findLocalTailwindCli = async (startDir) => {
|
|
203
|
+
const candidate = resolve(startDir, "node_modules", "tailwindcss", "lib", "cli.js");
|
|
204
|
+
try {
|
|
205
|
+
await stat(candidate);
|
|
206
|
+
return candidate;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const runTailwind = async ({ rootPath, cliRoot, appRoot, inputPath, contentGlob }) => {
|
|
213
|
+
const tailwindSearchRoots = [
|
|
214
|
+
rootPath,
|
|
215
|
+
cliRoot,
|
|
216
|
+
resolve(cliRoot, "..", ".."),
|
|
217
|
+
process.cwd()
|
|
218
|
+
];
|
|
219
|
+
let tailwindCli = null;
|
|
220
|
+
for (const searchRoot of [...new Set(tailwindSearchRoots)]) {
|
|
221
|
+
tailwindCli = await findLocalTailwindCli(searchRoot);
|
|
222
|
+
if (tailwindCli) {
|
|
223
|
+
break;
|
|
215
224
|
}
|
|
216
|
-
const parent = dirname(current);
|
|
217
|
-
if (parent === current) break;
|
|
218
|
-
current = parent;
|
|
219
225
|
}
|
|
220
|
-
|
|
226
|
+
|
|
227
|
+
const command = tailwindCli
|
|
228
|
+
? [process.execPath, tailwindCli]
|
|
229
|
+
: ["npx", "--yes", "tailwindcss"];
|
|
230
|
+
|
|
231
|
+
const child = Bun.spawn(
|
|
232
|
+
[...command, "-i", inputPath, "--content", contentGlob, "--minify"],
|
|
233
|
+
{ cwd: appRoot, stdout: "pipe", stderr: "pipe" }
|
|
234
|
+
);
|
|
235
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
236
|
+
new Response(child.stdout).text(),
|
|
237
|
+
new Response(child.stderr).text(),
|
|
238
|
+
child.exited
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
if (exitCode !== 0) {
|
|
242
|
+
throw new Error((stderr || stdout || `Tailwind exited with code ${exitCode}`).trim());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return stdout.toString().trim();
|
|
221
246
|
};
|
|
222
247
|
|
|
223
248
|
const compileStylesheet = async ({ inputPath, outputPath, appConfig, appRoot }) => {
|
|
@@ -234,17 +259,14 @@ const findLocalTailwind = async (startDir) => {
|
|
|
234
259
|
try {
|
|
235
260
|
console.log(`[elm-ssr] Compiling Tailwind CSS for app "${appConfig.name}"...`);
|
|
236
261
|
const contentGlob = "src/**/*.elm,src/**/*.ts,src/**/*.js";
|
|
237
|
-
|
|
238
|
-
let tailwindBin = await findLocalTailwind(rootPath);
|
|
239
|
-
if (!tailwindBin) {
|
|
240
|
-
tailwindBin = await findLocalTailwind(cliRoot);
|
|
241
|
-
}
|
|
242
|
-
const binCommand = tailwindBin ? tailwindBin : "npx --yes tailwindcss";
|
|
243
|
-
|
|
244
|
-
const command = `${binCommand} -i ${inputPath} --content "${contentGlob}" --minify`;
|
|
245
|
-
const { stdout } = await execAsync(command, { cwd: appRoot });
|
|
246
|
-
css = stdout.toString().trim();
|
|
262
|
+
css = await runTailwind({ rootPath, cliRoot, appRoot, inputPath, contentGlob });
|
|
247
263
|
} catch (err) {
|
|
264
|
+
if (appConfig.tailwindFallback !== true) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`[elm-ssr] Tailwind CSS compilation failed for app "${appConfig.name}". ` +
|
|
267
|
+
`Install tailwindcss in the workspace or set "tailwindFallback": true for development-only fallback. ${err.message}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
248
270
|
console.warn(`[elm-ssr] Tailwind CSS compilation failed. Falling back to plain CSS read. Error:`, err.message);
|
|
249
271
|
const raw = await readFile(inputPath, "utf8");
|
|
250
272
|
css = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
package/lib/scaffold.mjs
CHANGED
|
@@ -368,28 +368,57 @@ view user =
|
|
|
368
368
|
}
|
|
369
369
|
`;
|
|
370
370
|
|
|
371
|
-
const authEndpointTemplate = (authProvider) => `
|
|
371
|
+
const authEndpointTemplate = (authProvider) => `import { generateSessionId, signValue, generateCsrfToken, readSignedCookie } from "elm-ssr/sessions";
|
|
372
|
+
|
|
373
|
+
export const handleAuth = async (
|
|
374
|
+
request: Request,
|
|
375
|
+
env: any,
|
|
376
|
+
options?: { store?: any; secret?: string }
|
|
377
|
+
): Promise<Response> => {
|
|
372
378
|
const url = new URL(request.url);
|
|
373
379
|
|
|
374
380
|
if (url.pathname === "/api/auth/login") {
|
|
375
381
|
// Mock login session update for ${authProvider}
|
|
382
|
+
let cookieVal = "__elm_ssr_session=" + encodeURIComponent(JSON.stringify({
|
|
383
|
+
user: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" }
|
|
384
|
+
})) + "; Path=/; HttpOnly; SameSite=Lax";
|
|
385
|
+
|
|
386
|
+
if (options?.store && options?.secret) {
|
|
387
|
+
const sessionId = generateSessionId();
|
|
388
|
+
await options.store.set(sessionId, {
|
|
389
|
+
data: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" },
|
|
390
|
+
csrf: generateCsrfToken()
|
|
391
|
+
});
|
|
392
|
+
const signed = await signValue(options.secret, sessionId);
|
|
393
|
+
cookieVal = "session=" + signed + "; Path=/; HttpOnly; SameSite=Lax";
|
|
394
|
+
}
|
|
395
|
+
|
|
376
396
|
return new Response(null, {
|
|
377
397
|
status: 302,
|
|
378
398
|
headers: {
|
|
379
399
|
"Location": "/profile",
|
|
380
|
-
"Set-Cookie":
|
|
381
|
-
user: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" }
|
|
382
|
-
})) + "; Path=/; HttpOnly; SameSite=Lax"
|
|
400
|
+
"Set-Cookie": cookieVal
|
|
383
401
|
}
|
|
384
402
|
});
|
|
385
403
|
}
|
|
386
404
|
|
|
387
405
|
if (url.pathname === "/api/auth/logout") {
|
|
406
|
+
let cookieVal = "__elm_ssr_session=; Path=/; Max-Age=0; HttpOnly";
|
|
407
|
+
if (options?.store && options?.secret) {
|
|
408
|
+
cookieVal = "session=; Path=/; Max-Age=0; HttpOnly";
|
|
409
|
+
const cookieHeader = request.headers.get("cookie");
|
|
410
|
+
if (cookieHeader) {
|
|
411
|
+
const sessionId = await readSignedCookie(cookieHeader, "session", options.secret);
|
|
412
|
+
if (sessionId) {
|
|
413
|
+
await options.store.delete(sessionId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
388
417
|
return new Response(null, {
|
|
389
418
|
status: 302,
|
|
390
419
|
headers: {
|
|
391
420
|
"Location": "/login",
|
|
392
|
-
"Set-Cookie":
|
|
421
|
+
"Set-Cookie": cookieVal
|
|
393
422
|
}
|
|
394
423
|
});
|
|
395
424
|
}
|
|
@@ -474,11 +503,13 @@ if (typeof Bun !== "undefined") {
|
|
|
474
503
|
}`;
|
|
475
504
|
|
|
476
505
|
let sessionsConfig = '';
|
|
506
|
+
let authInit = '';
|
|
477
507
|
if (auth) {
|
|
508
|
+
authInit = `\nexport const sessionStore = memorySessionStore();\n`;
|
|
478
509
|
sessionsConfig = `,
|
|
479
510
|
sessions: {
|
|
480
511
|
secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
|
|
481
|
-
store:
|
|
512
|
+
store: sessionStore,
|
|
482
513
|
secure: false
|
|
483
514
|
},
|
|
484
515
|
csrf: true`;
|
|
@@ -492,7 +523,8 @@ const baseWorkerFetch = worker.fetch;
|
|
|
492
523
|
worker.fetch = async (request, env, ctx) => {
|
|
493
524
|
const url = new URL(request.url);
|
|
494
525
|
if (url.pathname.startsWith("/api/auth/")) {
|
|
495
|
-
|
|
526
|
+
const secret = (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars";
|
|
527
|
+
return handleAuth(request, env, { store: sessionStore, secret });
|
|
496
528
|
}
|
|
497
529
|
return baseWorkerFetch(request, env, ctx);
|
|
498
530
|
};
|
|
@@ -582,7 +614,7 @@ export const createFlags = ({ request, path, formData, env }: { request?: Reques
|
|
|
582
614
|
|
|
583
615
|
export const renderPath = async (path: string) =>
|
|
584
616
|
renderApp(elmModule, createFlags({ path }));
|
|
585
|
-
${dbInit}
|
|
617
|
+
${dbInit}${authInit}
|
|
586
618
|
export const worker = createWorkerApp({
|
|
587
619
|
elmModule,
|
|
588
620
|
islands,
|
|
@@ -663,6 +695,7 @@ const filesForApp = (name, appRoot, options = {}) => {
|
|
|
663
695
|
const namespace = toPascalCase(name);
|
|
664
696
|
const db = options.db || !!options.auth;
|
|
665
697
|
const auth = options.auth;
|
|
698
|
+
const tailwind = options.tailwind;
|
|
666
699
|
|
|
667
700
|
const files = [
|
|
668
701
|
{ path: `${appRoot}/elm.json`, content: JSON.stringify(elmJsonTemplate(), null, 2) + "\n" },
|
|
@@ -710,12 +743,27 @@ SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32
|
|
|
710
743
|
});
|
|
711
744
|
}
|
|
712
745
|
|
|
746
|
+
if (tailwind) {
|
|
747
|
+
files.push({
|
|
748
|
+
path: `${appRoot}/src/app.css`,
|
|
749
|
+
content: `@tailwind base;
|
|
750
|
+
@tailwind components;
|
|
751
|
+
@tailwind utilities;
|
|
752
|
+
`
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const configEntry = {
|
|
757
|
+
name,
|
|
758
|
+
root: appRoot,
|
|
759
|
+
module: namespace
|
|
760
|
+
};
|
|
761
|
+
if (tailwind) {
|
|
762
|
+
configEntry.tailwind = true;
|
|
763
|
+
}
|
|
764
|
+
|
|
713
765
|
return {
|
|
714
|
-
configEntry
|
|
715
|
-
name,
|
|
716
|
-
root: appRoot,
|
|
717
|
-
module: namespace
|
|
718
|
-
},
|
|
766
|
+
configEntry,
|
|
719
767
|
files
|
|
720
768
|
};
|
|
721
769
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elm-ssr",
|
|
3
|
-
"version": "0.91.
|
|
3
|
+
"version": "0.91.6",
|
|
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>",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"bun": ">=1.3"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"cookie": "^1.0.1"
|
|
61
|
+
"cookie": "^1.0.1",
|
|
62
|
+
"tailwindcss": "3.4.17"
|
|
62
63
|
}
|
|
63
64
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Middleware,
|
|
2
3
|
RenderFlagsFactory,
|
|
3
4
|
RouteCatalog,
|
|
4
5
|
WorkerExecutionContext,
|
|
@@ -50,6 +51,7 @@ export interface WorkerAppOptions {
|
|
|
50
51
|
* skip paths.
|
|
51
52
|
*/
|
|
52
53
|
csrf?: CsrfMiddlewareOptions | boolean;
|
|
54
|
+
middlewares?: Middleware[];
|
|
53
55
|
debug?: boolean;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -64,6 +66,7 @@ export const createWorkerApp = ({
|
|
|
64
66
|
log,
|
|
65
67
|
sessions,
|
|
66
68
|
csrf,
|
|
69
|
+
middlewares,
|
|
67
70
|
debug
|
|
68
71
|
}: WorkerAppOptions): WorkerHandler => {
|
|
69
72
|
const isDev = typeof process !== "undefined" && process.env ? (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") : false;
|
|
@@ -83,21 +86,24 @@ export const createWorkerApp = ({
|
|
|
83
86
|
debug: enableDebug
|
|
84
87
|
});
|
|
85
88
|
|
|
86
|
-
const
|
|
89
|
+
const middlewaresList = [
|
|
87
90
|
errorMiddleware,
|
|
88
91
|
requestIdMiddleware,
|
|
89
92
|
timingMiddleware,
|
|
90
93
|
loggingMiddleware(log)
|
|
91
94
|
];
|
|
92
95
|
if (sessions) {
|
|
93
|
-
|
|
96
|
+
middlewaresList.push(sessionMiddleware(sessions));
|
|
94
97
|
if (csrf) {
|
|
95
|
-
|
|
98
|
+
middlewaresList.push(csrfMiddleware(csrf === true ? {} : csrf));
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
middlewares
|
|
101
|
+
if (middlewares) {
|
|
102
|
+
middlewaresList.push(...middlewares);
|
|
103
|
+
}
|
|
104
|
+
middlewaresList.push(headMiddleware);
|
|
99
105
|
|
|
100
|
-
const appHandler = composeMiddleware(handler,
|
|
106
|
+
const appHandler = composeMiddleware(handler, middlewaresList);
|
|
101
107
|
|
|
102
108
|
return {
|
|
103
109
|
fetch(request: Request, env?: unknown, executionCtx?: WorkerExecutionContext) {
|
package/src/sessions/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ const appendSetCookie = (response: Response, header: string): Response => {
|
|
|
50
50
|
});
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const readSignedCookie = async (header: string | null, name: string, secret: string): Promise<string | null> => {
|
|
53
|
+
export const readSignedCookie = async (header: string | null, name: string, secret: string): Promise<string | null> => {
|
|
54
54
|
if (!header) {
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
@@ -104,14 +104,21 @@ export const sessionMiddleware = (options: SessionMiddlewareOptions): Middleware
|
|
|
104
104
|
if (sessionId) {
|
|
105
105
|
const existing = await options.store.get(sessionId);
|
|
106
106
|
if (existing) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (existing.expiresAt && Date.now() > existing.expiresAt) {
|
|
108
|
+
// Session expired
|
|
109
|
+
await options.store.delete(sessionId);
|
|
110
|
+
session = mintSession();
|
|
111
|
+
session.destroyed = true; // Ensure the cookie gets cleared as well
|
|
112
|
+
} else {
|
|
113
|
+
session = {
|
|
114
|
+
id: sessionId,
|
|
115
|
+
data: existing.data,
|
|
116
|
+
csrf: existing.csrf,
|
|
117
|
+
dirty: false,
|
|
118
|
+
destroyed: false,
|
|
119
|
+
isNew: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
115
122
|
} else {
|
|
116
123
|
session = mintSession();
|
|
117
124
|
}
|
package/src/sessions/store.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { CacheBackend } from "../backends";
|
|
|
2
2
|
import type { SessionRecord, SessionStore } from "./types";
|
|
3
3
|
|
|
4
4
|
/** In-memory session store. Survives only as long as the process. */
|
|
5
|
-
export const memorySessionStore = (initial?: Map<string, SessionRecord>): SessionStore => {
|
|
5
|
+
export const memorySessionStore = (initial?: Map<string, SessionRecord>): SessionStore & { store: Map<string, SessionRecord> } => {
|
|
6
6
|
const store = initial ?? new Map<string, SessionRecord>();
|
|
7
7
|
return {
|
|
8
|
+
store,
|
|
8
9
|
get: async (id) => {
|
|
9
10
|
const record = store.get(id);
|
|
10
11
|
if (!record) {
|