@b9g/shovel 0.2.6 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to Shovel will be documented in this file.
4
4
 
5
+ ## [0.2.7] - 2026-02-06
6
+
7
+ ### Features
8
+
9
+ - **Request logger middleware** - New `logger()` middleware in `@b9g/router/middleware` logs requests and responses with timing via LogTape (default category: `["app", "router"]`)
10
+ - **CLI flags for create-shovel** - `--template`, `--typescript`/`--no-typescript`, `--platform` flags to bypass interactive prompts. `--template crank` is shorthand for static-site + Crank.js.
11
+ - **Logger in generated templates** - All Router-based templates (api, full-stack) now include `router.use(logger())` out of the box
12
+
13
+ ### Dependencies
14
+
15
+ - **`@b9g/router`** `0.2.2` - Added `@logtape/logtape` as explicit dependency (was previously resolved via workspace only)
16
+
5
17
  ## [0.2.6] - 2026-02-06
6
18
 
7
19
  ### Features
package/bin/cli.js CHANGED
@@ -75,7 +75,7 @@ program.command("develop <entrypoint>").description("Start development server wi
75
75
  DEFAULTS.WORKERS
76
76
  ).option("--platform <name>", "Runtime platform (node, cloudflare, bun)").action(async (entrypoint, options) => {
77
77
  checkPlatformReexec(options);
78
- const { developCommand } = await import("../src/_chunks/develop-JUQG2G7M.js");
78
+ const { developCommand } = await import("../src/_chunks/develop-SVLFKAF5.js");
79
79
  await developCommand(entrypoint, options, config);
80
80
  });
81
81
  program.command("create [name]").description("Create a new Shovel project").action(async (name) => {
@@ -91,7 +91,7 @@ program.command("build <entrypoint>").description("Build app for production").op
91
91
  "Run ServiceWorker lifecycle after build (install or activate, default: activate)"
92
92
  ).action(async (entrypoint, options) => {
93
93
  checkPlatformReexec(options);
94
- const { buildCommand } = await import("../src/_chunks/build-KBQU2OA7.js");
94
+ const { buildCommand } = await import("../src/_chunks/build-O5LLUOND.js");
95
95
  await buildCommand(entrypoint, options, config);
96
96
  process.exit(0);
97
97
  });
package/bin/create.js CHANGED
@@ -20,10 +20,26 @@ function validateProjectName(name) {
20
20
  return "Use lowercase letters, numbers, and hyphens only";
21
21
  return void 0;
22
22
  }
23
+ function parseFlags(args) {
24
+ const flags = {};
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg === "--template" && args[i + 1])
28
+ flags.template = args[++i];
29
+ else if (arg === "--typescript")
30
+ flags.typescript = true;
31
+ else if (arg === "--no-typescript")
32
+ flags.typescript = false;
33
+ else if (arg === "--platform" && args[i + 1])
34
+ flags.platform = args[++i];
35
+ }
36
+ return flags;
37
+ }
23
38
  async function main() {
24
39
  console.info("");
25
40
  intro("Create Shovel App");
26
- let projectName = process.argv[2];
41
+ const flags = parseFlags(process.argv.slice(2));
42
+ let projectName = process.argv[2]?.startsWith("-") ? void 0 : process.argv[2];
27
43
  if (projectName) {
28
44
  const validationError = validateProjectName(projectName);
29
45
  if (validationError) {
@@ -52,37 +68,53 @@ async function main() {
52
68
  process.exit(0);
53
69
  }
54
70
  }
55
- const template = await select({
56
- message: "Choose a starter template:",
57
- options: [
58
- {
59
- value: "hello-world",
60
- label: "Hello World",
61
- hint: "Minimal fetch handler to get started"
62
- },
63
- {
64
- value: "api",
65
- label: "API",
66
- hint: "REST endpoints with JSON responses"
67
- },
68
- {
69
- value: "static-site",
70
- label: "Static Site",
71
- hint: "Server-rendered HTML pages"
72
- },
73
- {
74
- value: "full-stack",
75
- label: "Full Stack",
76
- hint: "HTML pages + API routes"
77
- }
78
- ]
79
- });
80
- if (typeof template === "symbol") {
81
- outro("Project creation cancelled");
82
- process.exit(0);
83
- }
71
+ let template;
84
72
  let uiFramework = "vanilla";
85
- if (template === "static-site" || template === "full-stack") {
73
+ if (flags.template === "crank") {
74
+ template = "static-site";
75
+ uiFramework = "crank";
76
+ } else if (flags.template) {
77
+ const valid = ["hello-world", "api", "static-site", "full-stack"];
78
+ if (!valid.includes(flags.template)) {
79
+ console.error(
80
+ `Error: Unknown template "${flags.template}". Valid options: ${valid.join(", ")}, crank`
81
+ );
82
+ process.exit(1);
83
+ }
84
+ template = flags.template;
85
+ } else {
86
+ const templateResult = await select({
87
+ message: "Choose a starter template:",
88
+ options: [
89
+ {
90
+ value: "hello-world",
91
+ label: "Hello World",
92
+ hint: "Minimal fetch handler to get started"
93
+ },
94
+ {
95
+ value: "api",
96
+ label: "API",
97
+ hint: "REST endpoints with JSON responses"
98
+ },
99
+ {
100
+ value: "static-site",
101
+ label: "Static Site",
102
+ hint: "Server-rendered HTML pages"
103
+ },
104
+ {
105
+ value: "full-stack",
106
+ label: "Full Stack",
107
+ hint: "HTML pages + API routes"
108
+ }
109
+ ]
110
+ });
111
+ if (typeof templateResult === "symbol") {
112
+ outro("Project creation cancelled");
113
+ process.exit(0);
114
+ }
115
+ template = templateResult;
116
+ }
117
+ if (uiFramework === "vanilla" && (template === "static-site" || template === "full-stack")) {
86
118
  const framework = await select({
87
119
  message: "UI framework:",
88
120
  initialValue: "crank",
@@ -115,39 +147,58 @@ async function main() {
115
147
  }
116
148
  uiFramework = framework;
117
149
  }
118
- const typescript = await confirm({
119
- message: "Use TypeScript?",
120
- initialValue: true
121
- });
122
- if (typeof typescript === "symbol") {
123
- outro("Project creation cancelled");
124
- process.exit(0);
150
+ let typescript;
151
+ if (flags.typescript !== void 0) {
152
+ typescript = flags.typescript;
153
+ } else {
154
+ const tsResult = await confirm({
155
+ message: "Use TypeScript?",
156
+ initialValue: true
157
+ });
158
+ if (typeof tsResult === "symbol") {
159
+ outro("Project creation cancelled");
160
+ process.exit(0);
161
+ }
162
+ typescript = tsResult;
125
163
  }
126
- const detectedPlatform = detectPlatform();
127
- const platform = await select({
128
- message: "Which platform?",
129
- initialValue: detectedPlatform,
130
- options: [
131
- {
132
- value: "node",
133
- label: "Node.js",
134
- hint: detectedPlatform === "node" ? "detected" : void 0
135
- },
136
- {
137
- value: "bun",
138
- label: "Bun",
139
- hint: detectedPlatform === "bun" ? "detected" : void 0
140
- },
141
- {
142
- value: "cloudflare",
143
- label: "Cloudflare Workers",
144
- hint: "Edge runtime"
145
- }
146
- ]
147
- });
148
- if (typeof platform === "symbol") {
149
- outro("Project creation cancelled");
150
- process.exit(0);
164
+ let platform;
165
+ if (flags.platform) {
166
+ const valid = ["node", "bun", "cloudflare"];
167
+ if (!valid.includes(flags.platform)) {
168
+ console.error(
169
+ `Error: Unknown platform "${flags.platform}". Valid options: ${valid.join(", ")}`
170
+ );
171
+ process.exit(1);
172
+ }
173
+ platform = flags.platform;
174
+ } else {
175
+ const detectedPlatform = detectPlatform();
176
+ const platformResult = await select({
177
+ message: "Which platform?",
178
+ initialValue: detectedPlatform,
179
+ options: [
180
+ {
181
+ value: "node",
182
+ label: "Node.js",
183
+ hint: detectedPlatform === "node" ? "detected" : void 0
184
+ },
185
+ {
186
+ value: "bun",
187
+ label: "Bun",
188
+ hint: detectedPlatform === "bun" ? "detected" : void 0
189
+ },
190
+ {
191
+ value: "cloudflare",
192
+ label: "Cloudflare Workers",
193
+ hint: "Edge runtime"
194
+ }
195
+ ]
196
+ });
197
+ if (typeof platformResult === "symbol") {
198
+ outro("Project creation cancelled");
199
+ process.exit(0);
200
+ }
201
+ platform = platformResult;
151
202
  }
152
203
  const config = {
153
204
  name: projectName,
@@ -287,8 +338,10 @@ self.addEventListener("fetch", (event) => {
287
338
  }
288
339
  function generateApi(config) {
289
340
  return `import { Router } from "@b9g/router";
341
+ import { logger } from "@b9g/router/middleware";
290
342
 
291
343
  const router = new Router();
344
+ router.use(logger());
292
345
 
293
346
  // In-memory data store
294
347
  const users = [
@@ -622,8 +675,10 @@ function generateFullStackVanilla(config) {
622
675
  const ext = config.typescript ? "ts" : "js";
623
676
  const t = config.typescript;
624
677
  return `import { Router } from "@b9g/router";
678
+ import { logger } from "@b9g/router/middleware";
625
679
 
626
680
  const router = new Router();
681
+ router.use(logger());
627
682
 
628
683
  // API routes
629
684
  router.route("/api/hello").get(() => {
@@ -688,8 +743,10 @@ function generateFullStackHtmx(config) {
688
743
  const ext = config.typescript ? "ts" : "js";
689
744
  const t = config.typescript;
690
745
  return `import { Router } from "@b9g/router";
746
+ import { logger } from "@b9g/router/middleware";
691
747
 
692
748
  const router = new Router();
749
+ router.use(logger());
693
750
 
694
751
  // API routes \u2014 return HTML fragments when HTMX requests, JSON otherwise
695
752
  router.route("/api/hello").get((req) => {
@@ -764,8 +821,10 @@ function generateFullStackAlpine(config) {
764
821
  const ext = config.typescript ? "ts" : "js";
765
822
  const t = config.typescript;
766
823
  return `import { Router } from "@b9g/router";
824
+ import { logger } from "@b9g/router/middleware";
767
825
 
768
826
  const router = new Router();
827
+ router.use(logger());
769
828
 
770
829
  // API routes
771
830
  router.route("/api/hello").get(() => {
@@ -837,9 +896,11 @@ self.addEventListener("fetch", (event) => {
837
896
  function generateFullStackCrank(config) {
838
897
  const t = config.typescript;
839
898
  return `import { Router } from "@b9g/router";
899
+ import { logger } from "@b9g/router/middleware";
840
900
  import {renderer} from "@b9g/crank/html";
841
901
 
842
902
  const router = new Router();
903
+ router.use(logger());
843
904
 
844
905
  const css = \`
845
906
  ${css}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": {
7
7
  "type": "git",
8
- "url": "https://github.com/bikeshaving/shovel.git"
8
+ "url": "git+https://github.com/bikeshaving/shovel.git"
9
9
  },
10
10
  "bin": {
11
11
  "shovel": "bin/cli.js",
@@ -16,11 +16,11 @@
16
16
  "dependencies": {
17
17
  "@b9g/async-context": "^0.2.1",
18
18
  "@b9g/cache": "^0.2.2",
19
- "@b9g/filesystem": "^0.1.10",
19
+ "@b9g/filesystem": "^0.1.11",
20
20
  "@b9g/http-errors": "^0.2.1",
21
21
  "@b9g/node-webworker": "^0.2.1",
22
22
  "@b9g/platform": "^0.1.17",
23
- "@b9g/platform-bun": "^0.1.15",
23
+ "@b9g/platform-bun": "^0.1.16",
24
24
  "@b9g/platform-cloudflare": "^0.1.15",
25
25
  "@b9g/platform-node": "^0.1.17",
26
26
  "@clack/prompts": "^0.7.0",
@@ -36,7 +36,7 @@
36
36
  "@b9g/assets": "^0.2.1",
37
37
  "@b9g/crank": "^0.7.2",
38
38
  "@b9g/libuild": "^0.1.22",
39
- "@b9g/router": "^0.2.1",
39
+ "@b9g/router": "^0.2.2",
40
40
  "@logtape/file": "^1.0.0",
41
41
  "@types/bun": "^1.3.4",
42
42
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ServerBundler,
3
3
  loadPlatformModule
4
- } from "./chunk-ABGHNBNM.js";
4
+ } from "./chunk-DQDUKJQ4.js";
5
5
  import {
6
6
  findProjectRoot,
7
7
  findWorkspaceRoot
@@ -448,10 +448,15 @@ function createConfigPlugin(projectRoot, outDir = "dist", options = {}) {
448
448
  const typesPath = join2(serverOutDir, "shovel.d.ts");
449
449
  writeFileSync2(typesPath, typesCode);
450
450
  }
451
+ const watchFiles = [
452
+ join2(projectRoot, "shovel.json"),
453
+ join2(projectRoot, "package.json")
454
+ ];
451
455
  return {
452
456
  contents: configModuleCode,
453
457
  loader: "js",
454
- resolveDir: projectRoot
458
+ resolveDir: projectRoot,
459
+ watchFiles
455
460
  };
456
461
  });
457
462
  }
@@ -720,7 +725,6 @@ var ServerBundler = class {
720
725
  #initialBuildComplete;
721
726
  #initialBuildResolve;
722
727
  #currentOutputs;
723
- #configWatchers;
724
728
  #dirWatchers;
725
729
  #userEntryPath;
726
730
  #watchOptions;
@@ -732,7 +736,6 @@ var ServerBundler = class {
732
736
  this.#projectRoot = findProjectRoot();
733
737
  this.#initialBuildComplete = false;
734
738
  this.#currentOutputs = { worker: "" };
735
- this.#configWatchers = [];
736
739
  this.#dirWatchers = /* @__PURE__ */ new Map();
737
740
  this.#userEntryPath = "";
738
741
  this.#changedFiles = /* @__PURE__ */ new Set();
@@ -798,9 +801,18 @@ var ServerBundler = class {
798
801
  this.#ctx = await ESBuild2.context(buildOptions);
799
802
  logger3.debug("Starting esbuild watch mode");
800
803
  await this.#ctx.watch();
801
- this.#watchConfigFiles();
802
804
  return initialBuildPromise;
803
805
  }
806
+ /**
807
+ * Trigger an immediate rebuild.
808
+ * Only works in watch mode (after calling watch()).
809
+ */
810
+ async rebuild() {
811
+ if (!this.#ctx) {
812
+ throw new Error("Cannot rebuild: bundler is not in watch mode");
813
+ }
814
+ await this.#ctx.rebuild();
815
+ }
804
816
  /**
805
817
  * Stop watching and dispose of resources.
806
818
  */
@@ -809,10 +821,6 @@ var ServerBundler = class {
809
821
  clearTimeout(this.#rebuildTimeout);
810
822
  this.#rebuildTimeout = void 0;
811
823
  }
812
- for (const watcher of this.#configWatchers) {
813
- watcher.close();
814
- }
815
- this.#configWatchers = [];
816
824
  for (const entry of this.#dirWatchers.values()) {
817
825
  entry.watcher.close();
818
826
  }
@@ -1123,30 +1131,6 @@ These modules are not bundled and won't be available at runtime.`
1123
1131
  }
1124
1132
  }
1125
1133
  }
1126
- /**
1127
- * Watch config files for changes.
1128
- */
1129
- #watchConfigFiles() {
1130
- const configFiles = ["shovel.json", "package.json"];
1131
- for (const filename of configFiles) {
1132
- const filepath = join5(this.#projectRoot, filename);
1133
- if (!existsSync4(filepath))
1134
- continue;
1135
- try {
1136
- const watcher = watch(filepath, { persistent: false }, (event) => {
1137
- if (event === "change") {
1138
- this.#scheduleRebuild(filepath);
1139
- }
1140
- });
1141
- this.#configWatchers.push(watcher);
1142
- } catch (err) {
1143
- logger3.warn("Failed to watch {file}: {error}", {
1144
- file: filename,
1145
- error: err
1146
- });
1147
- }
1148
- }
1149
- }
1150
1134
  /**
1151
1135
  * Update source file watchers from metafile.
1152
1136
  */
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ServerBundler,
3
3
  loadPlatformModule
4
- } from "./chunk-ABGHNBNM.js";
4
+ } from "./chunk-DQDUKJQ4.js";
5
5
  import {
6
6
  DEFAULTS
7
7
  } from "./chunk-7GONPLNW.js";
@@ -56,6 +56,7 @@ async function developCommand(entrypoint, options, config) {
56
56
  const platformModule = await loadPlatformModule(platformName);
57
57
  const platformESBuildConfig = platformModule.getESBuildConfig();
58
58
  let devServer = null;
59
+ const SHORTCUTS_HELP = "Shortcuts: Ctrl+R (reload) \xB7 Ctrl+L (clear) \xB7 Ctrl+C (quit) \xB7 ? (help)";
59
60
  const startOrReloadServer = async (workerPath) => {
60
61
  if (!devServer) {
61
62
  devServer = await platformModule.createDevServer({
@@ -73,6 +74,9 @@ async function developCommand(entrypoint, options, config) {
73
74
  } else {
74
75
  logger.info("Server running at {url}", { url: urls.local });
75
76
  }
77
+ if (process.stdin.isTTY) {
78
+ logger.info(SHORTCUTS_HELP);
79
+ }
76
80
  } else {
77
81
  await devServer.reload(workerPath);
78
82
  }
@@ -109,6 +113,28 @@ async function developCommand(entrypoint, options, config) {
109
113
  };
110
114
  process.on("SIGINT", () => shutdown("SIGINT"));
111
115
  process.on("SIGTERM", () => shutdown("SIGTERM"));
116
+ if (process.stdin.isTTY) {
117
+ process.stdin.setRawMode(true);
118
+ process.stdin.resume();
119
+ process.stdin.setEncoding("utf8");
120
+ process.stdin.on("data", async (key) => {
121
+ switch (key) {
122
+ case "":
123
+ logger.info("Manual reload...");
124
+ await bundler.rebuild();
125
+ break;
126
+ case "\f":
127
+ console.clear();
128
+ break;
129
+ case "":
130
+ await shutdown("SIGINT");
131
+ break;
132
+ case "?":
133
+ logger.info(SHORTCUTS_HELP);
134
+ break;
135
+ }
136
+ });
137
+ }
112
138
  await new Promise(() => {
113
139
  });
114
140
  } catch (error) {