@b9g/shovel 0.2.9 → 0.2.11

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/create.js +334 -120
  3. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to Shovel will be documented in this file.
4
4
 
5
+ ## [0.2.9] - 2026-02-20
6
+
7
+ ### Features
8
+
9
+ - **Dev server Ctrl+O** - Open the dev server URL in the default browser with `Ctrl+O`
10
+ - **Dev server signal handling** - `Ctrl+Z` (suspend), `Ctrl+D` (quit), `Ctrl+\` (quit) now work correctly in raw mode instead of being silently swallowed
11
+ - **Dev server input passthrough** - Typing, Enter, and Backspace now echo to stdout instead of being dropped
12
+
13
+ ### Dependencies
14
+
15
+ - **`@logtape/logtape`** `^1.2.0` → `^2.0.0` across all packages
16
+ - **`@logtape/file`** `^1.0.0` → `^2.0.0`
17
+ - **`@b9g/filesystem`** `0.2.0` - Version bump
18
+ - **`@b9g/filesystem-s3`** `0.2.0` - Version bump
19
+
20
+ ### Documentation
21
+
22
+ - **`@b9g/filesystem` README rewritten** - All class names were fabricated in the previous README. Corrected to match actual exports: `MemoryDirectory`, `NodeFSDirectory`, `S3Directory`, `CustomDirectoryStorage`
23
+ - Fixed inaccurate config options in `@b9g/assets` README
24
+ - Fixed `PostMessageCache` constructor signature in `@b9g/cache` README
25
+ - Fixed import paths in `@b9g/oauth2` README (`@b9g/auth` → `@b9g/oauth2`)
26
+
5
27
  ## [0.2.8] - 2026-02-10
6
28
 
7
29
  ### Bug Fixes
package/bin/create.js CHANGED
@@ -26,10 +26,16 @@ function parseFlags(args) {
26
26
  const arg = args[i];
27
27
  if (arg === "--template" && args[i + 1])
28
28
  flags.template = args[++i];
29
+ else if (arg === "--framework" && args[i + 1])
30
+ flags.framework = args[++i];
29
31
  else if (arg === "--typescript")
30
32
  flags.typescript = true;
31
33
  else if (arg === "--no-typescript")
32
34
  flags.typescript = false;
35
+ else if (arg === "--jsx")
36
+ flags.jsx = true;
37
+ else if (arg === "--no-jsx")
38
+ flags.jsx = false;
33
39
  else if (arg === "--platform" && args[i + 1])
34
40
  flags.platform = args[++i];
35
41
  }
@@ -70,14 +76,11 @@ async function main() {
70
76
  }
71
77
  let template;
72
78
  let uiFramework = "vanilla";
73
- if (flags.template === "crank") {
74
- template = "static-site";
75
- uiFramework = "crank";
76
- } else if (flags.template) {
79
+ if (flags.template) {
77
80
  const valid = ["hello-world", "api", "static-site", "full-stack"];
78
81
  if (!valid.includes(flags.template)) {
79
82
  console.error(
80
- `Error: Unknown template "${flags.template}". Valid options: ${valid.join(", ")}, crank`
83
+ `Error: Unknown template "${flags.template}". Valid options: ${valid.join(", ")}`
81
84
  );
82
85
  process.exit(1);
83
86
  }
@@ -114,38 +117,65 @@ async function main() {
114
117
  }
115
118
  template = templateResult;
116
119
  }
117
- if (uiFramework === "vanilla" && (template === "static-site" || template === "full-stack")) {
118
- const framework = await select({
119
- message: "UI framework:",
120
- initialValue: "crank",
121
- options: [
122
- {
123
- value: "alpine",
124
- label: "Alpine.js",
125
- hint: "Lightweight reactivity with x-data directives"
126
- },
127
- {
128
- value: "crank",
129
- label: "Crank.js",
130
- hint: "JSX components rendered on the server"
131
- },
132
- {
133
- value: "htmx",
134
- label: "HTMX",
135
- hint: "HTML-driven interactions with hx- attributes"
136
- },
137
- {
138
- value: "vanilla",
139
- label: "Vanilla",
140
- hint: "Plain HTML, no framework"
141
- }
142
- ]
143
- });
144
- if (typeof framework === "symbol") {
145
- outro("Project creation cancelled");
146
- process.exit(0);
120
+ if (template === "static-site" || template === "full-stack") {
121
+ if (flags.framework) {
122
+ const valid = ["vanilla", "htmx", "alpine", "crank"];
123
+ if (!valid.includes(flags.framework)) {
124
+ console.error(
125
+ `Error: Unknown framework "${flags.framework}". Valid options: ${valid.join(", ")}`
126
+ );
127
+ process.exit(1);
128
+ }
129
+ uiFramework = flags.framework;
130
+ } else {
131
+ const framework = await select({
132
+ message: "UI framework:",
133
+ initialValue: "crank",
134
+ options: [
135
+ {
136
+ value: "alpine",
137
+ label: "Alpine.js",
138
+ hint: "Lightweight reactivity with x-data directives"
139
+ },
140
+ {
141
+ value: "crank",
142
+ label: "Crank.js",
143
+ hint: "JSX components rendered on the server"
144
+ },
145
+ {
146
+ value: "htmx",
147
+ label: "HTMX",
148
+ hint: "HTML-driven interactions with hx- attributes"
149
+ },
150
+ {
151
+ value: "vanilla",
152
+ label: "Vanilla",
153
+ hint: "Plain HTML, no framework"
154
+ }
155
+ ]
156
+ });
157
+ if (typeof framework === "symbol") {
158
+ outro("Project creation cancelled");
159
+ process.exit(0);
160
+ }
161
+ uiFramework = framework;
162
+ }
163
+ }
164
+ let useJSX = true;
165
+ if (uiFramework === "crank") {
166
+ if (flags.jsx !== void 0) {
167
+ useJSX = flags.jsx;
168
+ } else {
169
+ const jsxResult = await confirm({
170
+ message: "Use JSX?",
171
+ initialValue: true
172
+ });
173
+ if (typeof jsxResult === "symbol") {
174
+ outro("Project creation cancelled");
175
+ process.exit(0);
176
+ }
177
+ useJSX = jsxResult;
147
178
  }
148
- uiFramework = framework;
149
179
  }
150
180
  let typescript;
151
181
  if (flags.typescript !== void 0) {
@@ -205,7 +235,8 @@ async function main() {
205
235
  platform,
206
236
  template,
207
237
  typescript,
208
- uiFramework
238
+ uiFramework,
239
+ useJSX
209
240
  };
210
241
  const s = spinner();
211
242
  s.start("Creating your Shovel project...");
@@ -213,12 +244,13 @@ async function main() {
213
244
  await createProject(config, projectPath);
214
245
  s.stop("Project created");
215
246
  console.info("");
216
- outro("Your Shovel project is ready!");
247
+ outro("Your project is shovel-ready!");
248
+ const pm = platform === "bun" ? "bun" : "npm";
217
249
  console.info("");
218
250
  console.info("Next steps:");
219
251
  console.info(` cd ${projectName}`);
220
- console.info(` npm install`);
221
- console.info(` npm run develop`);
252
+ console.info(` ${pm} install`);
253
+ console.info(` ${pm} run develop`);
222
254
  console.info("");
223
255
  console.info("Your app will be available at: http://localhost:7777");
224
256
  console.info("");
@@ -231,37 +263,55 @@ async function main() {
231
263
  async function createProject(config, projectPath) {
232
264
  await mkdir(projectPath, { recursive: true });
233
265
  await mkdir(join(projectPath, "src"), { recursive: true });
234
- const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
266
+ const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
267
+ const isCrank = config.uiFramework === "crank";
268
+ const entryFile = isCrank ? `src/server.${ext}` : `src/app.${ext}`;
235
269
  const startCmd = config.platform === "bun" ? "bun dist/server/supervisor.js" : "node dist/server/supervisor.js";
236
270
  const dependencies = {
237
271
  "@b9g/router": "^0.2.0",
238
272
  "@b9g/shovel": "^0.2.0"
239
273
  };
240
- if (config.uiFramework === "crank") {
274
+ if (isCrank) {
241
275
  dependencies["@b9g/crank"] = "^0.7.2";
242
276
  }
277
+ const devDependencies = {};
278
+ if (config.typescript) {
279
+ devDependencies["@types/node"] = "^18.0.0";
280
+ devDependencies["typescript"] = "^5.0.0";
281
+ }
282
+ if (isCrank) {
283
+ devDependencies["eslint"] = "^9.0.0";
284
+ devDependencies["@eslint/js"] = "^9.0.0";
285
+ }
286
+ const scripts = {
287
+ develop: `shovel develop ${entryFile} --platform ${config.platform}`,
288
+ build: `shovel build ${entryFile} --platform ${config.platform}`,
289
+ start: startCmd
290
+ };
291
+ if (isCrank) {
292
+ scripts.lint = "eslint src/";
293
+ }
243
294
  const packageJson = {
244
295
  name: config.name,
245
296
  private: true,
246
297
  version: "0.0.1",
247
298
  type: "module",
248
- scripts: {
249
- develop: `shovel develop src/app.${ext} --platform ${config.platform}`,
250
- build: `shovel build src/app.${ext} --platform ${config.platform}`,
251
- start: startCmd
252
- },
299
+ scripts,
253
300
  dependencies,
254
- devDependencies: config.typescript ? {
255
- "@types/node": "^18.0.0",
256
- typescript: "^5.0.0"
257
- } : {}
301
+ devDependencies
258
302
  };
259
303
  await writeFile(
260
304
  join(projectPath, "package.json"),
261
305
  JSON.stringify(packageJson, null, 2)
262
306
  );
263
- const appFile = generateAppFile(config);
264
- await writeFile(join(projectPath, `src/app.${ext}`), appFile);
307
+ const appResult = generateAppFile(config);
308
+ if (typeof appResult === "string") {
309
+ await writeFile(join(projectPath, `src/app.${ext}`), appResult);
310
+ } else {
311
+ for (const [filename, content] of Object.entries(appResult)) {
312
+ await writeFile(join(projectPath, `src/${filename}`), content);
313
+ }
314
+ }
265
315
  if (config.typescript) {
266
316
  const compilerOptions = {
267
317
  target: "ES2022",
@@ -274,12 +324,15 @@ async function createProject(config, projectPath) {
274
324
  forceConsistentCasingInFileNames: true,
275
325
  lib: ["ES2022", "WebWorker"]
276
326
  };
277
- if (config.uiFramework === "crank") {
327
+ if (config.uiFramework === "crank" && config.useJSX) {
278
328
  compilerOptions.jsx = "react-jsx";
279
329
  compilerOptions.jsxImportSource = "@b9g/crank";
280
330
  }
281
331
  const tsConfig = {
282
- compilerOptions,
332
+ compilerOptions: {
333
+ ...compilerOptions,
334
+ types: ["@b9g/platform/globals"]
335
+ },
283
336
  include: ["src/**/*"],
284
337
  exclude: ["node_modules", "dist"]
285
338
  };
@@ -287,18 +340,16 @@ async function createProject(config, projectPath) {
287
340
  join(projectPath, "tsconfig.json"),
288
341
  JSON.stringify(tsConfig, null, 2)
289
342
  );
290
- const envDts = `/// <reference lib="WebWorker" />
291
-
292
- // Shovel runs your code in a ServiceWorker-like environment.
293
- // This augments the Worker types with ServiceWorker events
294
- // so self.addEventListener("fetch", ...) etc. are properly typed.
295
- interface WorkerGlobalScopeEventMap {
296
- fetch: FetchEvent;
297
- install: ExtendableEvent;
298
- activate: ExtendableEvent;
299
- }
343
+ }
344
+ if (isCrank) {
345
+ const eslintConfig = `import js from "@eslint/js";
346
+
347
+ export default [
348
+ js.configs.recommended,
349
+ { ignores: ["dist/"] },
350
+ ];
300
351
  `;
301
- await writeFile(join(projectPath, "src/env.d.ts"), envDts);
352
+ await writeFile(join(projectPath, "eslint.config.js"), eslintConfig);
302
353
  }
303
354
  const readme = generateReadme(config);
304
355
  await writeFile(join(projectPath, "README.md"), readme);
@@ -600,16 +651,50 @@ ${css}
600
651
  }
601
652
  function generateStaticSiteCrank(config) {
602
653
  const t = config.typescript;
603
- return `import {renderer} from "@b9g/crank/html";
654
+ const ext = config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
655
+ if (config.useJSX) {
656
+ return {
657
+ [`server.${ext}`]: `import {renderer} from "@b9g/crank/html";
658
+ import {Router} from "@b9g/router";
659
+ import {Page} from "./components";
604
660
 
605
- // ${config.name} - Static Site with Crank.js
606
- // Server-rendered HTML with JSX components
661
+ const router = new Router();
607
662
 
608
- const css = \`
663
+ router.route("/").get(async () => {
664
+ const html = await renderer.render(
665
+ <Page title="Home">
666
+ <h1>Welcome to ${config.name}</h1>
667
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
668
+ <p><a href="/about">About</a></p>
669
+ </Page>
670
+ );
671
+ return new Response("<!DOCTYPE html>" + html, {
672
+ headers: { "Content-Type": "text/html" },
673
+ });
674
+ });
675
+
676
+ router.route("/about").get(async () => {
677
+ const html = await renderer.render(
678
+ <Page title="About">
679
+ <h1>About</h1>
680
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
681
+ <p><a href="/">Home</a></p>
682
+ </Page>
683
+ );
684
+ return new Response("<!DOCTYPE html>" + html, {
685
+ headers: { "Content-Type": "text/html" },
686
+ });
687
+ });
688
+
689
+ self.addEventListener("fetch", (event) => {
690
+ event.respondWith(router.handle(event.request));
691
+ });
692
+ `,
693
+ [`components.${ext}`]: `const css = \`
609
694
  ${css}
610
695
  \`;
611
696
 
612
- function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
697
+ export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
613
698
  return (
614
699
  <html lang="en">
615
700
  <head>
@@ -624,40 +709,70 @@ function Page({title, children}${t ? ": {title: string, children: unknown}" : ""
624
709
  </html>
625
710
  );
626
711
  }
712
+ `
713
+ };
714
+ }
715
+ return {
716
+ [`server.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
717
+ import {renderer} from "@b9g/crank/html";
718
+ import {Router} from "@b9g/router";
719
+ import {Page} from "./components";
627
720
 
628
- self.addEventListener("fetch", (event) => {
629
- event.respondWith(handleRequest(event.request));
630
- });
631
-
632
- async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
633
- const url = new URL(request.url);
634
- let html${t ? ": string" : ""};
721
+ const router = new Router();
635
722
 
636
- if (url.pathname === "/") {
637
- html = await renderer.render(
638
- <Page title="Home">
639
- <h1>Welcome to ${config.name}</h1>
640
- <p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
641
- <p><a href="/about">About</a></p>
642
- </Page>
643
- );
644
- } else if (url.pathname === "/about") {
645
- html = await renderer.render(
646
- <Page title="About">
647
- <h1>About</h1>
648
- <p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
649
- <p><a href="/">Home</a></p>
650
- </Page>
651
- );
652
- } else {
653
- return new Response("Not Found", { status: 404 });
654
- }
723
+ router.route("/").get(async () => {
724
+ const html = await renderer.render(jsx\`
725
+ <\${Page} title="Home">
726
+ <h1>Welcome to ${config.name}</h1>
727
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
728
+ <p><a href="/about">About</a></p>
729
+ </\${Page}>
730
+ \`);
731
+ return new Response("<!DOCTYPE html>" + html, {
732
+ headers: { "Content-Type": "text/html" },
733
+ });
734
+ });
655
735
 
736
+ router.route("/about").get(async () => {
737
+ const html = await renderer.render(jsx\`
738
+ <\${Page} title="About">
739
+ <h1>About</h1>
740
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
741
+ <p><a href="/">Home</a></p>
742
+ </\${Page}>
743
+ \`);
656
744
  return new Response("<!DOCTYPE html>" + html, {
657
745
  headers: { "Content-Type": "text/html" },
658
746
  });
747
+ });
748
+
749
+ self.addEventListener("fetch", (event) => {
750
+ event.respondWith(router.handle(event.request));
751
+ });
752
+ `,
753
+ [`components.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
754
+
755
+ const css = \`
756
+ ${css}
757
+ \`;
758
+
759
+ export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
760
+ return jsx\`
761
+ <html lang="en">
762
+ <head>
763
+ <meta charset="UTF-8" />
764
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
765
+ <title>\${title} - ${config.name}</title>
766
+ <style>\${css}</style>
767
+ </head>
768
+ <body>
769
+ <main>\${children}</main>
770
+ </body>
771
+ </html>
772
+ \`;
659
773
  }
660
- `;
774
+ `
775
+ };
661
776
  }
662
777
  function generateFullStack(config) {
663
778
  switch (config.uiFramework) {
@@ -895,18 +1010,69 @@ self.addEventListener("fetch", (event) => {
895
1010
  }
896
1011
  function generateFullStackCrank(config) {
897
1012
  const t = config.typescript;
898
- return `import { Router } from "@b9g/router";
899
- import { logger } from "@b9g/router/middleware";
900
- import {renderer} from "@b9g/crank/html";
1013
+ const ext = config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
1014
+ if (config.useJSX) {
1015
+ return {
1016
+ [`server.${ext}`]: `import {renderer} from "@b9g/crank/html";
1017
+ import {Router} from "@b9g/router";
1018
+ import {logger} from "@b9g/router/middleware";
1019
+ import {Page} from "./components";
901
1020
 
902
1021
  const router = new Router();
903
1022
  router.use(logger());
904
1023
 
905
- const css = \`
1024
+ // API routes
1025
+ router.route("/api/hello").get(() => {
1026
+ return Response.json({
1027
+ message: "Hello from the API!",
1028
+ timestamp: new Date().toISOString(),
1029
+ });
1030
+ });
1031
+
1032
+ router.route("/api/echo").post(async (req) => {
1033
+ const body = await req.json();
1034
+ return Response.json({ echo: body });
1035
+ });
1036
+
1037
+ // HTML pages
1038
+ router.route("/").get(async () => {
1039
+ const html = await renderer.render(
1040
+ <Page title="Home">
1041
+ <h1>Welcome to ${config.name}</h1>
1042
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
1043
+ <ul>
1044
+ <li><a href="/about">About</a></li>
1045
+ <li><a href="/api/hello">API: /api/hello</a></li>
1046
+ </ul>
1047
+ </Page>
1048
+ );
1049
+ return new Response("<!DOCTYPE html>" + html, {
1050
+ headers: { "Content-Type": "text/html" },
1051
+ });
1052
+ });
1053
+
1054
+ router.route("/about").get(async () => {
1055
+ const html = await renderer.render(
1056
+ <Page title="About">
1057
+ <h1>About</h1>
1058
+ <p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
1059
+ <p><a href="/">Home</a></p>
1060
+ </Page>
1061
+ );
1062
+ return new Response("<!DOCTYPE html>" + html, {
1063
+ headers: { "Content-Type": "text/html" },
1064
+ });
1065
+ });
1066
+
1067
+ self.addEventListener("fetch", (event) => {
1068
+ event.respondWith(router.handle(event.request));
1069
+ });
1070
+ `,
1071
+ [`components.${ext}`]: `const css = \`
906
1072
  ${css}
907
1073
  \`;
908
1074
 
909
- function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
1075
+ export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
910
1076
  return (
911
1077
  <html lang="en">
912
1078
  <head>
@@ -921,6 +1087,18 @@ function Page({title, children}${t ? ": {title: string, children: unknown}" : ""
921
1087
  </html>
922
1088
  );
923
1089
  }
1090
+ `
1091
+ };
1092
+ }
1093
+ return {
1094
+ [`server.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
1095
+ import {renderer} from "@b9g/crank/html";
1096
+ import {Router} from "@b9g/router";
1097
+ import {logger} from "@b9g/router/middleware";
1098
+ import {Page} from "./components";
1099
+
1100
+ const router = new Router();
1101
+ router.use(logger());
924
1102
 
925
1103
  // API routes
926
1104
  router.route("/api/hello").get(() => {
@@ -937,29 +1115,29 @@ router.route("/api/echo").post(async (req) => {
937
1115
 
938
1116
  // HTML pages
939
1117
  router.route("/").get(async () => {
940
- const html = await renderer.render(
941
- <Page title="Home">
1118
+ const html = await renderer.render(jsx\`
1119
+ <\${Page} title="Home">
942
1120
  <h1>Welcome to ${config.name}</h1>
943
- <p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
1121
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
944
1122
  <ul>
945
1123
  <li><a href="/about">About</a></li>
946
1124
  <li><a href="/api/hello">API: /api/hello</a></li>
947
1125
  </ul>
948
- </Page>
949
- );
1126
+ </\${Page}>
1127
+ \`);
950
1128
  return new Response("<!DOCTYPE html>" + html, {
951
1129
  headers: { "Content-Type": "text/html" },
952
1130
  });
953
1131
  });
954
1132
 
955
1133
  router.route("/about").get(async () => {
956
- const html = await renderer.render(
957
- <Page title="About">
1134
+ const html = await renderer.render(jsx\`
1135
+ <\${Page} title="About">
958
1136
  <h1>About</h1>
959
1137
  <p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
960
1138
  <p><a href="/">Home</a></p>
961
- </Page>
962
- );
1139
+ </\${Page}>
1140
+ \`);
963
1141
  return new Response("<!DOCTYPE html>" + html, {
964
1142
  headers: { "Content-Type": "text/html" },
965
1143
  });
@@ -968,7 +1146,30 @@ router.route("/about").get(async () => {
968
1146
  self.addEventListener("fetch", (event) => {
969
1147
  event.respondWith(router.handle(event.request));
970
1148
  });
971
- `;
1149
+ `,
1150
+ [`components.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
1151
+
1152
+ const css = \`
1153
+ ${css}
1154
+ \`;
1155
+
1156
+ export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
1157
+ return jsx\`
1158
+ <html lang="en">
1159
+ <head>
1160
+ <meta charset="UTF-8" />
1161
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1162
+ <title>\${title} - ${config.name}</title>
1163
+ <style>\${css}</style>
1164
+ </head>
1165
+ <body>
1166
+ <main>\${children}</main>
1167
+ </body>
1168
+ </html>
1169
+ \`;
1170
+ }
1171
+ `
1172
+ };
972
1173
  }
973
1174
  function generateReadme(config) {
974
1175
  const templateDescriptions = {
@@ -983,7 +1184,24 @@ function generateReadme(config) {
983
1184
  alpine: " using [Alpine.js](https://alpinejs.dev)",
984
1185
  crank: " using [Crank.js](https://crank.js.org)"
985
1186
  };
986
- const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
1187
+ const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
1188
+ const isCrank = config.uiFramework === "crank";
1189
+ let projectTree;
1190
+ if (isCrank) {
1191
+ projectTree = `${config.name}/
1192
+ \u251C\u2500\u2500 src/
1193
+ \u2502 \u251C\u2500\u2500 server.${ext} # Application entry point
1194
+ \u2502 \u2514\u2500\u2500 components.${ext} # Page components
1195
+ \u251C\u2500\u2500 eslint.config.js
1196
+ \u251C\u2500\u2500 package.json
1197
+ ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1198
+ } else {
1199
+ projectTree = `${config.name}/
1200
+ \u251C\u2500\u2500 src/
1201
+ \u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
1202
+ \u251C\u2500\u2500 package.json
1203
+ ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1204
+ }
987
1205
  return `# ${config.name}
988
1206
 
989
1207
  ${templateDescriptions[config.template]}${frameworkDescriptions[config.uiFramework]}, built with [Shovel](https://github.com/bikeshaving/shovel).
@@ -1001,16 +1219,12 @@ Open http://localhost:7777
1001
1219
 
1002
1220
  - \`npm run develop\` - Start development server
1003
1221
  - \`npm run build\` - Build for production
1004
- - \`npm start\` - Run production build
1222
+ - \`npm start\` - Run production build${isCrank ? "\n- `npm run lint` - Lint source files" : ""}
1005
1223
 
1006
1224
  ## Project Structure
1007
1225
 
1008
1226
  \`\`\`
1009
- ${config.name}/
1010
- \u251C\u2500\u2500 src/
1011
- \u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
1012
- \u251C\u2500\u2500 package.json
1013
- ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md
1227
+ ${projectTree}
1014
1228
  \`\`\`
1015
1229
 
1016
1230
  ## Learn More
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "ServiceWorker-first universal deployment platform. Write ServiceWorker apps once, deploy anywhere (Node/Bun/Cloudflare). Registry-based multi-app orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "devDependencies": {
36
36
  "@b9g/assets": "^0.2.1",
37
37
  "@b9g/crank": "^0.7.2",
38
- "@b9g/libuild": "^0.1.22",
38
+ "@b9g/libuild": "^0.1.24",
39
39
  "@b9g/router": "^0.2.2",
40
40
  "@logtape/file": "^2.0.0",
41
41
  "@types/bun": "^1.3.4",
@@ -53,19 +53,19 @@
53
53
  "./package.json": "./package.json",
54
54
  "./bin/cli": {
55
55
  "types": "./src/undefined.d.ts",
56
- "import": "./dist/bin/cli.js"
56
+ "import": "./bin/cli.js"
57
57
  },
58
58
  "./bin/cli.js": {
59
59
  "types": "./src/undefined.d.ts",
60
- "import": "./dist/bin/cli.js"
60
+ "import": "./bin/cli.js"
61
61
  },
62
62
  "./bin/create": {
63
- "types": "./dist/bin/create.d.ts",
64
- "import": "./dist/bin/create.js"
63
+ "types": "./bin/create.d.ts",
64
+ "import": "./bin/create.js"
65
65
  },
66
66
  "./bin/create.js": {
67
- "types": "./dist/bin/create.d.ts",
68
- "import": "./dist/bin/create.js"
67
+ "types": "./bin/create.d.ts",
68
+ "import": "./bin/create.js"
69
69
  }
70
70
  }
71
71
  }