@b9g/shovel 0.2.0-beta.2 → 0.2.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/create.js CHANGED
@@ -8,7 +8,7 @@ import picocolors from "picocolors";
8
8
  import { mkdir, writeFile } from "fs/promises";
9
9
  import { join, resolve } from "path";
10
10
  import { existsSync } from "fs";
11
- var { cyan, green, yellow: _yellow, red, dim, bold } = picocolors;
11
+ var { cyan, green, red, dim, bold } = picocolors;
12
12
  async function main() {
13
13
  console.info("");
14
14
  intro(cyan("\u{1F680} Create Shovel App"));
@@ -113,7 +113,7 @@ async function main() {
113
113
  console.info(` ${dim("$")} npm run develop`);
114
114
  console.info("");
115
115
  console.info(
116
- `\u{1F310} Your app will be available at: ${bold("http://localhost:8000")}`
116
+ `\u{1F310} Your app will be available at: ${bold("http://localhost:3000")}`
117
117
  );
118
118
  console.info("");
119
119
  console.info(dim("Happy coding with Shovel! \u{1F680}"));
@@ -200,28 +200,49 @@ function getRequestInfo(request: Request) {
200
200
 
201
201
  async function parseBody(request: Request) {
202
202
  const contentType = request.headers.get('content-type') || '';
203
-
203
+
204
204
  if (contentType.includes('application/json')) {
205
205
  try {
206
206
  return await request.json();
207
- } catch {
207
+ } catch (err) {
208
+ // Only ignore JSON parse errors, rethrow others
209
+ if (
210
+ !(err instanceof SyntaxError) ||
211
+ !/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
212
+ ) {
213
+ throw err;
214
+ }
208
215
  return null;
209
216
  }
210
217
  }
211
-
218
+
212
219
  if (contentType.includes('application/x-www-form-urlencoded')) {
213
220
  try {
214
221
  const formData = await request.formData();
215
222
  return Object.fromEntries(formData.entries());
216
- } catch {
223
+ } catch (err) {
224
+ // Only ignore form data parse errors, rethrow others
225
+ if (
226
+ !(err instanceof TypeError) ||
227
+ !String(err.message).includes('FormData')
228
+ ) {
229
+ throw err;
230
+ }
217
231
  return null;
218
232
  }
219
233
  }
220
-
234
+
221
235
  try {
222
236
  const text = await request.text();
223
237
  return text || null;
224
- } catch {
238
+ } catch (err) {
239
+ // Only ignore body already consumed errors, rethrow others
240
+ if (
241
+ !(err instanceof TypeError) ||
242
+ !String(err.message).includes('body')
243
+ ) {
244
+ throw err;
245
+ }
225
246
  return null;
226
247
  }
227
248
  }
@@ -237,28 +258,49 @@ function getRequestInfo(request) {
237
258
 
238
259
  async function parseBody(request) {
239
260
  const contentType = request.headers.get('content-type') || '';
240
-
261
+
241
262
  if (contentType.includes('application/json')) {
242
263
  try {
243
264
  return await request.json();
244
- } catch {
265
+ } catch (err) {
266
+ // Only ignore JSON parse errors, rethrow others
267
+ if (
268
+ !(err instanceof SyntaxError) ||
269
+ !/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
270
+ ) {
271
+ throw err;
272
+ }
245
273
  return null;
246
274
  }
247
275
  }
248
-
276
+
249
277
  if (contentType.includes('application/x-www-form-urlencoded')) {
250
278
  try {
251
279
  const formData = await request.formData();
252
280
  return Object.fromEntries(formData.entries());
253
- } catch {
281
+ } catch (err) {
282
+ // Only ignore form data parse errors, rethrow others
283
+ if (
284
+ !(err instanceof TypeError) ||
285
+ !String(err.message).includes('FormData')
286
+ ) {
287
+ throw err;
288
+ }
254
289
  return null;
255
290
  }
256
291
  }
257
-
292
+
258
293
  try {
259
294
  const text = await request.text();
260
295
  return text || null;
261
- } catch {
296
+ } catch (err) {
297
+ // Only ignore body already consumed errors, rethrow others
298
+ if (
299
+ !(err instanceof TypeError) ||
300
+ !String(err.message).includes('body')
301
+ ) {
302
+ throw err;
303
+ }
262
304
  return null;
263
305
  }
264
306
  }
@@ -281,7 +323,7 @@ self.addEventListener("activate", (event) => {
281
323
  // Handle HTTP requests
282
324
  self.addEventListener("fetch", (event) => {
283
325
  try {
284
- const responsePromise = router.handler(event.request);
326
+ const responsePromise = router.handle(event.request);
285
327
  event.respondWith(responsePromise);
286
328
  } catch (error) {
287
329
  console.error("[${config.name}] Error handling request:", error);
@@ -323,7 +365,7 @@ router.route("/").get(async (request, context) => {
323
365
  <body>
324
366
  <h1>\u{1F680} Welcome to Shovel!</h1>
325
367
  <p>Your ${config.template} app is running on the <strong>${config.platform}</strong> platform.</p>
326
-
368
+
327
369
  <div class="info">
328
370
  <strong>Try these endpoints:</strong>
329
371
  <ul>
@@ -331,12 +373,12 @@ router.route("/").get(async (request, context) => {
331
373
  <li><a href="/api/time">GET /api/time</a> - Current timestamp</li>
332
374
  </ul>
333
375
  </div>
334
-
376
+
335
377
  <p>Edit <code>src/app.${config.typescript ? "ts" : "js"}</code> to customize your app!</p>
336
378
  </body>
337
379
  </html>
338
380
  \`;
339
-
381
+
340
382
  return new Response(html, {
341
383
  headers: { "Content-Type": "text/html" }
342
384
  });
@@ -366,7 +408,7 @@ router.route("/api/time").get(async (request, context) => {
366
408
  router.route("/").get(async (request, context) => {
367
409
  return new Response(JSON.stringify({
368
410
  name: "${config.name}",
369
- platform: "${config.platform}",
411
+ platform: "${config.platform}",
370
412
  endpoints: [
371
413
  { method: "GET", path: "/api/users", description: "Get all users" },
372
414
  { method: "POST", path: "/api/users", description: "Create a user" },
@@ -388,12 +430,12 @@ const users = [
388
430
  router.route("/api/users").get(async (request, context) => {
389
431
  const url = new URL(request.url);
390
432
  const active = url.searchParams.get('active');
391
-
433
+
392
434
  let filteredUsers = users;
393
435
  if (active !== null) {
394
436
  filteredUsers = users.filter(user => user.active === (active === 'true'));
395
437
  }
396
-
438
+
397
439
  return new Response(JSON.stringify({
398
440
  users: filteredUsers,
399
441
  total: filteredUsers.length
@@ -410,9 +452,9 @@ router.route("/api/users").post(async (request, context) => {
410
452
  email: userData.email || \`user\${Date.now()}@example.com\`,
411
453
  active: userData.active !== false
412
454
  };
413
-
455
+
414
456
  users.push(newUser);
415
-
457
+
416
458
  return new Response(JSON.stringify({
417
459
  success: true,
418
460
  user: newUser
@@ -425,7 +467,7 @@ router.route("/api/users").post(async (request, context) => {
425
467
  router.route("/api/users/:id").get(async (request, context) => {
426
468
  const id = parseInt(context.params.id);
427
469
  const user = users.find(u => u.id === id);
428
-
470
+
429
471
  if (!user) {
430
472
  return new Response(JSON.stringify({
431
473
  error: "User not found"
@@ -434,7 +476,7 @@ router.route("/api/users/:id").get(async (request, context) => {
434
476
  headers: { "Content-Type": "application/json" }
435
477
  });
436
478
  }
437
-
479
+
438
480
  return new Response(JSON.stringify({ user }), {
439
481
  headers: { "Content-Type": "application/json" }
440
482
  });
@@ -470,32 +512,32 @@ router.route("/").get(async (request, context) => {
470
512
  <body>
471
513
  <h1>\u{1F504} HTTP Echo Service</h1>
472
514
  <p>A simple HTTP request/response inspection service.</p>
473
-
515
+
474
516
  <div class="endpoint">
475
517
  <strong>POST /echo</strong><br>
476
518
  Echo back request details including headers, body, and metadata.
477
519
  </div>
478
-
520
+
479
521
  <div class="endpoint">
480
522
  <strong>GET /ip</strong><br>
481
523
  Get your IP address.
482
524
  </div>
483
-
525
+
484
526
  <div class="endpoint">
485
527
  <strong>GET /headers</strong><br>
486
528
  Get your request headers.
487
529
  </div>
488
-
530
+
489
531
  <div class="endpoint">
490
532
  <strong>GET /user-agent</strong><br>
491
533
  Get your user agent string.
492
534
  </div>
493
-
535
+
494
536
  <p>Try: <code>curl -X POST https://your-app.com/echo -d '{"test": "data"}'</code></p>
495
537
  </body>
496
538
  </html>
497
539
  \`;
498
-
540
+
499
541
  return new Response(html, {
500
542
  headers: { "Content-Type": "text/html" }
501
543
  });
@@ -504,24 +546,24 @@ router.route("/").get(async (request, context) => {
504
546
  router.route("/echo").all(async (request, context) => {
505
547
  const info = getRequestInfo(request);
506
548
  const body = await parseBody(request);
507
-
549
+
508
550
  const response = {
509
551
  ...info,
510
552
  body,
511
553
  contentType: request.headers.get("content-type") || null,
512
554
  timestamp: new Date().toISOString()
513
555
  };
514
-
556
+
515
557
  return new Response(JSON.stringify(response, null, 2), {
516
558
  headers: { "Content-Type": "application/json" }
517
559
  });
518
560
  });
519
561
 
520
562
  router.route("/ip").get(async (request, context) => {
521
- const ip = request.headers.get("x-forwarded-for") ||
522
- request.headers.get("x-real-ip") ||
563
+ const ip = request.headers.get("x-forwarded-for") ||
564
+ request.headers.get("x-real-ip") ||
523
565
  "127.0.0.1";
524
-
566
+
525
567
  return new Response(JSON.stringify({ ip }), {
526
568
  headers: { "Content-Type": "application/json" }
527
569
  });
@@ -558,7 +600,7 @@ npm install
558
600
  npm run develop
559
601
  \`\`\`
560
602
 
561
- Your app will be available at: **http://localhost:8000**
603
+ Your app will be available at: **http://localhost:3000**
562
604
 
563
605
  ## \u{1F4C1} Project Structure
564
606
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.21",
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
  "bin": {
@@ -10,41 +10,50 @@
10
10
  "create": "bin/create.js"
11
11
  },
12
12
  "dependencies": {
13
- "@b9g/async-context": "^0.1.1",
13
+ "@b9g/async-context": "^0.2.0-beta.0",
14
14
  "@clack/prompts": "^0.7.0",
15
+ "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
16
+ "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
15
17
  "@logtape/logtape": "^1.2.0",
16
18
  "commander": "^13.1.0",
17
- "esbuild": "^0.19.12",
19
+ "esbuild": "^0.27.2",
18
20
  "magic-string": "^0.30.5",
19
21
  "mime": "^4.0.4",
20
22
  "picocolors": "^1.0.0",
21
- "source-map": "^0.7.4"
23
+ "source-map": "^0.7.4",
24
+ "zod": "^3.23.0"
22
25
  },
23
26
  "devDependencies": {
24
- "@b9g/assets": "^0.1.6",
25
- "@b9g/cache": "^0.1.4",
27
+ "@b9g/assets": "^0.2.0-beta.0",
28
+ "@b9g/cache": "^0.2.0-beta.0",
26
29
  "@b9g/crank": "^0.7.2",
27
- "@b9g/filesystem": "^0.1.5",
28
- "@b9g/http-errors": "^0.1.4",
29
- "@b9g/libuild": "^0.1.17",
30
- "@b9g/platform": "^0.1.6",
31
- "@b9g/platform-bun": "^0.1.6",
32
- "@b9g/platform-cloudflare": "^0.1.5",
33
- "@b9g/platform-node": "^0.1.8",
34
- "@b9g/router": "^0.1.6",
35
- "@types/bun": "^1.2.2",
30
+ "@logtape/file": "^1.0.0",
31
+ "@b9g/filesystem": "^0.1.8",
32
+ "@b9g/http-errors": "^0.2.0-beta.0",
33
+ "@b9g/libuild": "^0.1.22",
34
+ "@b9g/platform": "^0.1.14-beta.3",
35
+ "@b9g/platform-bun": "^0.1.12-beta.0",
36
+ "@b9g/platform-cloudflare": "^0.1.12-beta.0",
37
+ "@b9g/platform-node": "^0.1.14-beta.0",
38
+ "@b9g/router": "^0.2.0-beta.1",
39
+ "@types/bun": "^1.3.4",
40
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
41
+ "@typescript-eslint/parser": "^8.0.0",
42
+ "eslint": "^9.0.0",
43
+ "eslint-config-prettier": "^10.0.0",
44
+ "eslint-plugin-prettier": "^5.0.0",
36
45
  "mitata": "^1.0.34",
37
46
  "typescript": "^5.7.3"
38
47
  },
39
48
  "peerDependencies": {
40
- "@b9g/node-webworker": "^0.1.3",
41
- "@b9g/platform": "^0.1.6",
42
- "@b9g/platform-node": "^0.1.8",
43
- "@b9g/platform-cloudflare": "^0.1.5",
44
- "@b9g/platform-bun": "^0.1.6",
45
- "@b9g/cache": "^0.1.4",
46
- "@b9g/filesystem": "^0.1.5",
47
- "@b9g/http-errors": "^0.1.4"
49
+ "@b9g/node-webworker": "^0.2.0-beta.1",
50
+ "@b9g/platform": "^0.1.14-beta.3",
51
+ "@b9g/platform-node": "^0.1.14-beta.0",
52
+ "@b9g/platform-cloudflare": "^0.1.12-beta.0",
53
+ "@b9g/platform-bun": "^0.1.12-beta.0",
54
+ "@b9g/cache": "^0.2.0-beta.0",
55
+ "@b9g/filesystem": "^0.1.8",
56
+ "@b9g/http-errors": "^0.2.0-beta.0"
48
57
  },
49
58
  "peerDependenciesMeta": {
50
59
  "@b9g/platform": {
@@ -70,8 +79,7 @@
70
79
  }
71
80
  },
72
81
  "type": "module",
73
- "types": "src/worker-entry.d.ts",
74
- "module": "src/worker-entry.js",
82
+ "types": "./src/config.d.ts",
75
83
  "exports": {
76
84
  "./package.json": "./package.json",
77
85
  "./bin/cli": {
@@ -89,18 +97,6 @@
89
97
  "./bin/create.js": {
90
98
  "types": "./dist/bin/create.d.ts",
91
99
  "import": "./dist/bin/create.js"
92
- },
93
- ".": {
94
- "types": "./src/worker-entry.d.ts",
95
- "import": "./src/worker-entry.js"
96
- },
97
- "./worker-entry": {
98
- "types": "./src/worker-entry.d.ts",
99
- "import": "./src/worker-entry.js"
100
- },
101
- "./worker-entry.js": {
102
- "types": "./src/worker-entry.d.ts",
103
- "import": "./src/worker-entry.js"
104
100
  }
105
101
  }
106
102
  }
@@ -0,0 +1,160 @@
1
+ import {
2
+ ServerBundler,
3
+ createPlatform
4
+ } from "./chunk-VWH6D26D.js";
5
+ import {
6
+ findProjectRoot,
7
+ findWorkspaceRoot
8
+ } from "./chunk-PTLNYIRW.js";
9
+
10
+ // src/commands/build.ts
11
+ import { resolve, join, dirname } from "path";
12
+ import { getLogger } from "@logtape/logtape";
13
+ import { resolvePlatform } from "@b9g/platform";
14
+ import { readFile, writeFile } from "fs/promises";
15
+ var logger = getLogger(["shovel"]);
16
+ async function buildForProduction({
17
+ entrypoint,
18
+ outDir,
19
+ platform = "node",
20
+ userBuildConfig,
21
+ lifecycle
22
+ }) {
23
+ const entryPath = resolve(entrypoint);
24
+ const outputDir = resolve(outDir);
25
+ const serverDir = join(outputDir, "server");
26
+ const projectRoot = findProjectRoot(dirname(entryPath));
27
+ logger.debug("Entry:", { entryPath });
28
+ logger.debug("Output:", { outputDir });
29
+ logger.debug("Target platform:", { platform });
30
+ logger.debug("Project root:", { projectRoot });
31
+ const platformInstance = await createPlatform(platform);
32
+ const platformESBuildConfig = platformInstance.getESBuildConfig();
33
+ const bundler = new ServerBundler({
34
+ entrypoint,
35
+ outDir,
36
+ platform: platformInstance,
37
+ platformESBuildConfig,
38
+ userBuildConfig,
39
+ lifecycle
40
+ });
41
+ const { success, outputs } = await bundler.build();
42
+ if (!success) {
43
+ throw new Error("Build failed");
44
+ }
45
+ await generatePackageJSON({ serverDir, platform, entryPath });
46
+ logger.debug("Built app to", { outputDir });
47
+ logger.debug("Server files", { dir: serverDir });
48
+ logger.debug("Public files", { dir: join(outputDir, "public") });
49
+ logger.info("Build complete: {path}", {
50
+ path: outputs.index || outputs.worker
51
+ });
52
+ return {
53
+ platform: platformInstance,
54
+ workerPath: outputs.worker
55
+ };
56
+ }
57
+ async function generatePackageJSON({
58
+ serverDir,
59
+ platform,
60
+ entryPath
61
+ }) {
62
+ const entryDir = dirname(entryPath);
63
+ const sourcePackageJsonPath = resolve(entryDir, "package.json");
64
+ try {
65
+ const packageJSONContent = await readFile(sourcePackageJsonPath, "utf8");
66
+ try {
67
+ JSON.parse(packageJSONContent);
68
+ } catch (parseError) {
69
+ throw new Error(`Invalid package.json format: ${parseError}`);
70
+ }
71
+ await writeFile(
72
+ join(serverDir, "package.json"),
73
+ packageJSONContent,
74
+ "utf8"
75
+ );
76
+ logger.debug("Copied package.json", { serverDir });
77
+ } catch (error) {
78
+ logger.debug("Could not copy package.json: {error}", { error });
79
+ try {
80
+ const generatedPackageJson = await generateExecutablePackageJSON(platform);
81
+ await writeFile(
82
+ join(serverDir, "package.json"),
83
+ JSON.stringify(generatedPackageJson, null, 2),
84
+ "utf8"
85
+ );
86
+ logger.debug("Generated package.json", { platform });
87
+ } catch (generateError) {
88
+ logger.debug("Could not generate package.json: {error}", {
89
+ error: generateError
90
+ });
91
+ }
92
+ }
93
+ }
94
+ async function generateExecutablePackageJSON(platform) {
95
+ const packageJSON = {
96
+ name: "shovel-executable",
97
+ version: "1.0.0",
98
+ type: "module",
99
+ private: true,
100
+ dependencies: {}
101
+ };
102
+ const isWorkspaceEnvironment = findWorkspaceRoot() !== null;
103
+ if (isWorkspaceEnvironment) {
104
+ packageJSON.dependencies = {};
105
+ } else {
106
+ switch (platform) {
107
+ case "node":
108
+ packageJSON.dependencies["@b9g/platform-node"] = "^0.1.0";
109
+ break;
110
+ case "bun":
111
+ packageJSON.dependencies["@b9g/platform-bun"] = "^0.1.0";
112
+ break;
113
+ case "cloudflare":
114
+ packageJSON.dependencies["@b9g/platform-cloudflare"] = "^0.1.0";
115
+ break;
116
+ default:
117
+ packageJSON.dependencies["@b9g/platform"] = "^0.1.0";
118
+ }
119
+ packageJSON.dependencies["@b9g/cache"] = "^0.1.0";
120
+ packageJSON.dependencies["@b9g/filesystem"] = "^0.1.0";
121
+ }
122
+ return packageJSON;
123
+ }
124
+ async function buildCommand(entrypoint, options, config) {
125
+ const platform = resolvePlatform({ ...options, config });
126
+ let lifecycleOption;
127
+ if (options.lifecycle) {
128
+ const stage = typeof options.lifecycle === "string" ? options.lifecycle : "activate";
129
+ if (stage !== "install" && stage !== "activate") {
130
+ throw new Error(
131
+ `Invalid lifecycle stage: ${stage}. Must be "install" or "activate".`
132
+ );
133
+ }
134
+ lifecycleOption = { stage };
135
+ }
136
+ const { platform: platformInstance, workerPath } = await buildForProduction({
137
+ entrypoint,
138
+ outDir: "dist",
139
+ platform,
140
+ userBuildConfig: config.build,
141
+ lifecycle: lifecycleOption
142
+ });
143
+ if (lifecycleOption) {
144
+ if (!workerPath) {
145
+ throw new Error("No worker entry point found in build outputs");
146
+ }
147
+ logger.info("Running ServiceWorker lifecycle: {stage}", {
148
+ stage: lifecycleOption.stage
149
+ });
150
+ await platformInstance.serviceWorker.register(workerPath);
151
+ await platformInstance.serviceWorker.ready;
152
+ await platformInstance.serviceWorker.terminate();
153
+ logger.info("Lifecycle complete");
154
+ }
155
+ await platformInstance.dispose();
156
+ }
157
+ export {
158
+ buildCommand,
159
+ buildForProduction
160
+ };