@b9g/shovel 0.2.13 → 0.2.15

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 (2) hide show
  1. package/bin/create.js +282 -154
  2. package/package.json +2 -2
package/bin/create.js CHANGED
@@ -265,31 +265,48 @@ async function createProject(config, projectPath) {
265
265
  await mkdir(join(projectPath, "src"), { recursive: true });
266
266
  const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
267
267
  const isCrank = config.uiFramework === "crank";
268
- const entryFile = isCrank ? `src/server.${ext}` : `src/app.${ext}`;
268
+ const hasClientBundle = config.template === "static-site" || config.template === "full-stack";
269
+ const entryFile = hasClientBundle ? `src/server.${ext}` : `src/app.${ext}`;
269
270
  const startCmd = config.platform === "bun" ? "bun dist/server/supervisor.js" : "node dist/server/supervisor.js";
270
271
  const dependencies = {
271
272
  "@b9g/router": "^0.2.0",
272
273
  "@b9g/shovel": "^0.2.0"
273
274
  };
275
+ if (hasClientBundle) {
276
+ dependencies["@b9g/assets"] = "^0.2.0";
277
+ }
274
278
  if (isCrank) {
275
279
  dependencies["@b9g/crank"] = "^0.7.2";
276
- dependencies["@b9g/assets"] = "^0.2.0";
280
+ }
281
+ if (config.uiFramework === "htmx") {
282
+ dependencies["htmx.org"] = "^2.0.0";
283
+ }
284
+ if (config.uiFramework === "alpine") {
285
+ dependencies["alpinejs"] = "^3.14.0";
277
286
  }
278
287
  const devDependencies = {};
279
288
  if (config.typescript) {
280
289
  devDependencies["@types/node"] = "^18.0.0";
281
290
  devDependencies["typescript"] = "^5.0.0";
282
291
  }
283
- if (isCrank) {
292
+ if (hasClientBundle) {
284
293
  devDependencies["eslint"] = "^10.0.0";
285
294
  devDependencies["@eslint/js"] = "^10.0.0";
295
+ if (config.typescript) {
296
+ devDependencies["typescript-eslint"] = "^8.0.0";
297
+ } else {
298
+ devDependencies["globals"] = "^16.0.0";
299
+ }
300
+ if (isCrank) {
301
+ devDependencies["eslint-plugin-crank"] = "^0.2.0";
302
+ }
286
303
  }
287
304
  const scripts = {
288
305
  develop: `shovel develop ${entryFile} --platform ${config.platform}`,
289
306
  build: `shovel build ${entryFile} --platform ${config.platform}`,
290
307
  start: startCmd
291
308
  };
292
- if (isCrank) {
309
+ if (hasClientBundle) {
293
310
  scripts.lint = "eslint src/";
294
311
  }
295
312
  const packageJSON = {
@@ -322,34 +339,61 @@ async function createProject(config, projectPath) {
322
339
  esModuleInterop: true,
323
340
  strict: true,
324
341
  skipLibCheck: true,
342
+ noEmit: true,
343
+ allowImportingTsExtensions: true,
325
344
  forceConsistentCasingInFileNames: true,
326
- lib: ["ES2022", "WebWorker"]
345
+ lib: ["ES2022", "DOM", "DOM.Iterable", "WebWorker"]
327
346
  };
328
347
  if (config.uiFramework === "crank" && config.useJSX) {
329
348
  compilerOptions.jsx = "react-jsx";
330
349
  compilerOptions.jsxImportSource = "@b9g/crank";
331
350
  }
332
351
  const tsConfig = {
333
- compilerOptions: {
334
- ...compilerOptions,
335
- types: ["@b9g/platform/globals"]
336
- },
337
- include: ["src/**/*"],
338
- exclude: ["node_modules", "dist"]
352
+ compilerOptions,
353
+ include: [
354
+ "src/**/*",
355
+ "node_modules/@b9g/platform/src/globals.d.ts",
356
+ "dist/server/shovel.d.ts"
357
+ ],
358
+ exclude: []
339
359
  };
340
360
  await writeFile(
341
361
  join(projectPath, "tsconfig.json"),
342
362
  JSON.stringify(tsConfig, null, 2)
343
363
  );
344
364
  }
345
- if (isCrank) {
346
- const eslintConfig = `import js from "@eslint/js";
347
-
348
- export default [
365
+ if (hasClientBundle) {
366
+ const crankImport = isCrank ? `import crank from "eslint-plugin-crank";
367
+ ` : "";
368
+ const crankConfig = isCrank ? `
369
+ { plugins: { crank }, rules: crank.configs.recommended.rules },` : "";
370
+ let eslintConfig;
371
+ if (config.typescript) {
372
+ eslintConfig = `import js from "@eslint/js";
373
+ import tseslint from "typescript-eslint";
374
+ ${crankImport}
375
+ export default tseslint.config(
349
376
  js.configs.recommended,
350
- { ignores: ["dist/"] },
377
+ tseslint.configs.recommended,
378
+ { ignores: ["dist/"] },${crankConfig}
379
+ );
380
+ `;
381
+ } else {
382
+ let langOpts = "globals: globals.browser";
383
+ let filesOpt = "";
384
+ if (isCrank && config.useJSX) {
385
+ filesOpt = `files: ["**/*.{js,jsx}"], `;
386
+ langOpts += ", parserOptions: { ecmaFeatures: { jsx: true } }";
387
+ }
388
+ eslintConfig = `import js from "@eslint/js";
389
+ import globals from "globals";
390
+ ${crankImport}
391
+ export default [
392
+ { ${filesOpt}...js.configs.recommended, languageOptions: { ${langOpts} } },
393
+ { ignores: ["dist/"] },${crankConfig}
351
394
  ];
352
395
  `;
396
+ }
353
397
  await writeFile(join(projectPath, "eslint.config.js"), eslintConfig);
354
398
  }
355
399
  const readme = generateReadme(config);
@@ -477,44 +521,35 @@ function generateStaticSite(config) {
477
521
  function generateStaticSiteVanilla(config) {
478
522
  const ext = config.typescript ? "ts" : "js";
479
523
  const t = config.typescript;
480
- return `// ${config.name} - Static Site
481
- // Renders HTML pages server-side
482
-
483
- self.addEventListener("fetch", (event) => {
484
- event.respondWith(handleRequest(event.request));
485
- });
486
-
487
- async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
488
- const url = new URL(request.url);
524
+ return {
525
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
526
+ import {assets} from "@b9g/assets/middleware";
527
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
528
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
489
529
 
490
- if (url.pathname === "/") {
491
- return new Response(renderPage("Home", \`
492
- <h1>Welcome to ${config.name}</h1>
493
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
494
- <button id="counter">Clicked: 0</button>
495
- <script>
496
- let count = 0;
497
- const btn = document.getElementById("counter");
498
- btn.addEventListener("click", () => btn.textContent = "Clicked: " + ++count);
499
- </script>
500
- <p><a href="/about">About</a></p>
501
- \`), {
502
- headers: { "Content-Type": "text/html" },
503
- });
504
- }
530
+ const router = new Router();
531
+ router.use(assets());
505
532
 
506
- if (url.pathname === "/about") {
507
- return new Response(renderPage("About", \`
508
- <h1>About</h1>
509
- <p>This is a static site built with <strong>Shovel</strong>.</p>
510
- <p><a href="/">Home</a></p>
511
- \`), {
512
- headers: { "Content-Type": "text/html" },
513
- });
514
- }
533
+ router.route("/").get(() => {
534
+ return new Response(renderPage("Home", \`
535
+ <h1>Welcome to ${config.name}</h1>
536
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
537
+ <button id="counter">Clicked: 0</button>
538
+ <p><a href="/about">About</a></p>
539
+ \`), {
540
+ headers: { "Content-Type": "text/html" },
541
+ });
542
+ });
515
543
 
516
- return new Response("Not Found", { status: 404 });
517
- }
544
+ router.route("/about").get(() => {
545
+ return new Response(renderPage("About", \`
546
+ <h1>About</h1>
547
+ <p>This is a static site built with <strong>Shovel</strong>.</p>
548
+ <p><a href="/">Home</a></p>
549
+ \`), {
550
+ headers: { "Content-Type": "text/html" },
551
+ });
552
+ });
518
553
 
519
554
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
520
555
  return \`<!DOCTYPE html>
@@ -529,56 +564,64 @@ ${css}
529
564
  </head>
530
565
  <body>
531
566
  <main>\${content}</main>
567
+ <script src="\${clientUrl}" type="module"></script>
532
568
  </body>
533
569
  </html>\`;
534
570
  }
535
- `;
571
+
572
+ self.addEventListener("fetch", (event) => {
573
+ event.respondWith(router.handle(event.request));
574
+ });
575
+ `,
576
+ [`client.${ext}`]: `const btn = document.getElementById("counter");
577
+ if (btn) {
578
+ let count = 0;
579
+ btn.addEventListener("click", () => btn.textContent = "Clicked: " + ++count);
580
+ }
581
+ `
582
+ };
536
583
  }
537
584
  function generateStaticSiteHtmx(config) {
538
585
  const ext = config.typescript ? "ts" : "js";
539
586
  const t = config.typescript;
540
- return `// ${config.name} - Static Site with HTMX
541
- // Server-rendered HTML with HTMX interactions
542
-
543
- self.addEventListener("fetch", (event) => {
544
- event.respondWith(handleRequest(event.request));
545
- });
546
-
547
- async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
548
- const url = new URL(request.url);
587
+ const files = {
588
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
589
+ import {assets} from "@b9g/assets/middleware";
590
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
591
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
549
592
 
550
- if (url.pathname === "/") {
551
- return new Response(renderPage("Home", \`
552
- <h1>Welcome to ${config.name}</h1>
553
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
554
- <button hx-get="/greeting" hx-target="#result" hx-swap="innerHTML">Get Greeting</button>
555
- <div id="result"></div>
556
- <p><a href="/about">About</a></p>
557
- \`), {
558
- headers: { "Content-Type": "text/html" },
559
- });
560
- }
593
+ const router = new Router();
594
+ router.use(assets());
561
595
 
562
- if (url.pathname === "/greeting") {
563
- const hour = new Date().getHours();
564
- const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
565
- return new Response(\`<p>\${greeting}! The time is \${new Date().toLocaleTimeString()}.</p>\`, {
566
- headers: { "Content-Type": "text/html" },
567
- });
568
- }
596
+ router.route("/").get(() => {
597
+ return new Response(renderPage("Home", \`
598
+ <h1>Welcome to ${config.name}</h1>
599
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
600
+ <button hx-get="/greeting" hx-target="#result" hx-swap="innerHTML">Get Greeting</button>
601
+ <div id="result"></div>
602
+ <p><a href="/about">About</a></p>
603
+ \`), {
604
+ headers: { "Content-Type": "text/html" },
605
+ });
606
+ });
569
607
 
570
- if (url.pathname === "/about") {
571
- return new Response(renderPage("About", \`
572
- <h1>About</h1>
573
- <p>This is a static site built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
574
- <p><a href="/">Home</a></p>
575
- \`), {
576
- headers: { "Content-Type": "text/html" },
577
- });
578
- }
608
+ router.route("/greeting").get(() => {
609
+ const hour = new Date().getHours();
610
+ const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
611
+ return new Response(\`<p>\${greeting}! The time is \${new Date().toLocaleTimeString()}.</p>\`, {
612
+ headers: { "Content-Type": "text/html" },
613
+ });
614
+ });
579
615
 
580
- return new Response("Not Found", { status: 404 });
581
- }
616
+ router.route("/about").get(() => {
617
+ return new Response(renderPage("About", \`
618
+ <h1>About</h1>
619
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
620
+ <p><a href="/">Home</a></p>
621
+ \`), {
622
+ headers: { "Content-Type": "text/html" },
623
+ });
624
+ });
582
625
 
583
626
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
584
627
  return \`<!DOCTYPE html>
@@ -587,56 +630,64 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
587
630
  <meta charset="UTF-8">
588
631
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
589
632
  <title>\${title} - ${config.name}</title>
590
- <script src="https://unpkg.com/htmx.org@2.0.4"></script>
591
633
  <style>
592
634
  ${css}
593
635
  </style>
594
636
  </head>
595
637
  <body>
596
638
  <main>\${content}</main>
639
+ <script src="\${clientUrl}" type="module"></script>
597
640
  </body>
598
641
  </html>\`;
599
642
  }
643
+
644
+ self.addEventListener("fetch", (event) => {
645
+ event.respondWith(router.handle(event.request));
646
+ });
647
+ `,
648
+ [`client.${ext}`]: `import "htmx.org";
649
+ `
650
+ };
651
+ if (t) {
652
+ files[`env.d.ts`] = `declare module "htmx.org";
600
653
  `;
654
+ }
655
+ return files;
601
656
  }
602
657
  function generateStaticSiteAlpine(config) {
603
658
  const ext = config.typescript ? "ts" : "js";
604
659
  const t = config.typescript;
605
- return `// ${config.name} - Static Site with Alpine.js
606
- // Server-rendered HTML with Alpine.js interactions
607
-
608
- self.addEventListener("fetch", (event) => {
609
- event.respondWith(handleRequest(event.request));
610
- });
611
-
612
- async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
613
- const url = new URL(request.url);
660
+ const files = {
661
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
662
+ import {assets} from "@b9g/assets/middleware";
663
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
664
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
614
665
 
615
- if (url.pathname === "/") {
616
- return new Response(renderPage("Home", \`
617
- <h1>Welcome to ${config.name}</h1>
618
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
619
- <div x-data="{ count: 0 }">
620
- <button @click="count++">Clicked: <span x-text="count"></span></button>
621
- </div>
622
- <p><a href="/about">About</a></p>
623
- \`), {
624
- headers: { "Content-Type": "text/html" },
625
- });
626
- }
666
+ const router = new Router();
667
+ router.use(assets());
627
668
 
628
- if (url.pathname === "/about") {
629
- return new Response(renderPage("About", \`
630
- <h1>About</h1>
631
- <p>This is a static site built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
632
- <p><a href="/">Home</a></p>
633
- \`), {
634
- headers: { "Content-Type": "text/html" },
635
- });
636
- }
669
+ router.route("/").get(() => {
670
+ return new Response(renderPage("Home", \`
671
+ <h1>Welcome to ${config.name}</h1>
672
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
673
+ <div x-data="{ count: 0 }">
674
+ <button @click="count++">Clicked: <span x-text="count"></span></button>
675
+ </div>
676
+ <p><a href="/about">About</a></p>
677
+ \`), {
678
+ headers: { "Content-Type": "text/html" },
679
+ });
680
+ });
637
681
 
638
- return new Response("Not Found", { status: 404 });
639
- }
682
+ router.route("/about").get(() => {
683
+ return new Response(renderPage("About", \`
684
+ <h1>About</h1>
685
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
686
+ <p><a href="/">Home</a></p>
687
+ \`), {
688
+ headers: { "Content-Type": "text/html" },
689
+ });
690
+ });
640
691
 
641
692
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
642
693
  return \`<!DOCTYPE html>
@@ -645,17 +696,38 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
645
696
  <meta charset="UTF-8">
646
697
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
647
698
  <title>\${title} - ${config.name}</title>
648
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
649
699
  <style>
650
700
  ${css}
651
701
  </style>
652
702
  </head>
653
703
  <body>
654
704
  <main>\${content}</main>
705
+ <script src="\${clientUrl}" type="module"></script>
655
706
  </body>
656
707
  </html>\`;
657
708
  }
709
+
710
+ self.addEventListener("fetch", (event) => {
711
+ event.respondWith(router.handle(event.request));
712
+ });
713
+ `,
714
+ [`client.${ext}`]: `import Alpine from "alpinejs";
715
+ Alpine.start();
716
+ `
717
+ };
718
+ if (t) {
719
+ files[`env.d.ts`] = `declare module "alpinejs" {
720
+ interface Alpine {
721
+ start(): void;
722
+ plugin(plugin: unknown): void;
723
+ [key: string]: unknown;
724
+ }
725
+ const alpine: Alpine;
726
+ export default alpine;
727
+ }
658
728
  `;
729
+ }
730
+ return files;
659
731
  }
660
732
  function generateStaticSiteCrank(config) {
661
733
  const t = config.typescript;
@@ -666,7 +738,8 @@ function generateStaticSiteCrank(config) {
666
738
  import {Router} from "@b9g/router";
667
739
  import {assets} from "@b9g/assets/middleware";
668
740
  import {Page, Counter} from "./components";
669
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
741
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
742
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
670
743
 
671
744
  const router = new Router();
672
745
  router.use(assets());
@@ -726,8 +799,7 @@ export function Page({title, children, clientUrl}${t ? ": {title: string, childr
726
799
  export function *Counter(${t ? "this: Context" : ""}) {
727
800
  let count = 0;
728
801
  const handleClick = () => {
729
- count++;
730
- this.refresh();
802
+ this.refresh(() => count++);
731
803
  };
732
804
  for ({} of this) {
733
805
  yield <button onclick={handleClick}>Clicked: {count}</button>;
@@ -747,7 +819,8 @@ import {renderer} from "@b9g/crank/html";
747
819
  import {Router} from "@b9g/router";
748
820
  import {assets} from "@b9g/assets/middleware";
749
821
  import {Page, Counter} from "./components";
750
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
822
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
823
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
751
824
 
752
825
  const router = new Router();
753
826
  router.use(assets());
@@ -808,8 +881,7 @@ export function Page({title, children, clientUrl}${t ? ": {title: string, childr
808
881
  export function *Counter(${t ? "this: Context" : ""}) {
809
882
  let count = 0;
810
883
  const handleClick = () => {
811
- count++;
812
- this.refresh();
884
+ this.refresh(() => count++);
813
885
  };
814
886
  for ({} of this) {
815
887
  yield jsx\`<button onclick=\${handleClick}>Clicked: \${count}</button>\`;
@@ -838,11 +910,16 @@ function generateFullStack(config) {
838
910
  function generateFullStackVanilla(config) {
839
911
  const ext = config.typescript ? "ts" : "js";
840
912
  const t = config.typescript;
841
- return `import {Router} from "@b9g/router";
913
+ return {
914
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
842
915
  import {logger} from "@b9g/router/middleware";
916
+ import {assets} from "@b9g/assets/middleware";
917
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
918
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
843
919
 
844
920
  const router = new Router();
845
921
  router.use(logger());
922
+ router.use(assets());
846
923
 
847
924
  // API routes
848
925
  router.route("/api/hello").get(() => {
@@ -861,17 +938,9 @@ router.route("/api/echo").post(async (req) => {
861
938
  router.route("/").get(() => {
862
939
  return new Response(renderPage("Home", \`
863
940
  <h1>Welcome to ${config.name}</h1>
864
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
941
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
865
942
  <button id="call-api">Call API</button>
866
943
  <div id="result"></div>
867
- <script>
868
- document.getElementById("call-api").addEventListener("click", async () => {
869
- const res = await fetch("/api/hello");
870
- const data = await res.json();
871
- document.getElementById("result").innerHTML =
872
- "<p>" + data.message + "</p><p><small>" + data.timestamp + "</small></p>";
873
- });
874
- </script>
875
944
  <ul>
876
945
  <li><a href="/about">About</a></li>
877
946
  <li><a href="/api/hello">API: /api/hello</a></li>
@@ -904,6 +973,7 @@ ${css}
904
973
  </head>
905
974
  <body>
906
975
  <main>\${content}</main>
976
+ <script src="\${clientUrl}" type="module"></script>
907
977
  </body>
908
978
  </html>\`;
909
979
  }
@@ -911,16 +981,35 @@ ${css}
911
981
  self.addEventListener("fetch", (event) => {
912
982
  event.respondWith(router.handle(event.request));
913
983
  });
914
- `;
984
+ `,
985
+ [`client.${ext}`]: `const callBtn = document.getElementById("call-api");
986
+ if (callBtn) {
987
+ callBtn.addEventListener("click", async () => {
988
+ const res = await fetch("/api/hello");
989
+ const data = await res.json();
990
+ const result = document.getElementById("result");
991
+ if (result) {
992
+ result.innerHTML =
993
+ \`<p>\${data.message}</p><p><small>\${data.timestamp}</small></p>\`;
994
+ }
995
+ });
996
+ }
997
+ `
998
+ };
915
999
  }
916
1000
  function generateFullStackHtmx(config) {
917
1001
  const ext = config.typescript ? "ts" : "js";
918
1002
  const t = config.typescript;
919
- return `import {Router} from "@b9g/router";
1003
+ const files = {
1004
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
920
1005
  import {logger} from "@b9g/router/middleware";
1006
+ import {assets} from "@b9g/assets/middleware";
1007
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1008
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
921
1009
 
922
1010
  const router = new Router();
923
1011
  router.use(logger());
1012
+ router.use(assets());
924
1013
 
925
1014
  // API routes \u2014 return HTML fragments when HTMX requests, JSON otherwise
926
1015
  router.route("/api/hello").get((req) => {
@@ -946,7 +1035,7 @@ router.route("/api/echo").post(async (req) => {
946
1035
  router.route("/").get(() => {
947
1036
  return new Response(renderPage("Home", \`
948
1037
  <h1>Welcome to ${config.name}</h1>
949
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
1038
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
950
1039
  <button hx-get="/api/hello" hx-target="#result" hx-swap="innerHTML">Call API</button>
951
1040
  <div id="result"></div>
952
1041
  <ul>
@@ -975,13 +1064,13 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
975
1064
  <meta charset="UTF-8">
976
1065
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
977
1066
  <title>\${title} - ${config.name}</title>
978
- <script src="https://unpkg.com/htmx.org@2.0.4"></script>
979
1067
  <style>
980
1068
  ${css}
981
1069
  </style>
982
1070
  </head>
983
1071
  <body>
984
1072
  <main>\${content}</main>
1073
+ <script src="\${clientUrl}" type="module"></script>
985
1074
  </body>
986
1075
  </html>\`;
987
1076
  }
@@ -989,16 +1078,29 @@ ${css}
989
1078
  self.addEventListener("fetch", (event) => {
990
1079
  event.respondWith(router.handle(event.request));
991
1080
  });
1081
+ `,
1082
+ [`client.${ext}`]: `import "htmx.org";
1083
+ `
1084
+ };
1085
+ if (t) {
1086
+ files[`env.d.ts`] = `declare module "htmx.org";
992
1087
  `;
1088
+ }
1089
+ return files;
993
1090
  }
994
1091
  function generateFullStackAlpine(config) {
995
1092
  const ext = config.typescript ? "ts" : "js";
996
1093
  const t = config.typescript;
997
- return `import {Router} from "@b9g/router";
1094
+ const files = {
1095
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
998
1096
  import {logger} from "@b9g/router/middleware";
1097
+ import {assets} from "@b9g/assets/middleware";
1098
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1099
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
999
1100
 
1000
1101
  const router = new Router();
1001
1102
  router.use(logger());
1103
+ router.use(assets());
1002
1104
 
1003
1105
  // API routes
1004
1106
  router.route("/api/hello").get(() => {
@@ -1017,7 +1119,7 @@ router.route("/api/echo").post(async (req) => {
1017
1119
  router.route("/").get(() => {
1018
1120
  return new Response(renderPage("Home", \`
1019
1121
  <h1>Welcome to ${config.name}</h1>
1020
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
1122
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
1021
1123
  <div x-data="{ result: null }">
1022
1124
  <button @click="fetch('/api/hello').then(r => r.json()).then(d => result = d)">Call API</button>
1023
1125
  <div id="result" x-show="result">
@@ -1051,13 +1153,13 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
1051
1153
  <meta charset="UTF-8">
1052
1154
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1053
1155
  <title>\${title} - ${config.name}</title>
1054
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
1055
1156
  <style>
1056
1157
  ${css}
1057
1158
  </style>
1058
1159
  </head>
1059
1160
  <body>
1060
1161
  <main>\${content}</main>
1162
+ <script src="\${clientUrl}" type="module"></script>
1061
1163
  </body>
1062
1164
  </html>\`;
1063
1165
  }
@@ -1065,7 +1167,24 @@ ${css}
1065
1167
  self.addEventListener("fetch", (event) => {
1066
1168
  event.respondWith(router.handle(event.request));
1067
1169
  });
1170
+ `,
1171
+ [`client.${ext}`]: `import Alpine from "alpinejs";
1172
+ Alpine.start();
1173
+ `
1174
+ };
1175
+ if (t) {
1176
+ files[`env.d.ts`] = `declare module "alpinejs" {
1177
+ interface Alpine {
1178
+ start(): void;
1179
+ plugin(plugin: unknown): void;
1180
+ [key: string]: unknown;
1181
+ }
1182
+ const alpine: Alpine;
1183
+ export default alpine;
1184
+ }
1068
1185
  `;
1186
+ }
1187
+ return files;
1069
1188
  }
1070
1189
  function generateFullStackCrank(config) {
1071
1190
  const t = config.typescript;
@@ -1077,7 +1196,8 @@ import {Router} from "@b9g/router";
1077
1196
  import {logger} from "@b9g/router/middleware";
1078
1197
  import {assets} from "@b9g/assets/middleware";
1079
1198
  import {Page, Counter} from "./components";
1080
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1199
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1200
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1081
1201
 
1082
1202
  const router = new Router();
1083
1203
  router.use(logger());
@@ -1155,8 +1275,7 @@ export function Page({title, children, clientUrl}${t ? ": {title: string, childr
1155
1275
  export function *Counter(${t ? "this: Context" : ""}) {
1156
1276
  let count = 0;
1157
1277
  const handleClick = () => {
1158
- count++;
1159
- this.refresh();
1278
+ this.refresh(() => count++);
1160
1279
  };
1161
1280
  for ({} of this) {
1162
1281
  yield <button onclick={handleClick}>Clicked: {count}</button>;
@@ -1177,7 +1296,8 @@ import {Router} from "@b9g/router";
1177
1296
  import {logger} from "@b9g/router/middleware";
1178
1297
  import {assets} from "@b9g/assets/middleware";
1179
1298
  import {Page, Counter} from "./components";
1180
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1299
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1300
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1181
1301
 
1182
1302
  const router = new Router();
1183
1303
  router.use(logger());
@@ -1256,8 +1376,7 @@ export function Page({title, children, clientUrl}${t ? ": {title: string, childr
1256
1376
  export function *Counter(${t ? "this: Context" : ""}) {
1257
1377
  let count = 0;
1258
1378
  const handleClick = () => {
1259
- count++;
1260
- this.refresh();
1379
+ this.refresh(() => count++);
1261
1380
  };
1262
1381
  for ({} of this) {
1263
1382
  yield jsx\`<button onclick=\${handleClick}>Clicked: \${count}</button>\`;
@@ -1286,6 +1405,7 @@ function generateReadme(config) {
1286
1405
  };
1287
1406
  const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
1288
1407
  const isCrank = config.uiFramework === "crank";
1408
+ const hasClientBundle = config.template === "static-site" || config.template === "full-stack";
1289
1409
  let projectTree;
1290
1410
  if (isCrank) {
1291
1411
  projectTree = `${config.name}/
@@ -1295,6 +1415,14 @@ function generateReadme(config) {
1295
1415
  \u2502 \u2514\u2500\u2500 client.${ext} # Client-side hydration
1296
1416
  \u251C\u2500\u2500 eslint.config.js
1297
1417
  \u251C\u2500\u2500 package.json
1418
+ ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1419
+ } else if (hasClientBundle) {
1420
+ projectTree = `${config.name}/
1421
+ \u251C\u2500\u2500 src/
1422
+ \u2502 \u251C\u2500\u2500 server.${ext} # Application entry point
1423
+ \u2502 \u2514\u2500\u2500 client.${ext} # Client-side code
1424
+ \u251C\u2500\u2500 eslint.config.js
1425
+ \u251C\u2500\u2500 package.json
1298
1426
  ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1299
1427
  } else {
1300
1428
  projectTree = `${config.name}/
@@ -1320,7 +1448,7 @@ Open http://localhost:7777
1320
1448
 
1321
1449
  - \`npm run develop\` - Start development server
1322
1450
  - \`npm run build\` - Build for production
1323
- - \`npm start\` - Run production build${isCrank ? "\n- `npm run lint` - Lint source files" : ""}
1451
+ - \`npm start\` - Run production build${hasClientBundle ? "\n- `npm run lint` - Lint source files" : ""}
1324
1452
 
1325
1453
  ## Project Structure
1326
1454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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": {
@@ -19,7 +19,7 @@
19
19
  "@b9g/filesystem": "^0.2.0",
20
20
  "@b9g/http-errors": "^0.2.1",
21
21
  "@b9g/node-webworker": "^0.2.1",
22
- "@b9g/platform": "^0.1.17",
22
+ "@b9g/platform": "^0.1.18",
23
23
  "@b9g/platform-bun": "^0.1.16",
24
24
  "@b9g/platform-cloudflare": "^0.1.15",
25
25
  "@b9g/platform-node": "^0.1.17",