@b9g/shovel 0.2.13 → 0.2.14

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 +259 -143
  2. package/package.json +2 -2
package/bin/create.js CHANGED
@@ -265,31 +265,43 @@ 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
+ }
286
298
  }
287
299
  const scripts = {
288
300
  develop: `shovel develop ${entryFile} --platform ${config.platform}`,
289
301
  build: `shovel build ${entryFile} --platform ${config.platform}`,
290
302
  start: startCmd
291
303
  };
292
- if (isCrank) {
304
+ if (hasClientBundle) {
293
305
  scripts.lint = "eslint src/";
294
306
  }
295
307
  const packageJSON = {
@@ -322,34 +334,50 @@ async function createProject(config, projectPath) {
322
334
  esModuleInterop: true,
323
335
  strict: true,
324
336
  skipLibCheck: true,
337
+ noEmit: true,
338
+ allowImportingTsExtensions: true,
325
339
  forceConsistentCasingInFileNames: true,
326
- lib: ["ES2022", "WebWorker"]
340
+ lib: ["ES2022", "DOM", "DOM.Iterable", "WebWorker"]
327
341
  };
328
342
  if (config.uiFramework === "crank" && config.useJSX) {
329
343
  compilerOptions.jsx = "react-jsx";
330
344
  compilerOptions.jsxImportSource = "@b9g/crank";
331
345
  }
332
346
  const tsConfig = {
333
- compilerOptions: {
334
- ...compilerOptions,
335
- types: ["@b9g/platform/globals"]
336
- },
337
- include: ["src/**/*"],
338
- exclude: ["node_modules", "dist"]
347
+ compilerOptions,
348
+ include: [
349
+ "src/**/*",
350
+ "node_modules/@b9g/platform/src/globals.d.ts",
351
+ "dist/server/shovel.d.ts"
352
+ ],
353
+ exclude: []
339
354
  };
340
355
  await writeFile(
341
356
  join(projectPath, "tsconfig.json"),
342
357
  JSON.stringify(tsConfig, null, 2)
343
358
  );
344
359
  }
345
- if (isCrank) {
346
- const eslintConfig = `import js from "@eslint/js";
360
+ if (hasClientBundle) {
361
+ let eslintConfig;
362
+ if (config.typescript) {
363
+ eslintConfig = `import js from "@eslint/js";
364
+ import tseslint from "typescript-eslint";
365
+
366
+ export default tseslint.config(
367
+ js.configs.recommended,
368
+ tseslint.configs.recommended,
369
+ { ignores: ["dist/"] },
370
+ );
371
+ `;
372
+ } else {
373
+ eslintConfig = `import js from "@eslint/js";
347
374
 
348
375
  export default [
349
376
  js.configs.recommended,
350
377
  { ignores: ["dist/"] },
351
378
  ];
352
379
  `;
380
+ }
353
381
  await writeFile(join(projectPath, "eslint.config.js"), eslintConfig);
354
382
  }
355
383
  const readme = generateReadme(config);
@@ -477,44 +505,35 @@ function generateStaticSite(config) {
477
505
  function generateStaticSiteVanilla(config) {
478
506
  const ext = config.typescript ? "ts" : "js";
479
507
  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);
508
+ return {
509
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
510
+ import {assets} from "@b9g/assets/middleware";
511
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
512
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
489
513
 
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
- }
514
+ const router = new Router();
515
+ router.use(assets());
505
516
 
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
- }
517
+ router.route("/").get(() => {
518
+ return new Response(renderPage("Home", \`
519
+ <h1>Welcome to ${config.name}</h1>
520
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
521
+ <button id="counter">Clicked: 0</button>
522
+ <p><a href="/about">About</a></p>
523
+ \`), {
524
+ headers: { "Content-Type": "text/html" },
525
+ });
526
+ });
515
527
 
516
- return new Response("Not Found", { status: 404 });
517
- }
528
+ router.route("/about").get(() => {
529
+ return new Response(renderPage("About", \`
530
+ <h1>About</h1>
531
+ <p>This is a static site built with <strong>Shovel</strong>.</p>
532
+ <p><a href="/">Home</a></p>
533
+ \`), {
534
+ headers: { "Content-Type": "text/html" },
535
+ });
536
+ });
518
537
 
519
538
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
520
539
  return \`<!DOCTYPE html>
@@ -529,56 +548,64 @@ ${css}
529
548
  </head>
530
549
  <body>
531
550
  <main>\${content}</main>
551
+ <script src="\${clientUrl}" type="module"></script>
532
552
  </body>
533
553
  </html>\`;
534
554
  }
535
- `;
555
+
556
+ self.addEventListener("fetch", (event) => {
557
+ event.respondWith(router.handle(event.request));
558
+ });
559
+ `,
560
+ [`client.${ext}`]: `const btn = document.getElementById("counter");
561
+ if (btn) {
562
+ let count = 0;
563
+ btn.addEventListener("click", () => btn.textContent = "Clicked: " + ++count);
564
+ }
565
+ `
566
+ };
536
567
  }
537
568
  function generateStaticSiteHtmx(config) {
538
569
  const ext = config.typescript ? "ts" : "js";
539
570
  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);
571
+ const files = {
572
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
573
+ import {assets} from "@b9g/assets/middleware";
574
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
575
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
549
576
 
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
- }
577
+ const router = new Router();
578
+ router.use(assets());
561
579
 
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
- }
580
+ router.route("/").get(() => {
581
+ return new Response(renderPage("Home", \`
582
+ <h1>Welcome to ${config.name}</h1>
583
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
584
+ <button hx-get="/greeting" hx-target="#result" hx-swap="innerHTML">Get Greeting</button>
585
+ <div id="result"></div>
586
+ <p><a href="/about">About</a></p>
587
+ \`), {
588
+ headers: { "Content-Type": "text/html" },
589
+ });
590
+ });
569
591
 
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
- }
592
+ router.route("/greeting").get(() => {
593
+ const hour = new Date().getHours();
594
+ const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
595
+ return new Response(\`<p>\${greeting}! The time is \${new Date().toLocaleTimeString()}.</p>\`, {
596
+ headers: { "Content-Type": "text/html" },
597
+ });
598
+ });
579
599
 
580
- return new Response("Not Found", { status: 404 });
581
- }
600
+ router.route("/about").get(() => {
601
+ return new Response(renderPage("About", \`
602
+ <h1>About</h1>
603
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
604
+ <p><a href="/">Home</a></p>
605
+ \`), {
606
+ headers: { "Content-Type": "text/html" },
607
+ });
608
+ });
582
609
 
583
610
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
584
611
  return \`<!DOCTYPE html>
@@ -587,56 +614,64 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
587
614
  <meta charset="UTF-8">
588
615
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
589
616
  <title>\${title} - ${config.name}</title>
590
- <script src="https://unpkg.com/htmx.org@2.0.4"></script>
591
617
  <style>
592
618
  ${css}
593
619
  </style>
594
620
  </head>
595
621
  <body>
596
622
  <main>\${content}</main>
623
+ <script src="\${clientUrl}" type="module"></script>
597
624
  </body>
598
625
  </html>\`;
599
626
  }
627
+
628
+ self.addEventListener("fetch", (event) => {
629
+ event.respondWith(router.handle(event.request));
630
+ });
631
+ `,
632
+ [`client.${ext}`]: `import "htmx.org";
633
+ `
634
+ };
635
+ if (t) {
636
+ files[`env.d.ts`] = `declare module "htmx.org";
600
637
  `;
638
+ }
639
+ return files;
601
640
  }
602
641
  function generateStaticSiteAlpine(config) {
603
642
  const ext = config.typescript ? "ts" : "js";
604
643
  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);
644
+ const files = {
645
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
646
+ import {assets} from "@b9g/assets/middleware";
647
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
648
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
614
649
 
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
- }
650
+ const router = new Router();
651
+ router.use(assets());
627
652
 
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
- }
653
+ router.route("/").get(() => {
654
+ return new Response(renderPage("Home", \`
655
+ <h1>Welcome to ${config.name}</h1>
656
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
657
+ <div x-data="{ count: 0 }">
658
+ <button @click="count++">Clicked: <span x-text="count"></span></button>
659
+ </div>
660
+ <p><a href="/about">About</a></p>
661
+ \`), {
662
+ headers: { "Content-Type": "text/html" },
663
+ });
664
+ });
637
665
 
638
- return new Response("Not Found", { status: 404 });
639
- }
666
+ router.route("/about").get(() => {
667
+ return new Response(renderPage("About", \`
668
+ <h1>About</h1>
669
+ <p>This is a static site built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
670
+ <p><a href="/">Home</a></p>
671
+ \`), {
672
+ headers: { "Content-Type": "text/html" },
673
+ });
674
+ });
640
675
 
641
676
  function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
642
677
  return \`<!DOCTYPE html>
@@ -645,17 +680,38 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
645
680
  <meta charset="UTF-8">
646
681
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
647
682
  <title>\${title} - ${config.name}</title>
648
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
649
683
  <style>
650
684
  ${css}
651
685
  </style>
652
686
  </head>
653
687
  <body>
654
688
  <main>\${content}</main>
689
+ <script src="\${clientUrl}" type="module"></script>
655
690
  </body>
656
691
  </html>\`;
657
692
  }
693
+
694
+ self.addEventListener("fetch", (event) => {
695
+ event.respondWith(router.handle(event.request));
696
+ });
697
+ `,
698
+ [`client.${ext}`]: `import Alpine from "alpinejs";
699
+ Alpine.start();
700
+ `
701
+ };
702
+ if (t) {
703
+ files[`env.d.ts`] = `declare module "alpinejs" {
704
+ interface Alpine {
705
+ start(): void;
706
+ plugin(plugin: unknown): void;
707
+ [key: string]: unknown;
708
+ }
709
+ const alpine: Alpine;
710
+ export default alpine;
711
+ }
658
712
  `;
713
+ }
714
+ return files;
659
715
  }
660
716
  function generateStaticSiteCrank(config) {
661
717
  const t = config.typescript;
@@ -666,7 +722,8 @@ function generateStaticSiteCrank(config) {
666
722
  import {Router} from "@b9g/router";
667
723
  import {assets} from "@b9g/assets/middleware";
668
724
  import {Page, Counter} from "./components";
669
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
725
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
726
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
670
727
 
671
728
  const router = new Router();
672
729
  router.use(assets());
@@ -747,7 +804,8 @@ import {renderer} from "@b9g/crank/html";
747
804
  import {Router} from "@b9g/router";
748
805
  import {assets} from "@b9g/assets/middleware";
749
806
  import {Page, Counter} from "./components";
750
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
807
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
808
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
751
809
 
752
810
  const router = new Router();
753
811
  router.use(assets());
@@ -838,11 +896,16 @@ function generateFullStack(config) {
838
896
  function generateFullStackVanilla(config) {
839
897
  const ext = config.typescript ? "ts" : "js";
840
898
  const t = config.typescript;
841
- return `import {Router} from "@b9g/router";
899
+ return {
900
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
842
901
  import {logger} from "@b9g/router/middleware";
902
+ import {assets} from "@b9g/assets/middleware";
903
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
904
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
843
905
 
844
906
  const router = new Router();
845
907
  router.use(logger());
908
+ router.use(assets());
846
909
 
847
910
  // API routes
848
911
  router.route("/api/hello").get(() => {
@@ -861,17 +924,9 @@ router.route("/api/echo").post(async (req) => {
861
924
  router.route("/").get(() => {
862
925
  return new Response(renderPage("Home", \`
863
926
  <h1>Welcome to ${config.name}</h1>
864
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
927
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
865
928
  <button id="call-api">Call API</button>
866
929
  <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
930
  <ul>
876
931
  <li><a href="/about">About</a></li>
877
932
  <li><a href="/api/hello">API: /api/hello</a></li>
@@ -904,6 +959,7 @@ ${css}
904
959
  </head>
905
960
  <body>
906
961
  <main>\${content}</main>
962
+ <script src="\${clientUrl}" type="module"></script>
907
963
  </body>
908
964
  </html>\`;
909
965
  }
@@ -911,16 +967,35 @@ ${css}
911
967
  self.addEventListener("fetch", (event) => {
912
968
  event.respondWith(router.handle(event.request));
913
969
  });
914
- `;
970
+ `,
971
+ [`client.${ext}`]: `const callBtn = document.getElementById("call-api");
972
+ if (callBtn) {
973
+ callBtn.addEventListener("click", async () => {
974
+ const res = await fetch("/api/hello");
975
+ const data = await res.json();
976
+ const result = document.getElementById("result");
977
+ if (result) {
978
+ result.innerHTML =
979
+ \`<p>\${data.message}</p><p><small>\${data.timestamp}</small></p>\`;
980
+ }
981
+ });
982
+ }
983
+ `
984
+ };
915
985
  }
916
986
  function generateFullStackHtmx(config) {
917
987
  const ext = config.typescript ? "ts" : "js";
918
988
  const t = config.typescript;
919
- return `import {Router} from "@b9g/router";
989
+ const files = {
990
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
920
991
  import {logger} from "@b9g/router/middleware";
992
+ import {assets} from "@b9g/assets/middleware";
993
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
994
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
921
995
 
922
996
  const router = new Router();
923
997
  router.use(logger());
998
+ router.use(assets());
924
999
 
925
1000
  // API routes \u2014 return HTML fragments when HTMX requests, JSON otherwise
926
1001
  router.route("/api/hello").get((req) => {
@@ -946,7 +1021,7 @@ router.route("/api/echo").post(async (req) => {
946
1021
  router.route("/").get(() => {
947
1022
  return new Response(renderPage("Home", \`
948
1023
  <h1>Welcome to ${config.name}</h1>
949
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
1024
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
950
1025
  <button hx-get="/api/hello" hx-target="#result" hx-swap="innerHTML">Call API</button>
951
1026
  <div id="result"></div>
952
1027
  <ul>
@@ -975,13 +1050,13 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
975
1050
  <meta charset="UTF-8">
976
1051
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
977
1052
  <title>\${title} - ${config.name}</title>
978
- <script src="https://unpkg.com/htmx.org@2.0.4"></script>
979
1053
  <style>
980
1054
  ${css}
981
1055
  </style>
982
1056
  </head>
983
1057
  <body>
984
1058
  <main>\${content}</main>
1059
+ <script src="\${clientUrl}" type="module"></script>
985
1060
  </body>
986
1061
  </html>\`;
987
1062
  }
@@ -989,16 +1064,29 @@ ${css}
989
1064
  self.addEventListener("fetch", (event) => {
990
1065
  event.respondWith(router.handle(event.request));
991
1066
  });
1067
+ `,
1068
+ [`client.${ext}`]: `import "htmx.org";
1069
+ `
1070
+ };
1071
+ if (t) {
1072
+ files[`env.d.ts`] = `declare module "htmx.org";
992
1073
  `;
1074
+ }
1075
+ return files;
993
1076
  }
994
1077
  function generateFullStackAlpine(config) {
995
1078
  const ext = config.typescript ? "ts" : "js";
996
1079
  const t = config.typescript;
997
- return `import {Router} from "@b9g/router";
1080
+ const files = {
1081
+ [`server.${ext}`]: `import {Router} from "@b9g/router";
998
1082
  import {logger} from "@b9g/router/middleware";
1083
+ import {assets} from "@b9g/assets/middleware";
1084
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1085
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
999
1086
 
1000
1087
  const router = new Router();
1001
1088
  router.use(logger());
1089
+ router.use(assets());
1002
1090
 
1003
1091
  // API routes
1004
1092
  router.route("/api/hello").get(() => {
@@ -1017,7 +1105,7 @@ router.route("/api/echo").post(async (req) => {
1017
1105
  router.route("/").get(() => {
1018
1106
  return new Response(renderPage("Home", \`
1019
1107
  <h1>Welcome to ${config.name}</h1>
1020
- <p>Edit <code>src/app.${ext}</code> to get started.</p>
1108
+ <p>Edit <code>src/server.${ext}</code> to get started.</p>
1021
1109
  <div x-data="{ result: null }">
1022
1110
  <button @click="fetch('/api/hello').then(r => r.json()).then(d => result = d)">Call API</button>
1023
1111
  <div id="result" x-show="result">
@@ -1051,13 +1139,13 @@ function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})$
1051
1139
  <meta charset="UTF-8">
1052
1140
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1053
1141
  <title>\${title} - ${config.name}</title>
1054
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
1055
1142
  <style>
1056
1143
  ${css}
1057
1144
  </style>
1058
1145
  </head>
1059
1146
  <body>
1060
1147
  <main>\${content}</main>
1148
+ <script src="\${clientUrl}" type="module"></script>
1061
1149
  </body>
1062
1150
  </html>\`;
1063
1151
  }
@@ -1065,7 +1153,24 @@ ${css}
1065
1153
  self.addEventListener("fetch", (event) => {
1066
1154
  event.respondWith(router.handle(event.request));
1067
1155
  });
1156
+ `,
1157
+ [`client.${ext}`]: `import Alpine from "alpinejs";
1158
+ Alpine.start();
1159
+ `
1160
+ };
1161
+ if (t) {
1162
+ files[`env.d.ts`] = `declare module "alpinejs" {
1163
+ interface Alpine {
1164
+ start(): void;
1165
+ plugin(plugin: unknown): void;
1166
+ [key: string]: unknown;
1167
+ }
1168
+ const alpine: Alpine;
1169
+ export default alpine;
1170
+ }
1068
1171
  `;
1172
+ }
1173
+ return files;
1069
1174
  }
1070
1175
  function generateFullStackCrank(config) {
1071
1176
  const t = config.typescript;
@@ -1077,7 +1182,8 @@ import {Router} from "@b9g/router";
1077
1182
  import {logger} from "@b9g/router/middleware";
1078
1183
  import {assets} from "@b9g/assets/middleware";
1079
1184
  import {Page, Counter} from "./components";
1080
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1185
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1186
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1081
1187
 
1082
1188
  const router = new Router();
1083
1189
  router.use(logger());
@@ -1177,7 +1283,8 @@ import {Router} from "@b9g/router";
1177
1283
  import {logger} from "@b9g/router/middleware";
1178
1284
  import {assets} from "@b9g/assets/middleware";
1179
1285
  import {Page, Counter} from "./components";
1180
- import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1286
+ ${t ? `// @ts-expect-error \u2014 asset URL resolved by Shovel build system
1287
+ ` : ""}import clientUrl from "./client.${ext}" with {assetBase: "/assets/"};
1181
1288
 
1182
1289
  const router = new Router();
1183
1290
  router.use(logger());
@@ -1286,6 +1393,7 @@ function generateReadme(config) {
1286
1393
  };
1287
1394
  const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
1288
1395
  const isCrank = config.uiFramework === "crank";
1396
+ const hasClientBundle = config.template === "static-site" || config.template === "full-stack";
1289
1397
  let projectTree;
1290
1398
  if (isCrank) {
1291
1399
  projectTree = `${config.name}/
@@ -1295,6 +1403,14 @@ function generateReadme(config) {
1295
1403
  \u2502 \u2514\u2500\u2500 client.${ext} # Client-side hydration
1296
1404
  \u251C\u2500\u2500 eslint.config.js
1297
1405
  \u251C\u2500\u2500 package.json
1406
+ ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1407
+ } else if (hasClientBundle) {
1408
+ projectTree = `${config.name}/
1409
+ \u251C\u2500\u2500 src/
1410
+ \u2502 \u251C\u2500\u2500 server.${ext} # Application entry point
1411
+ \u2502 \u2514\u2500\u2500 client.${ext} # Client-side code
1412
+ \u251C\u2500\u2500 eslint.config.js
1413
+ \u251C\u2500\u2500 package.json
1298
1414
  ${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
1299
1415
  } else {
1300
1416
  projectTree = `${config.name}/
@@ -1320,7 +1436,7 @@ Open http://localhost:7777
1320
1436
 
1321
1437
  - \`npm run develop\` - Start development server
1322
1438
  - \`npm run build\` - Build for production
1323
- - \`npm start\` - Run production build${isCrank ? "\n- `npm run lint` - Lint source files" : ""}
1439
+ - \`npm start\` - Run production build${hasClientBundle ? "\n- `npm run lint` - Lint source files" : ""}
1324
1440
 
1325
1441
  ## Project Structure
1326
1442
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
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",