@b9g/shovel 0.2.0 → 0.2.2
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 +6 -0
- package/README.md +3 -1
- package/bin/cli.js +2 -2
- package/bin/create.js +310 -420
- package/package.json +1 -1
- package/src/_chunks/{build-QZE6GNRD.js → build-5DSZYRHR.js} +1 -1
- package/src/_chunks/{chunk-LVKYX67O.js → chunk-ZIKUWTXK.js} +6 -0
- package/src/_chunks/{develop-UJ7PMMOM.js → develop-DNLCO6BX.js} +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Shovel will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.2] - 2026-01-30
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **Fix asset manifest invalidation in dev mode** - Assets with new content hashes after client bundle changes no longer return 404. The root cause was build-time manifest resolution reading stale data from disk during rebuilds. ([#36](https://github.com/bikeshaving/shovel/pull/36), fixes [#35](https://github.com/bikeshaving/shovel/issues/35))
|
|
10
|
+
|
|
5
11
|
## [0.2.0-beta.12] - 2026-01-14
|
|
6
12
|
|
|
7
13
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -69,7 +69,9 @@ Your code uses standards. Shovel makes them work everywhere.
|
|
|
69
69
|
|
|
70
70
|
## Meta-Framework
|
|
71
71
|
|
|
72
|
-
Shovel is a meta-framework: it
|
|
72
|
+
Shovel is a meta-framework: it generates bundles and compiles your code with ESBuild.
|
|
73
|
+
You write code, and it runs in development and production with the exact same APIs.
|
|
74
|
+
Shovel takes care of single file bundle requirements, and transpiling JSX/TypeScript.
|
|
73
75
|
|
|
74
76
|
## True Portability
|
|
75
77
|
|
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-
|
|
78
|
+
const { developCommand } = await import("../src/_chunks/develop-DNLCO6BX.js");
|
|
79
79
|
await developCommand(entrypoint, options, config);
|
|
80
80
|
});
|
|
81
81
|
program.command("build <entrypoint>").description("Build app for production").option("--platform <name>", "Runtime platform (node, cloudflare, bun)").option(
|
|
@@ -83,7 +83,7 @@ program.command("build <entrypoint>").description("Build app for production").op
|
|
|
83
83
|
"Run ServiceWorker lifecycle after build (install or activate, default: activate)"
|
|
84
84
|
).action(async (entrypoint, options) => {
|
|
85
85
|
checkPlatformReexec(options);
|
|
86
|
-
const { buildCommand } = await import("../src/_chunks/build-
|
|
86
|
+
const { buildCommand } = await import("../src/_chunks/build-5DSZYRHR.js");
|
|
87
87
|
await buildCommand(entrypoint, options, config);
|
|
88
88
|
process.exit(0);
|
|
89
89
|
});
|
package/bin/create.js
CHANGED
|
@@ -7,6 +7,12 @@ import { intro, outro, text, select, confirm, spinner } from "@clack/prompts";
|
|
|
7
7
|
import { mkdir, writeFile } from "fs/promises";
|
|
8
8
|
import { join, resolve } from "path";
|
|
9
9
|
import { existsSync } from "fs";
|
|
10
|
+
function detectPlatform() {
|
|
11
|
+
if (process.env.npm_config_user_agent?.includes("bun")) {
|
|
12
|
+
return "bun";
|
|
13
|
+
}
|
|
14
|
+
return "node";
|
|
15
|
+
}
|
|
10
16
|
function validateProjectName(name) {
|
|
11
17
|
if (!name)
|
|
12
18
|
return "Project name is required";
|
|
@@ -46,54 +52,66 @@ async function main() {
|
|
|
46
52
|
process.exit(0);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
|
-
const
|
|
50
|
-
message: "
|
|
55
|
+
const template = await select({
|
|
56
|
+
message: "Choose a starter template:",
|
|
51
57
|
options: [
|
|
52
58
|
{
|
|
53
|
-
value: "
|
|
54
|
-
label:
|
|
55
|
-
hint: "
|
|
59
|
+
value: "hello-world",
|
|
60
|
+
label: "Hello World",
|
|
61
|
+
hint: "Minimal fetch handler to get started"
|
|
56
62
|
},
|
|
57
63
|
{
|
|
58
|
-
value: "
|
|
59
|
-
label:
|
|
64
|
+
value: "api",
|
|
65
|
+
label: "API",
|
|
66
|
+
hint: "REST endpoints with JSON responses"
|
|
60
67
|
},
|
|
61
68
|
{
|
|
62
|
-
value: "
|
|
63
|
-
label:
|
|
69
|
+
value: "static-site",
|
|
70
|
+
label: "Static Site",
|
|
71
|
+
hint: "Serve files from public/ directory"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
value: "full-stack",
|
|
75
|
+
label: "Full Stack",
|
|
76
|
+
hint: "HTML pages + API routes + static assets"
|
|
64
77
|
}
|
|
65
78
|
]
|
|
66
79
|
});
|
|
67
|
-
if (typeof
|
|
80
|
+
if (typeof template === "symbol") {
|
|
68
81
|
outro("Project creation cancelled");
|
|
69
82
|
process.exit(0);
|
|
70
83
|
}
|
|
71
|
-
const
|
|
72
|
-
message: "
|
|
84
|
+
const typescript = await confirm({
|
|
85
|
+
message: "Use TypeScript?",
|
|
86
|
+
initialValue: true
|
|
87
|
+
});
|
|
88
|
+
if (typeof typescript === "symbol") {
|
|
89
|
+
outro("Project creation cancelled");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
const detectedPlatform = detectPlatform();
|
|
93
|
+
const platform = await select({
|
|
94
|
+
message: "Which platform?",
|
|
95
|
+
initialValue: detectedPlatform,
|
|
73
96
|
options: [
|
|
74
97
|
{
|
|
75
|
-
value: "
|
|
76
|
-
label:
|
|
98
|
+
value: "node",
|
|
99
|
+
label: "Node.js",
|
|
100
|
+
hint: detectedPlatform === "node" ? "detected" : void 0
|
|
77
101
|
},
|
|
78
102
|
{
|
|
79
|
-
value: "
|
|
80
|
-
label:
|
|
103
|
+
value: "bun",
|
|
104
|
+
label: "Bun",
|
|
105
|
+
hint: detectedPlatform === "bun" ? "detected" : void 0
|
|
81
106
|
},
|
|
82
107
|
{
|
|
83
|
-
value: "
|
|
84
|
-
label:
|
|
108
|
+
value: "cloudflare",
|
|
109
|
+
label: "Cloudflare Workers",
|
|
110
|
+
hint: "Edge runtime"
|
|
85
111
|
}
|
|
86
112
|
]
|
|
87
113
|
});
|
|
88
|
-
if (typeof
|
|
89
|
-
outro("Project creation cancelled");
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
const typescript = await confirm({
|
|
93
|
-
message: "Use TypeScript?",
|
|
94
|
-
initialValue: false
|
|
95
|
-
});
|
|
96
|
-
if (typeof typescript === "symbol") {
|
|
114
|
+
if (typeof platform === "symbol") {
|
|
97
115
|
outro("Project creation cancelled");
|
|
98
116
|
process.exit(0);
|
|
99
117
|
}
|
|
@@ -112,11 +130,11 @@ async function main() {
|
|
|
112
130
|
outro("Your Shovel project is ready!");
|
|
113
131
|
console.info("");
|
|
114
132
|
console.info("Next steps:");
|
|
115
|
-
console.info(`
|
|
116
|
-
console.info(`
|
|
117
|
-
console.info(`
|
|
133
|
+
console.info(` cd ${projectName}`);
|
|
134
|
+
console.info(` npm install`);
|
|
135
|
+
console.info(` npm run dev`);
|
|
118
136
|
console.info("");
|
|
119
|
-
console.info(
|
|
137
|
+
console.info("Your app will be available at: http://localhost:3000");
|
|
120
138
|
console.info("");
|
|
121
139
|
} catch (error) {
|
|
122
140
|
s.stop("Failed to create project");
|
|
@@ -127,25 +145,28 @@ async function main() {
|
|
|
127
145
|
async function createProject(config, projectPath) {
|
|
128
146
|
await mkdir(projectPath, { recursive: true });
|
|
129
147
|
await mkdir(join(projectPath, "src"), { recursive: true });
|
|
148
|
+
if (config.template === "static-site" || config.template === "full-stack") {
|
|
149
|
+
await mkdir(join(projectPath, "public"), { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
const ext = config.typescript ? "ts" : "js";
|
|
130
152
|
const packageJson = {
|
|
131
153
|
name: config.name,
|
|
132
154
|
private: true,
|
|
133
|
-
version: "
|
|
134
|
-
description: `Shovel ${config.template} app for ${config.platform}`,
|
|
155
|
+
version: "0.0.1",
|
|
135
156
|
type: "module",
|
|
136
157
|
scripts: {
|
|
137
|
-
|
|
138
|
-
build: `shovel build src/app.${
|
|
139
|
-
start: "node dist/server/
|
|
158
|
+
dev: `shovel develop src/app.${ext} --platform ${config.platform}`,
|
|
159
|
+
build: `shovel build src/app.${ext} --platform ${config.platform}`,
|
|
160
|
+
start: "node dist/server/supervisor.js"
|
|
140
161
|
},
|
|
141
162
|
dependencies: {
|
|
142
|
-
"@b9g/router": "^0.
|
|
143
|
-
"@b9g/
|
|
144
|
-
|
|
145
|
-
"@b9g/
|
|
163
|
+
"@b9g/router": "^0.2.0",
|
|
164
|
+
"@b9g/shovel": "^0.2.0",
|
|
165
|
+
"@b9g/filesystem": "^0.1.8",
|
|
166
|
+
"@b9g/cache": "^0.2.0"
|
|
146
167
|
},
|
|
147
168
|
devDependencies: config.typescript ? {
|
|
148
|
-
"@types/node": "^
|
|
169
|
+
"@types/node": "^18.0.0",
|
|
149
170
|
typescript: "^5.0.0"
|
|
150
171
|
} : {}
|
|
151
172
|
};
|
|
@@ -154,8 +175,7 @@ async function createProject(config, projectPath) {
|
|
|
154
175
|
JSON.stringify(packageJson, null, 2)
|
|
155
176
|
);
|
|
156
177
|
const appFile = generateAppFile(config);
|
|
157
|
-
|
|
158
|
-
await writeFile(join(projectPath, `src/app.${appExt}`), appFile);
|
|
178
|
+
await writeFile(join(projectPath, `src/app.${ext}`), appFile);
|
|
159
179
|
if (config.typescript) {
|
|
160
180
|
const tsConfig = {
|
|
161
181
|
compilerOptions: {
|
|
@@ -177,6 +197,9 @@ async function createProject(config, projectPath) {
|
|
|
177
197
|
JSON.stringify(tsConfig, null, 2)
|
|
178
198
|
);
|
|
179
199
|
}
|
|
200
|
+
if (config.template === "static-site" || config.template === "full-stack") {
|
|
201
|
+
await createStaticFiles(config, projectPath);
|
|
202
|
+
}
|
|
180
203
|
const readme = generateReadme(config);
|
|
181
204
|
await writeFile(join(projectPath, "README.md"), readme);
|
|
182
205
|
const gitignore = `node_modules/
|
|
@@ -188,447 +211,314 @@ dist/
|
|
|
188
211
|
`;
|
|
189
212
|
await writeFile(join(projectPath, ".gitignore"), gitignore);
|
|
190
213
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
async function createStaticFiles(config, projectPath) {
|
|
215
|
+
const indexHtml = `<!DOCTYPE html>
|
|
216
|
+
<html lang="en">
|
|
217
|
+
<head>
|
|
218
|
+
<meta charset="UTF-8">
|
|
219
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
220
|
+
<title>${config.name}</title>
|
|
221
|
+
<link rel="stylesheet" href="/styles.css">
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
<main>
|
|
225
|
+
<h1>Welcome to ${config.name}</h1>
|
|
226
|
+
<p>Edit <code>public/index.html</code> to get started.</p>
|
|
227
|
+
${config.template === "full-stack" ? '<p>API endpoint: <a href="/api/hello">/api/hello</a></p>' : ""}
|
|
228
|
+
</main>
|
|
229
|
+
</body>
|
|
230
|
+
</html>
|
|
231
|
+
`;
|
|
232
|
+
await writeFile(join(projectPath, "public/index.html"), indexHtml);
|
|
233
|
+
const stylesCss = `* {
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
margin: 0;
|
|
236
|
+
padding: 0;
|
|
200
237
|
}
|
|
201
238
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return await request.json();
|
|
208
|
-
} catch (err) {
|
|
209
|
-
// Only ignore JSON parse errors, rethrow others
|
|
210
|
-
if (
|
|
211
|
-
!(err instanceof SyntaxError) ||
|
|
212
|
-
!/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
|
|
213
|
-
) {
|
|
214
|
-
throw err;
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
221
|
-
try {
|
|
222
|
-
const formData = await request.formData();
|
|
223
|
-
return Object.fromEntries(formData.entries());
|
|
224
|
-
} catch (err) {
|
|
225
|
-
// Only ignore form data parse errors, rethrow others
|
|
226
|
-
if (
|
|
227
|
-
!(err instanceof TypeError) ||
|
|
228
|
-
!String(err.message).includes('FormData')
|
|
229
|
-
) {
|
|
230
|
-
throw err;
|
|
231
|
-
}
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const text = await request.text();
|
|
238
|
-
return text || null;
|
|
239
|
-
} catch (err) {
|
|
240
|
-
// Only ignore body already consumed errors, rethrow others
|
|
241
|
-
if (
|
|
242
|
-
!(err instanceof TypeError) ||
|
|
243
|
-
!String(err.message).includes('body')
|
|
244
|
-
) {
|
|
245
|
-
throw err;
|
|
246
|
-
}
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
` : config.template === "echo" ? `
|
|
251
|
-
// Helper functions for echo service
|
|
252
|
-
function getRequestInfo(request) {
|
|
253
|
-
return {
|
|
254
|
-
method: request.method,
|
|
255
|
-
url: request.url,
|
|
256
|
-
headers: Object.fromEntries(request.headers.entries()),
|
|
257
|
-
};
|
|
239
|
+
body {
|
|
240
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
241
|
+
line-height: 1.6;
|
|
242
|
+
color: #333;
|
|
243
|
+
background: #fafafa;
|
|
258
244
|
}
|
|
259
245
|
|
|
260
|
-
|
|
261
|
-
|
|
246
|
+
main {
|
|
247
|
+
max-width: 640px;
|
|
248
|
+
margin: 4rem auto;
|
|
249
|
+
padding: 2rem;
|
|
250
|
+
}
|
|
262
251
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Only ignore JSON parse errors, rethrow others
|
|
268
|
-
if (
|
|
269
|
-
!(err instanceof SyntaxError) ||
|
|
270
|
-
!/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
|
|
271
|
-
) {
|
|
272
|
-
throw err;
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
252
|
+
h1 {
|
|
253
|
+
color: #2563eb;
|
|
254
|
+
margin-bottom: 1rem;
|
|
255
|
+
}
|
|
277
256
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
!(err instanceof TypeError) ||
|
|
286
|
-
!String(err.message).includes('FormData')
|
|
287
|
-
) {
|
|
288
|
-
throw err;
|
|
289
|
-
}
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
257
|
+
code {
|
|
258
|
+
background: #e5e7eb;
|
|
259
|
+
padding: 0.2rem 0.4rem;
|
|
260
|
+
border-radius: 4px;
|
|
261
|
+
font-size: 0.9em;
|
|
262
|
+
}
|
|
293
263
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
264
|
+
a {
|
|
265
|
+
color: #2563eb;
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
await writeFile(join(projectPath, "public/styles.css"), stylesCss);
|
|
269
|
+
}
|
|
270
|
+
function generateAppFile(config) {
|
|
271
|
+
switch (config.template) {
|
|
272
|
+
case "hello-world":
|
|
273
|
+
return generateHelloWorld(config);
|
|
274
|
+
case "api":
|
|
275
|
+
return generateApi(config);
|
|
276
|
+
case "static-site":
|
|
277
|
+
return generateStaticSite(config);
|
|
278
|
+
case "full-stack":
|
|
279
|
+
return generateFullStack(config);
|
|
280
|
+
default:
|
|
281
|
+
return generateHelloWorld(config);
|
|
306
282
|
}
|
|
307
283
|
}
|
|
308
|
-
|
|
309
|
-
return
|
|
284
|
+
function generateHelloWorld(config) {
|
|
285
|
+
return `// ${config.name} - Hello World
|
|
286
|
+
self.addEventListener("fetch", (event) => {
|
|
287
|
+
event.respondWith(
|
|
288
|
+
new Response("Hello from Shovel!", {
|
|
289
|
+
headers: { "Content-Type": "text/plain" },
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
function generateApi(config) {
|
|
296
|
+
return `import { Router } from "@b9g/router";
|
|
310
297
|
|
|
311
298
|
const router = new Router();
|
|
312
299
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
300
|
+
// In-memory data store
|
|
301
|
+
const users = [
|
|
302
|
+
{ id: 1, name: "Alice", email: "alice@example.com" },
|
|
303
|
+
{ id: 2, name: "Bob", email: "bob@example.com" },
|
|
304
|
+
];
|
|
319
305
|
|
|
320
|
-
|
|
321
|
-
|
|
306
|
+
// List all users
|
|
307
|
+
router.route("/api/users").get(() => {
|
|
308
|
+
return Response.json({ users });
|
|
322
309
|
});
|
|
323
310
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
} catch (error) {
|
|
330
|
-
console.error("[${config.name}] Error handling request:", error);
|
|
331
|
-
event.respondWith(
|
|
332
|
-
new Response(
|
|
333
|
-
JSON.stringify({
|
|
334
|
-
error: "Internal server error",
|
|
335
|
-
message: error.message,
|
|
336
|
-
}),
|
|
337
|
-
{
|
|
338
|
-
status: 500,
|
|
339
|
-
headers: { "Content-Type": "application/json" },
|
|
340
|
-
}
|
|
341
|
-
)
|
|
342
|
-
);
|
|
311
|
+
// Get user by ID
|
|
312
|
+
router.route("/api/users/:id").get((req, ctx) => {
|
|
313
|
+
const user = users.find((u) => u.id === Number(ctx.params.id));
|
|
314
|
+
if (!user) {
|
|
315
|
+
return Response.json({ error: "User not found" }, { status: 404 });
|
|
343
316
|
}
|
|
317
|
+
return Response.json({ user });
|
|
344
318
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<title>Welcome to Shovel!</title>
|
|
357
|
-
<meta charset="utf-8">
|
|
358
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
359
|
-
<style>
|
|
360
|
-
body { font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 2rem; }
|
|
361
|
-
h1 { color: #2563eb; }
|
|
362
|
-
.info { background: #f1f5f9; padding: 1rem; border-radius: 8px; margin: 1rem 0; }
|
|
363
|
-
code { background: #e2e8f0; padding: 0.2rem 0.4rem; border-radius: 4px; }
|
|
364
|
-
</style>
|
|
365
|
-
</head>
|
|
366
|
-
<body>
|
|
367
|
-
<h1>\u{1F680} Welcome to Shovel!</h1>
|
|
368
|
-
<p>Your ${config.template} app is running on the <strong>${config.platform}</strong> platform.</p>
|
|
369
|
-
|
|
370
|
-
<div class="info">
|
|
371
|
-
<strong>Try these endpoints:</strong>
|
|
372
|
-
<ul>
|
|
373
|
-
<li><a href="/api/hello">GET /api/hello</a> - Simple API endpoint</li>
|
|
374
|
-
<li><a href="/api/time">GET /api/time</a> - Current timestamp</li>
|
|
375
|
-
</ul>
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<p>Edit <code>src/app.${config.typescript ? "ts" : "js"}</code> to customize your app!</p>
|
|
379
|
-
</body>
|
|
380
|
-
</html>
|
|
381
|
-
\`;
|
|
382
|
-
|
|
383
|
-
return new Response(html, {
|
|
384
|
-
headers: { "Content-Type": "text/html" }
|
|
385
|
-
});
|
|
319
|
+
|
|
320
|
+
// Create user
|
|
321
|
+
router.route("/api/users").post(async (req) => {
|
|
322
|
+
const body = await req.json();
|
|
323
|
+
const user = {
|
|
324
|
+
id: users.length + 1,
|
|
325
|
+
name: body.name,
|
|
326
|
+
email: body.email,
|
|
327
|
+
};
|
|
328
|
+
users.push(user);
|
|
329
|
+
return Response.json({ user }, { status: 201 });
|
|
386
330
|
});
|
|
387
331
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
platform: "${config.platform}",
|
|
392
|
-
timestamp: new Date().toISOString()
|
|
393
|
-
}), {
|
|
394
|
-
headers: { "Content-Type": "application/json" }
|
|
395
|
-
});
|
|
332
|
+
// Health check
|
|
333
|
+
router.route("/health").get(() => {
|
|
334
|
+
return Response.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
396
335
|
});
|
|
397
336
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
unix: Date.now(),
|
|
402
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
403
|
-
}), {
|
|
404
|
-
headers: { "Content-Type": "application/json" }
|
|
405
|
-
});
|
|
406
|
-
});`;
|
|
407
|
-
case "api":
|
|
408
|
-
return `// API routes
|
|
409
|
-
router.route("/").get(async (request, context) => {
|
|
410
|
-
return new Response(JSON.stringify({
|
|
337
|
+
// Root - API info
|
|
338
|
+
router.route("/").get(() => {
|
|
339
|
+
return Response.json({
|
|
411
340
|
name: "${config.name}",
|
|
412
|
-
platform: "${config.platform}",
|
|
413
341
|
endpoints: [
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
]
|
|
419
|
-
}), {
|
|
420
|
-
headers: { "Content-Type": "application/json" }
|
|
342
|
+
"GET /api/users",
|
|
343
|
+
"GET /api/users/:id",
|
|
344
|
+
"POST /api/users",
|
|
345
|
+
"GET /health",
|
|
346
|
+
],
|
|
421
347
|
});
|
|
422
348
|
});
|
|
423
349
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
{ id: 1, name: "Alice Johnson", email: "alice@example.com", active: true },
|
|
427
|
-
{ id: 2, name: "Bob Smith", email: "bob@example.com", active: true },
|
|
428
|
-
{ id: 3, name: "Carol Davis", email: "carol@example.com", active: false }
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
router.route("/api/users").get(async (request, context) => {
|
|
432
|
-
const url = new URL(request.url);
|
|
433
|
-
const active = url.searchParams.get('active');
|
|
434
|
-
|
|
435
|
-
let filteredUsers = users;
|
|
436
|
-
if (active !== null) {
|
|
437
|
-
filteredUsers = users.filter(user => user.active === (active === 'true'));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return new Response(JSON.stringify({
|
|
441
|
-
users: filteredUsers,
|
|
442
|
-
total: filteredUsers.length
|
|
443
|
-
}), {
|
|
444
|
-
headers: { "Content-Type": "application/json" }
|
|
445
|
-
});
|
|
350
|
+
self.addEventListener("fetch", (event) => {
|
|
351
|
+
event.respondWith(router.handle(event.request));
|
|
446
352
|
});
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
function generateStaticSite(config) {
|
|
356
|
+
return `// ${config.name} - Static Site
|
|
357
|
+
// Serves files from the public/ directory
|
|
447
358
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
id: Math.max(...users.map(u => u.id)) + 1,
|
|
452
|
-
name: userData.name || "Unknown User",
|
|
453
|
-
email: userData.email || \`user\${Date.now()}@example.com\`,
|
|
454
|
-
active: userData.active !== false
|
|
455
|
-
};
|
|
359
|
+
self.addEventListener("fetch", (event) => {
|
|
360
|
+
event.respondWith(handleRequest(event.request));
|
|
361
|
+
});
|
|
456
362
|
|
|
457
|
-
|
|
363
|
+
async function handleRequest(request${config.typescript ? ": Request" : ""})${config.typescript ? ": Promise<Response>" : ""} {
|
|
364
|
+
const url = new URL(request.url);
|
|
365
|
+
let path = url.pathname;
|
|
458
366
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
status: 201,
|
|
464
|
-
headers: { "Content-Type": "application/json" }
|
|
465
|
-
});
|
|
466
|
-
});
|
|
367
|
+
// Default to index.html for root
|
|
368
|
+
if (path === "/") {
|
|
369
|
+
path = "/index.html";
|
|
370
|
+
}
|
|
467
371
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
372
|
+
// Try to serve from public directory
|
|
373
|
+
try {
|
|
374
|
+
const publicDir = await directories.open("public");
|
|
375
|
+
const file = await publicDir.getFileHandle(path.slice(1)); // Remove leading /
|
|
376
|
+
const blob = await file.getFile();
|
|
471
377
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
status: 404,
|
|
477
|
-
headers: { "Content-Type": "application/json" }
|
|
378
|
+
return new Response(blob, {
|
|
379
|
+
headers: {
|
|
380
|
+
"Content-Type": getContentType(path),
|
|
381
|
+
},
|
|
478
382
|
});
|
|
383
|
+
} catch {
|
|
384
|
+
// File not found - return 404
|
|
385
|
+
return new Response("Not Found", { status: 404 });
|
|
479
386
|
}
|
|
387
|
+
}
|
|
480
388
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
389
|
+
function getContentType(path${config.typescript ? ": string" : ""})${config.typescript ? ": string" : ""} {
|
|
390
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
391
|
+
const types${config.typescript ? ": Record<string, string>" : ""} = {
|
|
392
|
+
html: "text/html",
|
|
393
|
+
css: "text/css",
|
|
394
|
+
js: "text/javascript",
|
|
395
|
+
json: "application/json",
|
|
396
|
+
png: "image/png",
|
|
397
|
+
jpg: "image/jpeg",
|
|
398
|
+
jpeg: "image/jpeg",
|
|
399
|
+
gif: "image/gif",
|
|
400
|
+
svg: "image/svg+xml",
|
|
401
|
+
ico: "image/x-icon",
|
|
402
|
+
};
|
|
403
|
+
return types[ext || ""] || "application/octet-stream";
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
function generateFullStack(config) {
|
|
408
|
+
return `import { Router } from "@b9g/router";
|
|
409
|
+
|
|
410
|
+
const router = new Router();
|
|
485
411
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
412
|
+
// API routes
|
|
413
|
+
router.route("/api/hello").get(() => {
|
|
414
|
+
return Response.json({
|
|
415
|
+
message: "Hello from the API!",
|
|
490
416
|
timestamp: new Date().toISOString(),
|
|
491
|
-
uptime: process.uptime()
|
|
492
|
-
}), {
|
|
493
|
-
headers: { "Content-Type": "application/json" }
|
|
494
|
-
});
|
|
495
|
-
});`;
|
|
496
|
-
case "echo":
|
|
497
|
-
return `// Echo service routes (like httpbin)
|
|
498
|
-
router.route("/").get(async (request, context) => {
|
|
499
|
-
const html = \`
|
|
500
|
-
<!DOCTYPE html>
|
|
501
|
-
<html>
|
|
502
|
-
<head>
|
|
503
|
-
<title>HTTP Echo Service</title>
|
|
504
|
-
<meta charset="utf-8">
|
|
505
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
506
|
-
<style>
|
|
507
|
-
body { font-family: monospace; max-width: 800px; margin: 2rem auto; padding: 2rem; }
|
|
508
|
-
h1 { color: #059669; }
|
|
509
|
-
.endpoint { background: #f0fdf4; padding: 1rem; margin: 1rem 0; border-radius: 8px; }
|
|
510
|
-
code { background: #dcfce7; padding: 0.2rem 0.4rem; border-radius: 4px; }
|
|
511
|
-
</style>
|
|
512
|
-
</head>
|
|
513
|
-
<body>
|
|
514
|
-
<h1>\u{1F504} HTTP Echo Service</h1>
|
|
515
|
-
<p>A simple HTTP request/response inspection service.</p>
|
|
516
|
-
|
|
517
|
-
<div class="endpoint">
|
|
518
|
-
<strong>POST /echo</strong><br>
|
|
519
|
-
Echo back request details including headers, body, and metadata.
|
|
520
|
-
</div>
|
|
521
|
-
|
|
522
|
-
<div class="endpoint">
|
|
523
|
-
<strong>GET /ip</strong><br>
|
|
524
|
-
Get your IP address.
|
|
525
|
-
</div>
|
|
526
|
-
|
|
527
|
-
<div class="endpoint">
|
|
528
|
-
<strong>GET /headers</strong><br>
|
|
529
|
-
Get your request headers.
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
<div class="endpoint">
|
|
533
|
-
<strong>GET /user-agent</strong><br>
|
|
534
|
-
Get your user agent string.
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
<p>Try: <code>curl -X POST https://your-app.com/echo -d '{"test": "data"}'</code></p>
|
|
538
|
-
</body>
|
|
539
|
-
</html>
|
|
540
|
-
\`;
|
|
541
|
-
|
|
542
|
-
return new Response(html, {
|
|
543
|
-
headers: { "Content-Type": "text/html" }
|
|
544
417
|
});
|
|
545
418
|
});
|
|
546
419
|
|
|
547
|
-
router.route("/echo").
|
|
548
|
-
const
|
|
549
|
-
|
|
420
|
+
router.route("/api/echo").post(async (req) => {
|
|
421
|
+
const body = await req.json();
|
|
422
|
+
return Response.json({ echo: body });
|
|
423
|
+
});
|
|
550
424
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
timestamp: new Date().toISOString()
|
|
556
|
-
};
|
|
425
|
+
// Serve static files from public/ for all other routes
|
|
426
|
+
router.route("/*").get(async (req, ctx) => {
|
|
427
|
+
const url = new URL(req.url);
|
|
428
|
+
let path = url.pathname;
|
|
557
429
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
430
|
+
// Default to index.html for root
|
|
431
|
+
if (path === "/") {
|
|
432
|
+
path = "/index.html";
|
|
433
|
+
}
|
|
562
434
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
435
|
+
try {
|
|
436
|
+
const publicDir = await directories.open("public");
|
|
437
|
+
const file = await publicDir.getFileHandle(path.slice(1));
|
|
438
|
+
const blob = await file.getFile();
|
|
567
439
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
440
|
+
return new Response(blob, {
|
|
441
|
+
headers: {
|
|
442
|
+
"Content-Type": getContentType(path),
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
// Try index.html for SPA routing
|
|
447
|
+
try {
|
|
448
|
+
const publicDir = await directories.open("public");
|
|
449
|
+
const file = await publicDir.getFileHandle("index.html");
|
|
450
|
+
const blob = await file.getFile();
|
|
451
|
+
return new Response(blob, {
|
|
452
|
+
headers: { "Content-Type": "text/html" },
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
return new Response("Not Found", { status: 404 });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
571
458
|
});
|
|
572
459
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
460
|
+
function getContentType(path${config.typescript ? ": string" : ""})${config.typescript ? ": string" : ""} {
|
|
461
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
462
|
+
const types${config.typescript ? ": Record<string, string>" : ""} = {
|
|
463
|
+
html: "text/html",
|
|
464
|
+
css: "text/css",
|
|
465
|
+
js: "text/javascript",
|
|
466
|
+
json: "application/json",
|
|
467
|
+
png: "image/png",
|
|
468
|
+
jpg: "image/jpeg",
|
|
469
|
+
jpeg: "image/jpeg",
|
|
470
|
+
gif: "image/gif",
|
|
471
|
+
svg: "image/svg+xml",
|
|
472
|
+
ico: "image/x-icon",
|
|
473
|
+
};
|
|
474
|
+
return types[ext || ""] || "application/octet-stream";
|
|
475
|
+
}
|
|
580
476
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
headers: { "Content-Type": "application/json" }
|
|
586
|
-
});
|
|
587
|
-
});`;
|
|
588
|
-
default:
|
|
589
|
-
return "// Routes will be added here";
|
|
590
|
-
}
|
|
477
|
+
self.addEventListener("fetch", (event) => {
|
|
478
|
+
event.respondWith(router.handle(event.request));
|
|
479
|
+
});
|
|
480
|
+
`;
|
|
591
481
|
}
|
|
592
482
|
function generateReadme(config) {
|
|
483
|
+
const templateDescriptions = {
|
|
484
|
+
"hello-world": "A minimal Shovel application",
|
|
485
|
+
api: "A REST API with JSON endpoints",
|
|
486
|
+
"static-site": "A static file server",
|
|
487
|
+
"full-stack": "A full-stack app with API routes and static files"
|
|
488
|
+
};
|
|
593
489
|
return `# ${config.name}
|
|
594
490
|
|
|
595
|
-
|
|
491
|
+
${templateDescriptions[config.template]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
596
492
|
|
|
597
|
-
##
|
|
493
|
+
## Getting Started
|
|
598
494
|
|
|
599
495
|
\`\`\`bash
|
|
600
496
|
npm install
|
|
601
|
-
npm run
|
|
497
|
+
npm run dev
|
|
602
498
|
\`\`\`
|
|
603
499
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
## \u{1F4C1} Project Structure
|
|
500
|
+
Open http://localhost:3000
|
|
607
501
|
|
|
608
|
-
|
|
609
|
-
- \`package.json\` - Dependencies and scripts${config.typescript ? "\n- `tsconfig.json` - TypeScript configuration" : ""}
|
|
502
|
+
## Scripts
|
|
610
503
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
- \`npm run develop\` - Start development server with hot reload
|
|
504
|
+
- \`npm run dev\` - Start development server
|
|
614
505
|
- \`npm run build\` - Build for production
|
|
615
|
-
- \`npm
|
|
616
|
-
|
|
617
|
-
## \u2728 Features
|
|
618
|
-
|
|
619
|
-
- \u2705 **ServiceWorker APIs** - Standard web APIs (\`self.addEventListener\`, etc.)
|
|
620
|
-
- \u2705 **${config.platform} Runtime** - Optimized for ${config.platform}
|
|
621
|
-
- \u2705 **${config.typescript ? "TypeScript" : "JavaScript"}** - ${config.typescript ? "Full type safety" : "Modern JavaScript"} with ESM modules
|
|
622
|
-
- \u2705 **${config.template} Template** - Ready-to-use starter with routing
|
|
506
|
+
- \`npm start\` - Run production build
|
|
623
507
|
|
|
624
|
-
##
|
|
508
|
+
## Project Structure
|
|
625
509
|
|
|
626
|
-
|
|
627
|
-
|
|
510
|
+
\`\`\`
|
|
511
|
+
${config.name}/
|
|
512
|
+
\u251C\u2500\u2500 src/
|
|
513
|
+
\u2502 \u2514\u2500\u2500 app.${config.typescript ? "ts" : "js"} # Application entry point
|
|
514
|
+
${config.template === "static-site" || config.template === "full-stack" ? "\u251C\u2500\u2500 public/ # Static files\n\u2502 \u251C\u2500\u2500 index.html\n\u2502 \u2514\u2500\u2500 styles.css\n" : ""}\u251C\u2500\u2500 package.json
|
|
515
|
+
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md
|
|
516
|
+
\`\`\`
|
|
628
517
|
|
|
629
|
-
|
|
518
|
+
## Learn More
|
|
630
519
|
|
|
631
|
-
|
|
520
|
+
- [Shovel Documentation](https://github.com/bikeshaving/shovel)
|
|
521
|
+
- [ServiceWorker API](https://developer.mozilla.org/docs/Web/API/Service_Worker_API)
|
|
632
522
|
`;
|
|
633
523
|
}
|
|
634
524
|
process.on("SIGINT", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/shovel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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": {
|
|
@@ -475,6 +475,12 @@ function createAssetsManifestPlugin(projectRoot, outDir = "dist", sharedManifest
|
|
|
475
475
|
namespace: "shovel-assets"
|
|
476
476
|
}));
|
|
477
477
|
build2.onLoad({ filter: /.*/, namespace: "shovel-assets" }, () => {
|
|
478
|
+
if (sharedManifest) {
|
|
479
|
+
return {
|
|
480
|
+
contents: `export default ${MANIFEST_PLACEHOLDER};`,
|
|
481
|
+
loader: "js"
|
|
482
|
+
};
|
|
483
|
+
}
|
|
478
484
|
if (existsSync2(manifestPath)) {
|
|
479
485
|
try {
|
|
480
486
|
const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
|