elm-ssr 0.5.1 → 0.6.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/bin/elm-ssr.mjs CHANGED
@@ -1,21 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { readFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { watch } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
5
6
  import { createAppScaffold } from "../lib/scaffold.mjs";
6
7
  import { readWorkspaceConfig } from "../lib/workspace.mjs";
7
8
  import { build } from "../lib/build.mjs";
8
9
  import { migrate } from "../lib/migrate.mjs";
9
10
 
10
11
  const defaultRootPath = process.cwd();
11
- const packageJsonPath = resolve(defaultRootPath, "package.json");
12
-
13
- let packageJson = { name: "unknown" };
14
- try {
15
- packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
16
- } catch {
17
- // Not in a package root, that's okay for some commands
18
- }
19
12
  const args = process.argv.slice(2);
20
13
  const command = args[0] ?? "help";
21
14
 
@@ -24,7 +17,34 @@ const findFlagValue = (flagName) => {
24
17
  return index >= 0 ? args[index + 1] : undefined;
25
18
  };
26
19
 
27
- const rootPath = resolve(findFlagValue("--root") ?? defaultRootPath);
20
+ const findWorkspaceRoot = async (startPath) => {
21
+ let current = startPath;
22
+ while (true) {
23
+ try {
24
+ const configPath = resolve(current, "elm-ssr.config.json");
25
+ await stat(configPath);
26
+ return current;
27
+ } catch {
28
+ const parent = dirname(current);
29
+ if (parent === current) {
30
+ break;
31
+ }
32
+ current = parent;
33
+ }
34
+ }
35
+ return startPath;
36
+ };
37
+
38
+ const userRoot = findFlagValue("--root");
39
+ const rootPath = resolve(userRoot ?? await findWorkspaceRoot(defaultRootPath));
40
+ const packageJsonPath = resolve(rootPath, "package.json");
41
+
42
+ let packageJson = { name: "unknown" };
43
+ try {
44
+ packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
45
+ } catch {
46
+ // Not in a package root, that's okay for some commands
47
+ }
28
48
 
29
49
  const run = async (cmd, cmdArgs, cwd = rootPath) => {
30
50
  const child = Bun.spawn([cmd, ...cmdArgs], {
@@ -48,6 +68,7 @@ const printHelp = () => {
48
68
  build Generate wrapper modules and compile configured Elm SSR apps
49
69
  compress Pre-compress island and app bundles using Gzip for faster edge delivery
50
70
  dev Build and start wrangler dev using the current workspace config
71
+ init <name> Initialize a self-contained single-app project in the current directory
51
72
  new <name> Create a new app at <workspace>/<name>/ (or <workspace>/<subdir>/<name>/
52
73
  with --in <subdir>) and register it in elm-ssr.config.json
53
74
  routes Print configured apps and their public modules
@@ -80,11 +101,98 @@ switch (command) {
80
101
  await build({ rootPath, config });
81
102
  break;
82
103
 
83
- case "dev":
104
+ case "dev": {
84
105
  requireConfig();
85
- await run("bun", ["run", "build"], rootPath);
86
- await run("./node_modules/.bin/wrangler", ["dev"], rootPath);
106
+ if (packageJson && packageJson.scripts && packageJson.scripts.build) {
107
+ await run("bun", ["run", "build"], rootPath);
108
+ } else {
109
+ await build({ rootPath, config });
110
+ }
111
+
112
+ let buildTimeout = null;
113
+ const triggerBuild = () => {
114
+ if (buildTimeout) clearTimeout(buildTimeout);
115
+ buildTimeout = setTimeout(async () => {
116
+ console.log("\n[elm-ssr] File change detected. Rebuilding...");
117
+ try {
118
+ await build({ rootPath, config });
119
+ console.log("[elm-ssr] Rebuild successful.");
120
+ } catch (err) {
121
+ console.error("[elm-ssr] Rebuild failed:", err);
122
+ }
123
+ }, 100);
124
+ };
125
+
126
+ const watchers = [];
127
+ for (const app of config.apps) {
128
+ const srcDir = resolve(rootPath, app.root, "src");
129
+ try {
130
+ const watcher = watch(srcDir, { recursive: true }, (eventType, filename) => {
131
+ if (filename && filename.endsWith(".elm")) {
132
+ triggerBuild();
133
+ }
134
+ });
135
+ watchers.push(watcher);
136
+ } catch (err) {
137
+ console.warn(`[elm-ssr] Could not watch directory ${srcDir}:`, err);
138
+ }
139
+ }
140
+
141
+ const cleanup = () => {
142
+ for (const w of watchers) w.close();
143
+ };
144
+
145
+ process.on("SIGINT", cleanup);
146
+ process.on("exit", cleanup);
147
+
148
+ let wranglerCmd = "./node_modules/.bin/wrangler";
149
+ let wranglerArgs = ["dev"];
150
+ try {
151
+ await readFile(resolve(rootPath, wranglerCmd));
152
+ } catch {
153
+ wranglerCmd = "bunx";
154
+ wranglerArgs = ["wrangler", "dev"];
155
+ }
156
+
157
+ let hasWranglerConfig = false;
158
+ try {
159
+ const tomlContent = await readFile(resolve(rootPath, "wrangler.toml"), "utf8");
160
+ if (tomlContent.trim().length > 0) hasWranglerConfig = true;
161
+ } catch {}
162
+ try {
163
+ const jsonContent = await readFile(resolve(rootPath, "wrangler.jsonc"), "utf8");
164
+ if (jsonContent.trim().length > 0) hasWranglerConfig = true;
165
+ } catch {}
166
+
167
+ if (!hasWranglerConfig) {
168
+ const app = config.apps[0];
169
+ if (app) {
170
+ wranglerArgs.push(`${app.root}/worker.ts`);
171
+ wranglerArgs.push("--compatibility-date", "2026-05-28");
172
+ wranglerArgs.push("--compatibility-flags", "nodejs_compat");
173
+ }
174
+ }
175
+
176
+ try {
177
+ await run(wranglerCmd, wranglerArgs, rootPath);
178
+ } finally {
179
+ cleanup();
180
+ }
87
181
  break;
182
+ }
183
+
184
+ case "init": {
185
+ const name = args[1];
186
+
187
+ if (!name) {
188
+ console.error("Usage: elm-ssr init <name>");
189
+ process.exit(1);
190
+ }
191
+
192
+ const created = await createAppScaffold(rootPath, name, { root: "." });
193
+ console.log(`Initialized ${created.name} in current directory`);
194
+ break;
195
+ }
88
196
 
89
197
  case "new": {
90
198
  const name = args[1];
package/lib/scaffold.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { readWorkspaceConfig, writeWorkspaceConfig } from "./workspace.mjs";
4
4
 
@@ -276,8 +276,8 @@ const runtimeTemplate = (appRoot) => {
276
276
  // appRoot is a slash-separated path like "my-app" or "apps/my-app".
277
277
  // The generated bundles live at <workspaceRoot>/generated/<appRoot>/. From
278
278
  // <workspaceRoot>/<appRoot>/runtime.ts, we climb out by one ".." per segment.
279
- const upToRoot = appRoot.split("/").map(() => "..").join("/");
280
- const generatedPrefix = `${upToRoot}/generated/${appRoot}`;
279
+ const upToRoot = appRoot === "." ? "." : appRoot.split("/").map(() => "..").join("/");
280
+ const generatedPrefix = `${upToRoot}/generated/${appRoot === "." ? "" : appRoot}`.replace(/\/+$/, "");
281
281
  return `import { createWorkerApp } from "elm-ssr";
282
282
  import { renderApp, type CompiledElmModule } from "elm-ssr/render";
283
283
  import type { RouteCatalog } from "elm-ssr/http";
@@ -467,6 +467,38 @@ const normalizeAppRoot = (rawRoot, name) => {
467
467
  return candidate;
468
468
  };
469
469
 
470
+ const ensurePackageJson = async (rootPath, appName) => {
471
+ const packageJsonPath = resolve(rootPath, "package.json");
472
+ let packageJson = {};
473
+ try {
474
+ packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
475
+ } catch {
476
+ packageJson = {
477
+ name: appName,
478
+ version: "0.1.0",
479
+ type: "module",
480
+ scripts: {},
481
+ devDependencies: {}
482
+ };
483
+ }
484
+
485
+ packageJson.scripts = {
486
+ build: "elm-ssr build",
487
+ dev: "elm-ssr dev",
488
+ routes: "elm-ssr routes",
489
+ migrate: "elm-ssr migrate",
490
+ ...packageJson.scripts
491
+ };
492
+
493
+ packageJson.devDependencies = {
494
+ "elm-ssr": "latest",
495
+ wrangler: "^4.0.0",
496
+ ...packageJson.devDependencies
497
+ };
498
+
499
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
500
+ };
501
+
470
502
  /**
471
503
  * Scaffold a new elm-ssr app under `<rootPath>/<appRoot>/` and register it in
472
504
  * elm-ssr.config.json. `appRoot` defaults to the app's `name`; pass an
@@ -502,6 +534,8 @@ export const createAppScaffold = async (rootPath, name, options = {}) => {
502
534
  apps: [...config.apps, configEntry]
503
535
  });
504
536
 
537
+ await ensurePackageJson(rootPath, name);
538
+
505
539
  return configEntry;
506
540
  };
507
541
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-ssr",
3
- "version": "0.5.1",
3
+ "version": "0.6.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>",