@b9g/shovel 0.2.4 → 0.2.7
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 +34 -0
- package/README.md +2 -8
- package/bin/create.js +727 -229
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
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
|
+
|
|
17
|
+
## [0.2.6] - 2026-02-06
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- **UI framework selection in create-shovel** - Choose between Vanilla, HTMX, Alpine.js, and Crank.js when scaffolding static-site and full-stack templates ([#44](https://github.com/bikeshaving/shovel/pull/44))
|
|
22
|
+
- **Default `["app"]` logger category** - User application logs under `["app", ...]` now work out of the box without configuration. Framework logs under `["shovel", ...]`, third-party libraries remain silent unless opted in.
|
|
23
|
+
- **Default exports for cache and filesystem modules** - `@b9g/cache/memory`, `@b9g/cache-redis`, `@b9g/filesystem/node-fs`, `@b9g/filesystem/memory`, and `@b9g/filesystem/bun-s3` now have default exports, so `"export"` can be omitted from `shovel.json` config.
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
- **Cache API compliance** - Wildcard pattern matching (`"*"`) for cache and directory configs, `PostMessageCache` now accepts `RequestInfo | URL` per spec, `matchPattern()` restored for config lookups ([#43](https://github.com/bikeshaving/shovel/pull/43))
|
|
28
|
+
- **Direct cache in single-worker dev mode** - Dev workers now use `MemoryCache` directly instead of `PostMessageCache` when `workers: 1`, avoiding unnecessary serialization overhead
|
|
29
|
+
- **Node.js Request body duplex** - Added `duplex: "half"` to Node.js Request construction to fix body streaming
|
|
30
|
+
- **Website 404 errors** - Views now throw `NotFound` from `@b9g/http-errors` instead of raw errors, returning proper 404 responses
|
|
31
|
+
- **Fixed `@b9g/cache-redis` module path in docs** - Documentation referenced `@b9g/cache/redis` instead of the correct `@b9g/cache-redis`
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
|
|
35
|
+
- **PostMessageCache WPT tests** - 29 Web Platform Tests now run against PostMessageCache to verify serialization round-trip compliance
|
|
36
|
+
- **Pattern matching unit tests** - Wildcard, prefix, and exact-match priority tests for cache and directory factories
|
|
37
|
+
- **End-to-end cache tests** - Runtime tests for KV server, multi-cache independence, and wildcard priority
|
|
38
|
+
|
|
5
39
|
## [0.2.3] - 2026-02-02
|
|
6
40
|
|
|
7
41
|
### Features
|
package/README.md
CHANGED
|
@@ -184,14 +184,12 @@ Shovel's configuration follows these principles:
|
|
|
184
184
|
"caches": {
|
|
185
185
|
"sessions": {
|
|
186
186
|
"module": "@b9g/cache-redis",
|
|
187
|
-
"export": "RedisCache",
|
|
188
187
|
"url": "$REDIS_URL"
|
|
189
188
|
}
|
|
190
189
|
},
|
|
191
190
|
"directories": {
|
|
192
191
|
"uploads": {
|
|
193
192
|
"module": "@b9g/filesystem-s3",
|
|
194
|
-
"export": "S3Directory",
|
|
195
193
|
"bucket": "$S3_BUCKET"
|
|
196
194
|
}
|
|
197
195
|
},
|
|
@@ -211,18 +209,16 @@ Shovel's configuration follows these principles:
|
|
|
211
209
|
|
|
212
210
|
### Caches
|
|
213
211
|
|
|
214
|
-
Configure cache backends using `module`
|
|
212
|
+
Configure cache backends using `module` (uses default export, or specify `export` for named exports):
|
|
215
213
|
|
|
216
214
|
```json
|
|
217
215
|
{
|
|
218
216
|
"caches": {
|
|
219
217
|
"api-responses": {
|
|
220
|
-
"module": "@b9g/cache/memory"
|
|
221
|
-
"export": "MemoryCache"
|
|
218
|
+
"module": "@b9g/cache/memory"
|
|
222
219
|
},
|
|
223
220
|
"sessions": {
|
|
224
221
|
"module": "@b9g/cache-redis",
|
|
225
|
-
"export": "RedisCache",
|
|
226
222
|
"url": "$REDIS_URL"
|
|
227
223
|
}
|
|
228
224
|
}
|
|
@@ -242,13 +238,11 @@ Configure directory backends. Platforms provide defaults for well-known director
|
|
|
242
238
|
"directories": {
|
|
243
239
|
"uploads": {
|
|
244
240
|
"module": "@b9g/filesystem-s3",
|
|
245
|
-
"export": "S3Directory",
|
|
246
241
|
"bucket": "MY_BUCKET",
|
|
247
242
|
"region": "us-east-1"
|
|
248
243
|
},
|
|
249
244
|
"data": {
|
|
250
245
|
"module": "@b9g/filesystem/node-fs",
|
|
251
|
-
"export": "NodeFSDirectory",
|
|
252
246
|
"path": "./data"
|
|
253
247
|
}
|
|
254
248
|
}
|
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
|
-
|
|
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,74 +68,144 @@ async function main() {
|
|
|
52
68
|
process.exit(0);
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
let template;
|
|
72
|
+
let uiFramework = "vanilla";
|
|
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;
|
|
83
116
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
if (uiFramework === "vanilla" && (template === "static-site" || template === "full-stack")) {
|
|
118
|
+
const framework = await select({
|
|
119
|
+
message: "UI framework:",
|
|
120
|
+
initialValue: "crank",
|
|
121
|
+
options: [
|
|
122
|
+
{
|
|
123
|
+
value: "alpine",
|
|
124
|
+
label: "Alpine.js",
|
|
125
|
+
hint: "Lightweight reactivity with x-data directives"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
value: "crank",
|
|
129
|
+
label: "Crank.js",
|
|
130
|
+
hint: "JSX components rendered on the server"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: "htmx",
|
|
134
|
+
label: "HTMX",
|
|
135
|
+
hint: "HTML-driven interactions with hx- attributes"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
value: "vanilla",
|
|
139
|
+
label: "Vanilla",
|
|
140
|
+
hint: "Plain HTML, no framework"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
if (typeof framework === "symbol") {
|
|
145
|
+
outro("Project creation cancelled");
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
uiFramework = framework;
|
|
91
149
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
163
|
+
}
|
|
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;
|
|
117
202
|
}
|
|
118
203
|
const config = {
|
|
119
204
|
name: projectName,
|
|
120
205
|
platform,
|
|
121
206
|
template,
|
|
122
|
-
typescript
|
|
207
|
+
typescript,
|
|
208
|
+
uiFramework
|
|
123
209
|
};
|
|
124
210
|
const s = spinner();
|
|
125
211
|
s.start("Creating your Shovel project...");
|
|
@@ -132,7 +218,7 @@ async function main() {
|
|
|
132
218
|
console.info("Next steps:");
|
|
133
219
|
console.info(` cd ${projectName}`);
|
|
134
220
|
console.info(` npm install`);
|
|
135
|
-
console.info(` npm run
|
|
221
|
+
console.info(` npm run develop`);
|
|
136
222
|
console.info("");
|
|
137
223
|
console.info("Your app will be available at: http://localhost:7777");
|
|
138
224
|
console.info("");
|
|
@@ -145,26 +231,26 @@ async function main() {
|
|
|
145
231
|
async function createProject(config, projectPath) {
|
|
146
232
|
await mkdir(projectPath, { recursive: true });
|
|
147
233
|
await mkdir(join(projectPath, "src"), { recursive: true });
|
|
148
|
-
|
|
149
|
-
|
|
234
|
+
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
235
|
+
const startCmd = config.platform === "bun" ? "bun dist/server/supervisor.js" : "node dist/server/supervisor.js";
|
|
236
|
+
const dependencies = {
|
|
237
|
+
"@b9g/router": "^0.2.0",
|
|
238
|
+
"@b9g/shovel": "^0.2.0"
|
|
239
|
+
};
|
|
240
|
+
if (config.uiFramework === "crank") {
|
|
241
|
+
dependencies["@b9g/crank"] = "^0.7.2";
|
|
150
242
|
}
|
|
151
|
-
const ext = config.typescript ? "ts" : "js";
|
|
152
243
|
const packageJson = {
|
|
153
244
|
name: config.name,
|
|
154
245
|
private: true,
|
|
155
246
|
version: "0.0.1",
|
|
156
247
|
type: "module",
|
|
157
248
|
scripts: {
|
|
158
|
-
|
|
249
|
+
develop: `shovel develop src/app.${ext} --platform ${config.platform}`,
|
|
159
250
|
build: `shovel build src/app.${ext} --platform ${config.platform}`,
|
|
160
|
-
start:
|
|
161
|
-
},
|
|
162
|
-
dependencies: {
|
|
163
|
-
"@b9g/router": "^0.2.0",
|
|
164
|
-
"@b9g/shovel": "^0.2.0",
|
|
165
|
-
"@b9g/filesystem": "^0.1.8",
|
|
166
|
-
"@b9g/cache": "^0.2.0"
|
|
251
|
+
start: startCmd
|
|
167
252
|
},
|
|
253
|
+
dependencies,
|
|
168
254
|
devDependencies: config.typescript ? {
|
|
169
255
|
"@types/node": "^18.0.0",
|
|
170
256
|
typescript: "^5.0.0"
|
|
@@ -177,18 +263,23 @@ async function createProject(config, projectPath) {
|
|
|
177
263
|
const appFile = generateAppFile(config);
|
|
178
264
|
await writeFile(join(projectPath, `src/app.${ext}`), appFile);
|
|
179
265
|
if (config.typescript) {
|
|
266
|
+
const compilerOptions = {
|
|
267
|
+
target: "ES2022",
|
|
268
|
+
module: "ESNext",
|
|
269
|
+
moduleResolution: "bundler",
|
|
270
|
+
allowSyntheticDefaultImports: true,
|
|
271
|
+
esModuleInterop: true,
|
|
272
|
+
strict: true,
|
|
273
|
+
skipLibCheck: true,
|
|
274
|
+
forceConsistentCasingInFileNames: true,
|
|
275
|
+
lib: ["ES2022", "WebWorker"]
|
|
276
|
+
};
|
|
277
|
+
if (config.uiFramework === "crank") {
|
|
278
|
+
compilerOptions.jsx = "react-jsx";
|
|
279
|
+
compilerOptions.jsxImportSource = "@b9g/crank";
|
|
280
|
+
}
|
|
180
281
|
const tsConfig = {
|
|
181
|
-
compilerOptions
|
|
182
|
-
target: "ES2022",
|
|
183
|
-
module: "ESNext",
|
|
184
|
-
moduleResolution: "bundler",
|
|
185
|
-
allowSyntheticDefaultImports: true,
|
|
186
|
-
esModuleInterop: true,
|
|
187
|
-
strict: true,
|
|
188
|
-
skipLibCheck: true,
|
|
189
|
-
forceConsistentCasingInFileNames: true,
|
|
190
|
-
lib: ["ES2022", "WebWorker"]
|
|
191
|
-
},
|
|
282
|
+
compilerOptions,
|
|
192
283
|
include: ["src/**/*"],
|
|
193
284
|
exclude: ["node_modules", "dist"]
|
|
194
285
|
};
|
|
@@ -196,9 +287,18 @@ async function createProject(config, projectPath) {
|
|
|
196
287
|
join(projectPath, "tsconfig.json"),
|
|
197
288
|
JSON.stringify(tsConfig, null, 2)
|
|
198
289
|
);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
290
|
+
const envDts = `/// <reference lib="WebWorker" />
|
|
291
|
+
|
|
292
|
+
// Shovel runs your code in a ServiceWorker-like environment.
|
|
293
|
+
// This augments the Worker types with ServiceWorker events
|
|
294
|
+
// so self.addEventListener("fetch", ...) etc. are properly typed.
|
|
295
|
+
interface WorkerGlobalScopeEventMap {
|
|
296
|
+
fetch: FetchEvent;
|
|
297
|
+
install: ExtendableEvent;
|
|
298
|
+
activate: ExtendableEvent;
|
|
299
|
+
}
|
|
300
|
+
`;
|
|
301
|
+
await writeFile(join(projectPath, "src/env.d.ts"), envDts);
|
|
202
302
|
}
|
|
203
303
|
const readme = generateReadme(config);
|
|
204
304
|
await writeFile(join(projectPath, "README.md"), readme);
|
|
@@ -211,62 +311,6 @@ dist/
|
|
|
211
311
|
`;
|
|
212
312
|
await writeFile(join(projectPath, ".gitignore"), gitignore);
|
|
213
313
|
}
|
|
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;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
body {
|
|
240
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
241
|
-
line-height: 1.6;
|
|
242
|
-
color: #333;
|
|
243
|
-
background: #fafafa;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
main {
|
|
247
|
-
max-width: 640px;
|
|
248
|
-
margin: 4rem auto;
|
|
249
|
-
padding: 2rem;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
h1 {
|
|
253
|
-
color: #2563eb;
|
|
254
|
-
margin-bottom: 1rem;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
code {
|
|
258
|
-
background: #e5e7eb;
|
|
259
|
-
padding: 0.2rem 0.4rem;
|
|
260
|
-
border-radius: 4px;
|
|
261
|
-
font-size: 0.9em;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
a {
|
|
265
|
-
color: #2563eb;
|
|
266
|
-
}
|
|
267
|
-
`;
|
|
268
|
-
await writeFile(join(projectPath, "public/styles.css"), stylesCss);
|
|
269
|
-
}
|
|
270
314
|
function generateAppFile(config) {
|
|
271
315
|
switch (config.template) {
|
|
272
316
|
case "hello-world":
|
|
@@ -294,8 +338,10 @@ self.addEventListener("fetch", (event) => {
|
|
|
294
338
|
}
|
|
295
339
|
function generateApi(config) {
|
|
296
340
|
return `import { Router } from "@b9g/router";
|
|
341
|
+
import { logger } from "@b9g/router/middleware";
|
|
297
342
|
|
|
298
343
|
const router = new Router();
|
|
344
|
+
router.use(logger());
|
|
299
345
|
|
|
300
346
|
// In-memory data store
|
|
301
347
|
const users = [
|
|
@@ -352,62 +398,287 @@ self.addEventListener("fetch", (event) => {
|
|
|
352
398
|
});
|
|
353
399
|
`;
|
|
354
400
|
}
|
|
401
|
+
var css = ` * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
402
|
+
body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; background: #fafafa; }
|
|
403
|
+
main { max-width: 640px; margin: 4rem auto; padding: 2rem; }
|
|
404
|
+
h1 { color: #2563eb; margin-bottom: 1rem; }
|
|
405
|
+
code { background: #e5e7eb; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
406
|
+
a { color: #2563eb; }
|
|
407
|
+
ul { margin-top: 1rem; padding-left: 1.5rem; }
|
|
408
|
+
li { margin-bottom: 0.5rem; }
|
|
409
|
+
button { background: #2563eb; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
|
410
|
+
button:hover { background: #1d4ed8; }
|
|
411
|
+
#result { margin-top: 1rem; padding: 1rem; background: #f3f4f6; border-radius: 4px; }`;
|
|
355
412
|
function generateStaticSite(config) {
|
|
413
|
+
switch (config.uiFramework) {
|
|
414
|
+
case "vanilla":
|
|
415
|
+
return generateStaticSiteVanilla(config);
|
|
416
|
+
case "htmx":
|
|
417
|
+
return generateStaticSiteHtmx(config);
|
|
418
|
+
case "alpine":
|
|
419
|
+
return generateStaticSiteAlpine(config);
|
|
420
|
+
case "crank":
|
|
421
|
+
return generateStaticSiteCrank(config);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function generateStaticSiteVanilla(config) {
|
|
425
|
+
const ext = config.typescript ? "ts" : "js";
|
|
426
|
+
const t = config.typescript;
|
|
356
427
|
return `// ${config.name} - Static Site
|
|
357
|
-
//
|
|
428
|
+
// Renders HTML pages server-side
|
|
358
429
|
|
|
359
430
|
self.addEventListener("fetch", (event) => {
|
|
360
431
|
event.respondWith(handleRequest(event.request));
|
|
361
432
|
});
|
|
362
433
|
|
|
363
|
-
async function handleRequest(request${
|
|
434
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
364
435
|
const url = new URL(request.url);
|
|
365
|
-
let path = url.pathname;
|
|
366
436
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
437
|
+
if (url.pathname === "/") {
|
|
438
|
+
return new Response(renderPage("Home", \`
|
|
439
|
+
<h1>Welcome to ${config.name}</h1>
|
|
440
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
441
|
+
<p><a href="/about">About</a></p>
|
|
442
|
+
\`), {
|
|
443
|
+
headers: { "Content-Type": "text/html" },
|
|
444
|
+
});
|
|
370
445
|
}
|
|
371
446
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
headers: {
|
|
380
|
-
"Content-Type": getContentType(path),
|
|
381
|
-
},
|
|
447
|
+
if (url.pathname === "/about") {
|
|
448
|
+
return new Response(renderPage("About", \`
|
|
449
|
+
<h1>About</h1>
|
|
450
|
+
<p>This is a static site built with <strong>Shovel</strong>.</p>
|
|
451
|
+
<p><a href="/">Home</a></p>
|
|
452
|
+
\`), {
|
|
453
|
+
headers: { "Content-Type": "text/html" },
|
|
382
454
|
});
|
|
383
|
-
} catch {
|
|
384
|
-
// File not found - return 404
|
|
385
|
-
return new Response("Not Found", { status: 404 });
|
|
386
455
|
}
|
|
456
|
+
|
|
457
|
+
return new Response("Not Found", { status: 404 });
|
|
387
458
|
}
|
|
388
459
|
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
460
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
461
|
+
return \`<!DOCTYPE html>
|
|
462
|
+
<html lang="en">
|
|
463
|
+
<head>
|
|
464
|
+
<meta charset="UTF-8">
|
|
465
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
466
|
+
<title>\${title} - ${config.name}</title>
|
|
467
|
+
<style>
|
|
468
|
+
${css}
|
|
469
|
+
</style>
|
|
470
|
+
</head>
|
|
471
|
+
<body>
|
|
472
|
+
<main>\${content}</main>
|
|
473
|
+
</body>
|
|
474
|
+
</html>\`;
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
function generateStaticSiteHtmx(config) {
|
|
479
|
+
const ext = config.typescript ? "ts" : "js";
|
|
480
|
+
const t = config.typescript;
|
|
481
|
+
return `// ${config.name} - Static Site with HTMX
|
|
482
|
+
// Server-rendered HTML with HTMX interactions
|
|
483
|
+
|
|
484
|
+
self.addEventListener("fetch", (event) => {
|
|
485
|
+
event.respondWith(handleRequest(event.request));
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
489
|
+
const url = new URL(request.url);
|
|
490
|
+
|
|
491
|
+
if (url.pathname === "/") {
|
|
492
|
+
return new Response(renderPage("Home", \`
|
|
493
|
+
<h1>Welcome to ${config.name}</h1>
|
|
494
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
495
|
+
<button hx-get="/greeting" hx-target="#result" hx-swap="innerHTML">Get Greeting</button>
|
|
496
|
+
<div id="result"></div>
|
|
497
|
+
<p style="margin-top: 1rem;"><a href="/about">About</a></p>
|
|
498
|
+
\`), {
|
|
499
|
+
headers: { "Content-Type": "text/html" },
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (url.pathname === "/greeting") {
|
|
504
|
+
const hour = new Date().getHours();
|
|
505
|
+
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
|
506
|
+
return new Response(\`<p>\${greeting}! The time is \${new Date().toLocaleTimeString()}.</p>\`, {
|
|
507
|
+
headers: { "Content-Type": "text/html" },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (url.pathname === "/about") {
|
|
512
|
+
return new Response(renderPage("About", \`
|
|
513
|
+
<h1>About</h1>
|
|
514
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
|
|
515
|
+
<p><a href="/">Home</a></p>
|
|
516
|
+
\`), {
|
|
517
|
+
headers: { "Content-Type": "text/html" },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return new Response("Not Found", { status: 404 });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
525
|
+
return \`<!DOCTYPE html>
|
|
526
|
+
<html lang="en">
|
|
527
|
+
<head>
|
|
528
|
+
<meta charset="UTF-8">
|
|
529
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
530
|
+
<title>\${title} - ${config.name}</title>
|
|
531
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
532
|
+
<style>
|
|
533
|
+
${css}
|
|
534
|
+
</style>
|
|
535
|
+
</head>
|
|
536
|
+
<body>
|
|
537
|
+
<main>\${content}</main>
|
|
538
|
+
</body>
|
|
539
|
+
</html>\`;
|
|
540
|
+
}
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
function generateStaticSiteAlpine(config) {
|
|
544
|
+
const ext = config.typescript ? "ts" : "js";
|
|
545
|
+
const t = config.typescript;
|
|
546
|
+
return `// ${config.name} - Static Site with Alpine.js
|
|
547
|
+
// Server-rendered HTML with Alpine.js interactions
|
|
548
|
+
|
|
549
|
+
self.addEventListener("fetch", (event) => {
|
|
550
|
+
event.respondWith(handleRequest(event.request));
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
554
|
+
const url = new URL(request.url);
|
|
555
|
+
|
|
556
|
+
if (url.pathname === "/") {
|
|
557
|
+
return new Response(renderPage("Home", \`
|
|
558
|
+
<h1>Welcome to ${config.name}</h1>
|
|
559
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
560
|
+
<div x-data="{ count: 0 }">
|
|
561
|
+
<button @click="count++">Clicked: <span x-text="count"></span></button>
|
|
562
|
+
</div>
|
|
563
|
+
<p style="margin-top: 1rem;"><a href="/about">About</a></p>
|
|
564
|
+
\`), {
|
|
565
|
+
headers: { "Content-Type": "text/html" },
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (url.pathname === "/about") {
|
|
570
|
+
return new Response(renderPage("About", \`
|
|
571
|
+
<h1>About</h1>
|
|
572
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
|
|
573
|
+
<p><a href="/">Home</a></p>
|
|
574
|
+
\`), {
|
|
575
|
+
headers: { "Content-Type": "text/html" },
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return new Response("Not Found", { status: 404 });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
583
|
+
return \`<!DOCTYPE html>
|
|
584
|
+
<html lang="en">
|
|
585
|
+
<head>
|
|
586
|
+
<meta charset="UTF-8">
|
|
587
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
588
|
+
<title>\${title} - ${config.name}</title>
|
|
589
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
|
590
|
+
<style>
|
|
591
|
+
${css}
|
|
592
|
+
</style>
|
|
593
|
+
</head>
|
|
594
|
+
<body>
|
|
595
|
+
<main>\${content}</main>
|
|
596
|
+
</body>
|
|
597
|
+
</html>\`;
|
|
598
|
+
}
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
function generateStaticSiteCrank(config) {
|
|
602
|
+
const t = config.typescript;
|
|
603
|
+
return `import {renderer} from "@b9g/crank/html";
|
|
604
|
+
|
|
605
|
+
// ${config.name} - Static Site with Crank.js
|
|
606
|
+
// Server-rendered HTML with JSX components
|
|
607
|
+
|
|
608
|
+
const css = \`
|
|
609
|
+
${css}
|
|
610
|
+
\`;
|
|
611
|
+
|
|
612
|
+
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
613
|
+
return (
|
|
614
|
+
<html lang="en">
|
|
615
|
+
<head>
|
|
616
|
+
<meta charset="UTF-8" />
|
|
617
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
618
|
+
<title>{title} - ${config.name}</title>
|
|
619
|
+
<style>{css}</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<main>{children}</main>
|
|
623
|
+
</body>
|
|
624
|
+
</html>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
self.addEventListener("fetch", (event) => {
|
|
629
|
+
event.respondWith(handleRequest(event.request));
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
633
|
+
const url = new URL(request.url);
|
|
634
|
+
let html${t ? ": string" : ""};
|
|
635
|
+
|
|
636
|
+
if (url.pathname === "/") {
|
|
637
|
+
html = await renderer.render(
|
|
638
|
+
<Page title="Home">
|
|
639
|
+
<h1>Welcome to ${config.name}</h1>
|
|
640
|
+
<p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
|
|
641
|
+
<p><a href="/about">About</a></p>
|
|
642
|
+
</Page>
|
|
643
|
+
);
|
|
644
|
+
} else if (url.pathname === "/about") {
|
|
645
|
+
html = await renderer.render(
|
|
646
|
+
<Page title="About">
|
|
647
|
+
<h1>About</h1>
|
|
648
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
649
|
+
<p><a href="/">Home</a></p>
|
|
650
|
+
</Page>
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
return new Response("Not Found", { status: 404 });
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
657
|
+
headers: { "Content-Type": "text/html" },
|
|
658
|
+
});
|
|
404
659
|
}
|
|
405
660
|
`;
|
|
406
661
|
}
|
|
407
662
|
function generateFullStack(config) {
|
|
663
|
+
switch (config.uiFramework) {
|
|
664
|
+
case "vanilla":
|
|
665
|
+
return generateFullStackVanilla(config);
|
|
666
|
+
case "htmx":
|
|
667
|
+
return generateFullStackHtmx(config);
|
|
668
|
+
case "alpine":
|
|
669
|
+
return generateFullStackAlpine(config);
|
|
670
|
+
case "crank":
|
|
671
|
+
return generateFullStackCrank(config);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function generateFullStackVanilla(config) {
|
|
675
|
+
const ext = config.typescript ? "ts" : "js";
|
|
676
|
+
const t = config.typescript;
|
|
408
677
|
return `import { Router } from "@b9g/router";
|
|
678
|
+
import { logger } from "@b9g/router/middleware";
|
|
409
679
|
|
|
410
680
|
const router = new Router();
|
|
681
|
+
router.use(logger());
|
|
411
682
|
|
|
412
683
|
// API routes
|
|
413
684
|
router.route("/api/hello").get(() => {
|
|
@@ -422,58 +693,278 @@ router.route("/api/echo").post(async (req) => {
|
|
|
422
693
|
return Response.json({ echo: body });
|
|
423
694
|
});
|
|
424
695
|
|
|
425
|
-
//
|
|
426
|
-
router.route("
|
|
427
|
-
|
|
428
|
-
|
|
696
|
+
// HTML pages
|
|
697
|
+
router.route("/").get(() => {
|
|
698
|
+
return new Response(renderPage("Home", \`
|
|
699
|
+
<h1>Welcome to ${config.name}</h1>
|
|
700
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
701
|
+
<ul>
|
|
702
|
+
<li><a href="/about">About</a></li>
|
|
703
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
704
|
+
</ul>
|
|
705
|
+
\`), {
|
|
706
|
+
headers: { "Content-Type": "text/html" },
|
|
707
|
+
});
|
|
708
|
+
});
|
|
429
709
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
710
|
+
router.route("/about").get(() => {
|
|
711
|
+
return new Response(renderPage("About", \`
|
|
712
|
+
<h1>About</h1>
|
|
713
|
+
<p>This is a full-stack app built with <strong>Shovel</strong>.</p>
|
|
714
|
+
<p><a href="/">Home</a></p>
|
|
715
|
+
\`), {
|
|
716
|
+
headers: { "Content-Type": "text/html" },
|
|
717
|
+
});
|
|
718
|
+
});
|
|
434
719
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
720
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
721
|
+
return \`<!DOCTYPE html>
|
|
722
|
+
<html lang="en">
|
|
723
|
+
<head>
|
|
724
|
+
<meta charset="UTF-8">
|
|
725
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
726
|
+
<title>\${title} - ${config.name}</title>
|
|
727
|
+
<style>
|
|
728
|
+
${css}
|
|
729
|
+
</style>
|
|
730
|
+
</head>
|
|
731
|
+
<body>
|
|
732
|
+
<main>\${content}</main>
|
|
733
|
+
</body>
|
|
734
|
+
</html>\`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
self.addEventListener("fetch", (event) => {
|
|
738
|
+
event.respondWith(router.handle(event.request));
|
|
739
|
+
});
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
function generateFullStackHtmx(config) {
|
|
743
|
+
const ext = config.typescript ? "ts" : "js";
|
|
744
|
+
const t = config.typescript;
|
|
745
|
+
return `import { Router } from "@b9g/router";
|
|
746
|
+
import { logger } from "@b9g/router/middleware";
|
|
747
|
+
|
|
748
|
+
const router = new Router();
|
|
749
|
+
router.use(logger());
|
|
750
|
+
|
|
751
|
+
// API routes \u2014 return HTML fragments when HTMX requests, JSON otherwise
|
|
752
|
+
router.route("/api/hello").get((req) => {
|
|
753
|
+
const data = {
|
|
754
|
+
message: "Hello from the API!",
|
|
755
|
+
timestamp: new Date().toISOString(),
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
if (req.headers.get("HX-Request")) {
|
|
759
|
+
return new Response(\`<p>\${data.message}</p><p><small>\${data.timestamp}</small></p>\`, {
|
|
760
|
+
headers: { "Content-Type": "text/html" },
|
|
444
761
|
});
|
|
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
762
|
}
|
|
763
|
+
return Response.json(data);
|
|
458
764
|
});
|
|
459
765
|
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
766
|
+
router.route("/api/echo").post(async (req) => {
|
|
767
|
+
const body = await req.json();
|
|
768
|
+
return Response.json({ echo: body });
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// HTML pages
|
|
772
|
+
router.route("/").get(() => {
|
|
773
|
+
return new Response(renderPage("Home", \`
|
|
774
|
+
<h1>Welcome to ${config.name}</h1>
|
|
775
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
776
|
+
<button hx-get="/api/hello" hx-target="#result" hx-swap="innerHTML">Call API</button>
|
|
777
|
+
<div id="result"></div>
|
|
778
|
+
<ul>
|
|
779
|
+
<li><a href="/about">About</a></li>
|
|
780
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
781
|
+
</ul>
|
|
782
|
+
\`), {
|
|
783
|
+
headers: { "Content-Type": "text/html" },
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
router.route("/about").get(() => {
|
|
788
|
+
return new Response(renderPage("About", \`
|
|
789
|
+
<h1>About</h1>
|
|
790
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
|
|
791
|
+
<p><a href="/">Home</a></p>
|
|
792
|
+
\`), {
|
|
793
|
+
headers: { "Content-Type": "text/html" },
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
798
|
+
return \`<!DOCTYPE html>
|
|
799
|
+
<html lang="en">
|
|
800
|
+
<head>
|
|
801
|
+
<meta charset="UTF-8">
|
|
802
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
803
|
+
<title>\${title} - ${config.name}</title>
|
|
804
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
805
|
+
<style>
|
|
806
|
+
${css}
|
|
807
|
+
</style>
|
|
808
|
+
</head>
|
|
809
|
+
<body>
|
|
810
|
+
<main>\${content}</main>
|
|
811
|
+
</body>
|
|
812
|
+
</html>\`;
|
|
475
813
|
}
|
|
476
814
|
|
|
815
|
+
self.addEventListener("fetch", (event) => {
|
|
816
|
+
event.respondWith(router.handle(event.request));
|
|
817
|
+
});
|
|
818
|
+
`;
|
|
819
|
+
}
|
|
820
|
+
function generateFullStackAlpine(config) {
|
|
821
|
+
const ext = config.typescript ? "ts" : "js";
|
|
822
|
+
const t = config.typescript;
|
|
823
|
+
return `import { Router } from "@b9g/router";
|
|
824
|
+
import { logger } from "@b9g/router/middleware";
|
|
825
|
+
|
|
826
|
+
const router = new Router();
|
|
827
|
+
router.use(logger());
|
|
828
|
+
|
|
829
|
+
// API routes
|
|
830
|
+
router.route("/api/hello").get(() => {
|
|
831
|
+
return Response.json({
|
|
832
|
+
message: "Hello from the API!",
|
|
833
|
+
timestamp: new Date().toISOString(),
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
router.route("/api/echo").post(async (req) => {
|
|
838
|
+
const body = await req.json();
|
|
839
|
+
return Response.json({ echo: body });
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// HTML pages
|
|
843
|
+
router.route("/").get(() => {
|
|
844
|
+
return new Response(renderPage("Home", \`
|
|
845
|
+
<h1>Welcome to ${config.name}</h1>
|
|
846
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
847
|
+
<div x-data="{ result: null }">
|
|
848
|
+
<button @click="fetch('/api/hello').then(r => r.json()).then(d => result = d)">Call API</button>
|
|
849
|
+
<div id="result" x-show="result">
|
|
850
|
+
<p x-text="result?.message"></p>
|
|
851
|
+
<p><small x-text="result?.timestamp"></small></p>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
<ul>
|
|
855
|
+
<li><a href="/about">About</a></li>
|
|
856
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
857
|
+
</ul>
|
|
858
|
+
\`), {
|
|
859
|
+
headers: { "Content-Type": "text/html" },
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
router.route("/about").get(() => {
|
|
864
|
+
return new Response(renderPage("About", \`
|
|
865
|
+
<h1>About</h1>
|
|
866
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
|
|
867
|
+
<p><a href="/">Home</a></p>
|
|
868
|
+
\`), {
|
|
869
|
+
headers: { "Content-Type": "text/html" },
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
874
|
+
return \`<!DOCTYPE html>
|
|
875
|
+
<html lang="en">
|
|
876
|
+
<head>
|
|
877
|
+
<meta charset="UTF-8">
|
|
878
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
879
|
+
<title>\${title} - ${config.name}</title>
|
|
880
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
|
881
|
+
<style>
|
|
882
|
+
${css}
|
|
883
|
+
</style>
|
|
884
|
+
</head>
|
|
885
|
+
<body>
|
|
886
|
+
<main>\${content}</main>
|
|
887
|
+
</body>
|
|
888
|
+
</html>\`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
self.addEventListener("fetch", (event) => {
|
|
892
|
+
event.respondWith(router.handle(event.request));
|
|
893
|
+
});
|
|
894
|
+
`;
|
|
895
|
+
}
|
|
896
|
+
function generateFullStackCrank(config) {
|
|
897
|
+
const t = config.typescript;
|
|
898
|
+
return `import { Router } from "@b9g/router";
|
|
899
|
+
import { logger } from "@b9g/router/middleware";
|
|
900
|
+
import {renderer} from "@b9g/crank/html";
|
|
901
|
+
|
|
902
|
+
const router = new Router();
|
|
903
|
+
router.use(logger());
|
|
904
|
+
|
|
905
|
+
const css = \`
|
|
906
|
+
${css}
|
|
907
|
+
\`;
|
|
908
|
+
|
|
909
|
+
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
910
|
+
return (
|
|
911
|
+
<html lang="en">
|
|
912
|
+
<head>
|
|
913
|
+
<meta charset="UTF-8" />
|
|
914
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
915
|
+
<title>{title} - ${config.name}</title>
|
|
916
|
+
<style>{css}</style>
|
|
917
|
+
</head>
|
|
918
|
+
<body>
|
|
919
|
+
<main>{children}</main>
|
|
920
|
+
</body>
|
|
921
|
+
</html>
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// API routes
|
|
926
|
+
router.route("/api/hello").get(() => {
|
|
927
|
+
return Response.json({
|
|
928
|
+
message: "Hello from the API!",
|
|
929
|
+
timestamp: new Date().toISOString(),
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
router.route("/api/echo").post(async (req) => {
|
|
934
|
+
const body = await req.json();
|
|
935
|
+
return Response.json({ echo: body });
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// HTML pages
|
|
939
|
+
router.route("/").get(async () => {
|
|
940
|
+
const html = await renderer.render(
|
|
941
|
+
<Page title="Home">
|
|
942
|
+
<h1>Welcome to ${config.name}</h1>
|
|
943
|
+
<p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
|
|
944
|
+
<ul>
|
|
945
|
+
<li><a href="/about">About</a></li>
|
|
946
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
947
|
+
</ul>
|
|
948
|
+
</Page>
|
|
949
|
+
);
|
|
950
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
951
|
+
headers: { "Content-Type": "text/html" },
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
router.route("/about").get(async () => {
|
|
956
|
+
const html = await renderer.render(
|
|
957
|
+
<Page title="About">
|
|
958
|
+
<h1>About</h1>
|
|
959
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
960
|
+
<p><a href="/">Home</a></p>
|
|
961
|
+
</Page>
|
|
962
|
+
);
|
|
963
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
964
|
+
headers: { "Content-Type": "text/html" },
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
477
968
|
self.addEventListener("fetch", (event) => {
|
|
478
969
|
event.respondWith(router.handle(event.request));
|
|
479
970
|
});
|
|
@@ -483,25 +974,32 @@ function generateReadme(config) {
|
|
|
483
974
|
const templateDescriptions = {
|
|
484
975
|
"hello-world": "A minimal Shovel application",
|
|
485
976
|
api: "A REST API with JSON endpoints",
|
|
486
|
-
"static-site": "A static
|
|
487
|
-
"full-stack": "A full-stack app with
|
|
977
|
+
"static-site": "A static site with server-rendered pages",
|
|
978
|
+
"full-stack": "A full-stack app with HTML pages and API routes"
|
|
979
|
+
};
|
|
980
|
+
const frameworkDescriptions = {
|
|
981
|
+
vanilla: "",
|
|
982
|
+
htmx: " using [HTMX](https://htmx.org)",
|
|
983
|
+
alpine: " using [Alpine.js](https://alpinejs.dev)",
|
|
984
|
+
crank: " using [Crank.js](https://crank.js.org)"
|
|
488
985
|
};
|
|
986
|
+
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
489
987
|
return `# ${config.name}
|
|
490
988
|
|
|
491
|
-
${templateDescriptions[config.template]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
989
|
+
${templateDescriptions[config.template]}${frameworkDescriptions[config.uiFramework]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
492
990
|
|
|
493
991
|
## Getting Started
|
|
494
992
|
|
|
495
993
|
\`\`\`bash
|
|
496
994
|
npm install
|
|
497
|
-
npm run
|
|
995
|
+
npm run develop
|
|
498
996
|
\`\`\`
|
|
499
997
|
|
|
500
998
|
Open http://localhost:7777
|
|
501
999
|
|
|
502
1000
|
## Scripts
|
|
503
1001
|
|
|
504
|
-
- \`npm run
|
|
1002
|
+
- \`npm run develop\` - Start development server
|
|
505
1003
|
- \`npm run build\` - Build for production
|
|
506
1004
|
- \`npm start\` - Run production build
|
|
507
1005
|
|
|
@@ -510,8 +1008,8 @@ Open http://localhost:7777
|
|
|
510
1008
|
\`\`\`
|
|
511
1009
|
${config.name}/
|
|
512
1010
|
\u251C\u2500\u2500 src/
|
|
513
|
-
\u2502 \u2514\u2500\u2500 app.${
|
|
514
|
-
|
|
1011
|
+
\u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
|
|
1012
|
+
\u251C\u2500\u2500 package.json
|
|
515
1013
|
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md
|
|
516
1014
|
\`\`\`
|
|
517
1015
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/shovel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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": {
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@b9g/async-context": "^0.2.1",
|
|
18
|
-
"@b9g/cache": "^0.2.
|
|
19
|
-
"@b9g/filesystem": "^0.1.
|
|
18
|
+
"@b9g/cache": "^0.2.2",
|
|
19
|
+
"@b9g/filesystem": "^0.1.10",
|
|
20
20
|
"@b9g/http-errors": "^0.2.1",
|
|
21
21
|
"@b9g/node-webworker": "^0.2.1",
|
|
22
|
-
"@b9g/platform": "^0.1.
|
|
23
|
-
"@b9g/platform-bun": "^0.1.
|
|
24
|
-
"@b9g/platform-cloudflare": "^0.1.
|
|
25
|
-
"@b9g/platform-node": "^0.1.
|
|
22
|
+
"@b9g/platform": "^0.1.17",
|
|
23
|
+
"@b9g/platform-bun": "^0.1.15",
|
|
24
|
+
"@b9g/platform-cloudflare": "^0.1.15",
|
|
25
|
+
"@b9g/platform-node": "^0.1.17",
|
|
26
26
|
"@clack/prompts": "^0.7.0",
|
|
27
27
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
28
28
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
@@ -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.
|
|
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",
|