elm-ssr 0.5.2 → 0.6.1

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,7 +1,8 @@
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";
@@ -16,7 +17,26 @@ const findFlagValue = (flagName) => {
16
17
  return index >= 0 ? args[index + 1] : undefined;
17
18
  };
18
19
 
19
- 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));
20
40
  const packageJsonPath = resolve(rootPath, "package.json");
21
41
 
22
42
  let packageJson = { name: "unknown" };
@@ -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
@@ -88,6 +109,42 @@ switch (command) {
88
109
  await build({ rootPath, config });
89
110
  }
90
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
+
91
148
  let wranglerCmd = "./node_modules/.bin/wrangler";
92
149
  let wranglerArgs = ["dev"];
93
150
  try {
@@ -116,7 +173,24 @@ switch (command) {
116
173
  }
117
174
  }
118
175
 
119
- await run(wranglerCmd, wranglerArgs, rootPath);
176
+ try {
177
+ await run(wranglerCmd, wranglerArgs, rootPath);
178
+ } finally {
179
+ cleanup();
180
+ }
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`);
120
194
  break;
121
195
  }
122
196
 
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
 
@@ -15,13 +15,13 @@ const toPascalCase = (value) =>
15
15
 
16
16
  const ensureValidName = (name) => {
17
17
  if (!/^[a-z0-9-]+$/.test(name)) {
18
- throw new Error("Example name must use lowercase letters, numbers, and dashes only.");
18
+ throw new Error("App name must use lowercase letters, numbers, and dashes only.");
19
19
  }
20
20
  };
21
21
 
22
22
  const ensureAppMissing = (config, name) => {
23
23
  if (config.apps.some((app) => app.name === name)) {
24
- throw new Error(`Example "${name}" already exists in elm-ssr.config.json.`);
24
+ throw new Error(`App "${name}" already exists in elm-ssr.config.json. If you want to recreate it, remove its entry from elm-ssr.config.json first.`);
25
25
  }
26
26
  };
27
27
 
@@ -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.2",
3
+ "version": "0.6.1",
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>",