@b9g/shovel 0.2.3 → 0.2.6
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 +22 -0
- package/LICENSE +1 -1
- package/README.md +60 -56
- package/bin/cli.js +11 -3
- package/bin/create.js +607 -170
- package/package.json +17 -20
- package/src/_chunks/{build-BM4A74RI.js → build-KBQU2OA7.js} +2 -2
- package/src/_chunks/{chunk-NZVIBZYG.js → chunk-7GONPLNW.js} +1 -1
- package/src/_chunks/{chunk-WVHECOTO.js → chunk-ABGHNBNM.js} +1 -1
- package/src/_chunks/{develop-6DPQE5H4.js → develop-JUQG2G7M.js} +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Shovel will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.6] - 2026-02-06
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **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))
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **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))
|
|
16
|
+
- **Direct cache in single-worker dev mode** - Dev workers now use `MemoryCache` directly instead of `PostMessageCache` when `workers: 1`, avoiding unnecessary serialization overhead
|
|
17
|
+
- **Node.js Request body duplex** - Added `duplex: "half"` to Node.js Request construction to fix body streaming
|
|
18
|
+
- **Website 404 errors** - Views now throw `NotFound` from `@b9g/http-errors` instead of raw errors, returning proper 404 responses
|
|
19
|
+
- **Fixed `@b9g/cache-redis` module path in docs** - Documentation referenced `@b9g/cache/redis` instead of the correct `@b9g/cache-redis`
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- **PostMessageCache WPT tests** - 29 Web Platform Tests now run against PostMessageCache to verify serialization round-trip compliance
|
|
24
|
+
- **Pattern matching unit tests** - Wildcard, prefix, and exact-match priority tests for cache and directory factories
|
|
25
|
+
- **End-to-end cache tests** - Runtime tests for KV server, multi-cache independence, and wildcard priority
|
|
26
|
+
|
|
5
27
|
## [0.2.3] - 2026-02-02
|
|
6
28
|
|
|
7
29
|
### Features
|
package/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2026 Brian Kim
|
|
2
2
|
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
4
|
|
package/README.md
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
# Shovel.js
|
|
1
|
+
# Shovel.js
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Run Service Workers anywhere.**
|
|
4
4
|
|
|
5
|
-
Shovel is a
|
|
5
|
+
Shovel is a meta-framework for building server applications using the ServiceWorker API. Write once, deploy to Node.js, Bun, or Cloudflare Workers.
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
//
|
|
7
|
+
```typescript
|
|
8
|
+
// server.ts
|
|
9
9
|
import {Router} from "@b9g/router";
|
|
10
|
+
|
|
10
11
|
const router = new Router();
|
|
11
12
|
|
|
12
|
-
router.route("/")
|
|
13
|
+
router.route("/kv/:key")
|
|
14
|
+
.get(async (req, ctx) => {
|
|
15
|
+
const cache = await self.caches.open("kv");
|
|
16
|
+
const cached = await cache.match(ctx.params.key);
|
|
17
|
+
return cached ?? new Response(null, {status: 404});
|
|
18
|
+
})
|
|
19
|
+
.put(async (req, ctx) => {
|
|
20
|
+
const cache = await self.caches.open("kv");
|
|
21
|
+
await cache.put(ctx.params.key, new Response(await req.text()));
|
|
22
|
+
return new Response(null, {status: 201});
|
|
23
|
+
})
|
|
24
|
+
.delete(async (req, ctx) => {
|
|
25
|
+
const cache = await self.caches.open("kv");
|
|
26
|
+
await cache.delete(ctx.params.key);
|
|
27
|
+
return new Response(null, {status: 204});
|
|
28
|
+
});
|
|
13
29
|
|
|
14
30
|
self.addEventListener("fetch", (ev) => {
|
|
15
31
|
ev.respondWith(router.handle(ev.request));
|
|
@@ -17,30 +33,20 @@ self.addEventListener("fetch", (ev) => {
|
|
|
17
33
|
```
|
|
18
34
|
|
|
19
35
|
```bash
|
|
20
|
-
shovel develop
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
36
|
+
$ shovel develop server.ts
|
|
37
|
+
listening on http://localhost:7777
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
// src/server.js
|
|
26
|
-
import {Router} from "@b9g/router";
|
|
39
|
+
$ curl -X PUT :7777/kv/hello -d "world"
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
router.route("/").get(() => new Response("Hello World"));
|
|
31
|
-
|
|
32
|
-
router.route("/greet/:name").get((request, {params}) => {
|
|
33
|
-
return new Response(`Hello ${params.name}`);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
self.addEventListener("fetch", (event) => {
|
|
37
|
-
event.respondWith(router.handle(event.request));
|
|
38
|
-
});
|
|
41
|
+
$ curl :7777/kv/hello
|
|
42
|
+
world
|
|
39
43
|
```
|
|
40
44
|
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
41
47
|
```bash
|
|
42
48
|
# Create a new project
|
|
43
|
-
npm create
|
|
49
|
+
npm create shovel my-app
|
|
44
50
|
|
|
45
51
|
# Development with hot reload
|
|
46
52
|
npx @b9g/shovel develop src/server.ts
|
|
@@ -51,19 +57,23 @@ npx @b9g/shovel build src/server.ts --platform=bun
|
|
|
51
57
|
npx @b9g/shovel build src/server.ts --platform=cloudflare
|
|
52
58
|
```
|
|
53
59
|
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
Visit [shovel.js.org](https://shovel.js.org) for guides and API reference.
|
|
54
63
|
|
|
55
64
|
## Web Standards
|
|
65
|
+
|
|
56
66
|
Shovel is obsessively standards-first. All Shovel APIs use web standards, and Shovel implements/shims useful standards when they're missing.
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
| API | Standard | Purpose |
|
|
69
|
+
|-----|----------|---------|
|
|
70
|
+
| `fetch()` | [Fetch](https://fetch.spec.whatwg.org) | Networking |
|
|
71
|
+
| `install`, `activate`, `fetch` events | [Service Workers](https://w3c.github.io/ServiceWorker/) | Server lifecycle |
|
|
72
|
+
| `AsyncContext.Variable` | [TC39 Stage 2](https://github.com/tc39/proposal-async-context) | Request-scoped state |
|
|
73
|
+
| `self.caches` | [Cache API](https://w3c.github.io/ServiceWorker/#cache-interface) | Response caching |
|
|
74
|
+
| `self.directories` | [FileSystem API](https://fs.spec.whatwg.org/) | Storage (local, S3, R2) |
|
|
75
|
+
| `self.cookieStore` | [CookieStore API](https://cookiestore.spec.whatwg.org) | Cookie management |
|
|
76
|
+
| `URLPattern` | [URLPattern](https://urlpattern.spec.whatwg.org/) | Route matching |
|
|
67
77
|
|
|
68
78
|
Your code uses standards. Shovel makes them work everywhere.
|
|
69
79
|
|
|
@@ -87,7 +97,7 @@ The core abstraction is the **ServiceWorker-style storage pattern**. Globals pro
|
|
|
87
97
|
const cache = await self.caches.open("sessions"); // Cache API
|
|
88
98
|
const dir = await self.directories.open("uploads"); // FileSystem API
|
|
89
99
|
const db = self.databases.get("main"); // Zen DB (opened on activate)
|
|
90
|
-
const logger = self.loggers.get(["app", "requests"]);
|
|
100
|
+
const logger = self.loggers.get(["app", "requests"]); // LogTape
|
|
91
101
|
```
|
|
92
102
|
|
|
93
103
|
Each storage type is:
|
|
@@ -168,27 +178,25 @@ Shovel's configuration follows these principles:
|
|
|
168
178
|
|
|
169
179
|
```json
|
|
170
180
|
{
|
|
171
|
-
"port": "PORT ||
|
|
172
|
-
"host": "HOST || localhost",
|
|
173
|
-
"workers": "WORKERS ?? 1",
|
|
181
|
+
"port": "$PORT || 7777",
|
|
182
|
+
"host": "$HOST || localhost",
|
|
183
|
+
"workers": "$WORKERS ?? 1",
|
|
174
184
|
"caches": {
|
|
175
185
|
"sessions": {
|
|
176
186
|
"module": "@b9g/cache-redis",
|
|
177
|
-
"
|
|
178
|
-
"url": "REDIS_URL"
|
|
187
|
+
"url": "$REDIS_URL"
|
|
179
188
|
}
|
|
180
189
|
},
|
|
181
190
|
"directories": {
|
|
182
191
|
"uploads": {
|
|
183
192
|
"module": "@b9g/filesystem-s3",
|
|
184
|
-
"
|
|
185
|
-
"bucket": "S3_BUCKET"
|
|
193
|
+
"bucket": "$S3_BUCKET"
|
|
186
194
|
}
|
|
187
195
|
},
|
|
188
196
|
"databases": {
|
|
189
197
|
"main": {
|
|
190
198
|
"module": "@b9g/zen/bun",
|
|
191
|
-
"url": "DATABASE_URL"
|
|
199
|
+
"url": "$DATABASE_URL"
|
|
192
200
|
}
|
|
193
201
|
},
|
|
194
202
|
"logging": {
|
|
@@ -201,19 +209,17 @@ Shovel's configuration follows these principles:
|
|
|
201
209
|
|
|
202
210
|
### Caches
|
|
203
211
|
|
|
204
|
-
Configure cache backends using `module`
|
|
212
|
+
Configure cache backends using `module` (uses default export, or specify `export` for named exports):
|
|
205
213
|
|
|
206
214
|
```json
|
|
207
215
|
{
|
|
208
216
|
"caches": {
|
|
209
217
|
"api-responses": {
|
|
210
|
-
"module": "@b9g/cache/memory"
|
|
211
|
-
"export": "MemoryCache"
|
|
218
|
+
"module": "@b9g/cache/memory"
|
|
212
219
|
},
|
|
213
220
|
"sessions": {
|
|
214
221
|
"module": "@b9g/cache-redis",
|
|
215
|
-
"
|
|
216
|
-
"url": "REDIS_URL"
|
|
222
|
+
"url": "$REDIS_URL"
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
}
|
|
@@ -232,13 +238,11 @@ Configure directory backends. Platforms provide defaults for well-known director
|
|
|
232
238
|
"directories": {
|
|
233
239
|
"uploads": {
|
|
234
240
|
"module": "@b9g/filesystem-s3",
|
|
235
|
-
"export": "S3Directory",
|
|
236
241
|
"bucket": "MY_BUCKET",
|
|
237
242
|
"region": "us-east-1"
|
|
238
243
|
},
|
|
239
244
|
"data": {
|
|
240
245
|
"module": "@b9g/filesystem/node-fs",
|
|
241
|
-
"export": "NodeFSDirectory",
|
|
242
246
|
"path": "./data"
|
|
243
247
|
}
|
|
244
248
|
}
|
|
@@ -292,7 +296,7 @@ Configure database drivers using the same `module`/`export` pattern:
|
|
|
292
296
|
"databases": {
|
|
293
297
|
"main": {
|
|
294
298
|
"module": "@b9g/zen/bun",
|
|
295
|
-
"url": "DATABASE_URL"
|
|
299
|
+
"url": "$DATABASE_URL"
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
302
|
}
|
|
@@ -359,7 +363,7 @@ $DATADIR/uploads → joins env var with path segment
|
|
|
359
363
|
|
|
360
364
|
```json
|
|
361
365
|
{
|
|
362
|
-
"port": "$PORT ||
|
|
366
|
+
"port": "$PORT || 7777",
|
|
363
367
|
"host": "$HOST || 0.0.0.0",
|
|
364
368
|
"directories": {
|
|
365
369
|
"server": { "path": "[outdir]/server" },
|
|
@@ -388,17 +392,17 @@ console.log(config.port); // Resolved value
|
|
|
388
392
|
| Package | Description |
|
|
389
393
|
|---------|-------------|
|
|
390
394
|
| `@b9g/shovel` | CLI for development and deployment |
|
|
391
|
-
| `@b9g/platform` | Core runtime and platform APIs |
|
|
392
|
-
| `@b9g/platform-node` | Node.js adapter |
|
|
393
|
-
| `@b9g/platform-bun` | Bun.js adapter |
|
|
394
|
-
| `@b9g/platform-cloudflare` | Cloudflare Workers adapter |
|
|
395
395
|
| `@b9g/router` | URLPattern-based routing with middleware |
|
|
396
396
|
| `@b9g/cache` | Cache API implementation |
|
|
397
397
|
| `@b9g/filesystem` | File System Access implementation |
|
|
398
|
-
| `@b9g/match-pattern` | URLPattern with extensions (100% WPT) |
|
|
399
398
|
| `@b9g/async-context` | AsyncContext.Variable implementation |
|
|
400
399
|
| `@b9g/http-errors` | Standard HTTP error classes |
|
|
401
400
|
| `@b9g/assets` | Static asset handling |
|
|
401
|
+
| `@b9g/platform` | Core runtime and platform APIs |
|
|
402
|
+
| `@b9g/platform-node` | Node.js adapter |
|
|
403
|
+
| `@b9g/platform-bun` | Bun adapter |
|
|
404
|
+
| `@b9g/platform-cloudflare` | Cloudflare Workers adapter |
|
|
405
|
+
| `@b9g/match-pattern` | URLPattern with extensions (100% WPT) |
|
|
402
406
|
|
|
403
407
|
## License
|
|
404
408
|
|
package/bin/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
DEFAULTS,
|
|
6
6
|
findProjectRoot,
|
|
7
7
|
loadConfig
|
|
8
|
-
} from "../src/_chunks/chunk-
|
|
8
|
+
} from "../src/_chunks/chunk-7GONPLNW.js";
|
|
9
9
|
|
|
10
10
|
// bin/cli.ts
|
|
11
11
|
import { resolve, relative } from "path";
|
|
@@ -75,15 +75,23 @@ 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-JUQG2G7M.js");
|
|
79
79
|
await developCommand(entrypoint, options, config);
|
|
80
80
|
});
|
|
81
|
+
program.command("create [name]").description("Create a new Shovel project").action(async (name) => {
|
|
82
|
+
if (name) {
|
|
83
|
+
process.argv = [process.argv[0], process.argv[1], name];
|
|
84
|
+
} else {
|
|
85
|
+
process.argv = [process.argv[0], process.argv[1]];
|
|
86
|
+
}
|
|
87
|
+
await import("./create.js");
|
|
88
|
+
});
|
|
81
89
|
program.command("build <entrypoint>").description("Build app for production").option("--platform <name>", "Runtime platform (node, cloudflare, bun)").option(
|
|
82
90
|
"--lifecycle [stage]",
|
|
83
91
|
"Run ServiceWorker lifecycle after build (install or activate, default: activate)"
|
|
84
92
|
).action(async (entrypoint, options) => {
|
|
85
93
|
checkPlatformReexec(options);
|
|
86
|
-
const { buildCommand } = await import("../src/_chunks/build-
|
|
94
|
+
const { buildCommand } = await import("../src/_chunks/build-KBQU2OA7.js");
|
|
87
95
|
await buildCommand(entrypoint, options, config);
|
|
88
96
|
process.exit(0);
|
|
89
97
|
});
|
package/bin/create.js
CHANGED
|
@@ -68,12 +68,12 @@ async function main() {
|
|
|
68
68
|
{
|
|
69
69
|
value: "static-site",
|
|
70
70
|
label: "Static Site",
|
|
71
|
-
hint: "
|
|
71
|
+
hint: "Server-rendered HTML pages"
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
value: "full-stack",
|
|
75
75
|
label: "Full Stack",
|
|
76
|
-
hint: "HTML pages + API routes
|
|
76
|
+
hint: "HTML pages + API routes"
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
79
|
});
|
|
@@ -81,6 +81,40 @@ async function main() {
|
|
|
81
81
|
outro("Project creation cancelled");
|
|
82
82
|
process.exit(0);
|
|
83
83
|
}
|
|
84
|
+
let uiFramework = "vanilla";
|
|
85
|
+
if (template === "static-site" || template === "full-stack") {
|
|
86
|
+
const framework = await select({
|
|
87
|
+
message: "UI framework:",
|
|
88
|
+
initialValue: "crank",
|
|
89
|
+
options: [
|
|
90
|
+
{
|
|
91
|
+
value: "alpine",
|
|
92
|
+
label: "Alpine.js",
|
|
93
|
+
hint: "Lightweight reactivity with x-data directives"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
value: "crank",
|
|
97
|
+
label: "Crank.js",
|
|
98
|
+
hint: "JSX components rendered on the server"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
value: "htmx",
|
|
102
|
+
label: "HTMX",
|
|
103
|
+
hint: "HTML-driven interactions with hx- attributes"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
value: "vanilla",
|
|
107
|
+
label: "Vanilla",
|
|
108
|
+
hint: "Plain HTML, no framework"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
if (typeof framework === "symbol") {
|
|
113
|
+
outro("Project creation cancelled");
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
uiFramework = framework;
|
|
117
|
+
}
|
|
84
118
|
const typescript = await confirm({
|
|
85
119
|
message: "Use TypeScript?",
|
|
86
120
|
initialValue: true
|
|
@@ -119,7 +153,8 @@ async function main() {
|
|
|
119
153
|
name: projectName,
|
|
120
154
|
platform,
|
|
121
155
|
template,
|
|
122
|
-
typescript
|
|
156
|
+
typescript,
|
|
157
|
+
uiFramework
|
|
123
158
|
};
|
|
124
159
|
const s = spinner();
|
|
125
160
|
s.start("Creating your Shovel project...");
|
|
@@ -132,9 +167,9 @@ async function main() {
|
|
|
132
167
|
console.info("Next steps:");
|
|
133
168
|
console.info(` cd ${projectName}`);
|
|
134
169
|
console.info(` npm install`);
|
|
135
|
-
console.info(` npm run
|
|
170
|
+
console.info(` npm run develop`);
|
|
136
171
|
console.info("");
|
|
137
|
-
console.info("Your app will be available at: http://localhost:
|
|
172
|
+
console.info("Your app will be available at: http://localhost:7777");
|
|
138
173
|
console.info("");
|
|
139
174
|
} catch (error) {
|
|
140
175
|
s.stop("Failed to create project");
|
|
@@ -145,26 +180,26 @@ async function main() {
|
|
|
145
180
|
async function createProject(config, projectPath) {
|
|
146
181
|
await mkdir(projectPath, { recursive: true });
|
|
147
182
|
await mkdir(join(projectPath, "src"), { recursive: true });
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
184
|
+
const startCmd = config.platform === "bun" ? "bun dist/server/supervisor.js" : "node dist/server/supervisor.js";
|
|
185
|
+
const dependencies = {
|
|
186
|
+
"@b9g/router": "^0.2.0",
|
|
187
|
+
"@b9g/shovel": "^0.2.0"
|
|
188
|
+
};
|
|
189
|
+
if (config.uiFramework === "crank") {
|
|
190
|
+
dependencies["@b9g/crank"] = "^0.7.2";
|
|
150
191
|
}
|
|
151
|
-
const ext = config.typescript ? "ts" : "js";
|
|
152
192
|
const packageJson = {
|
|
153
193
|
name: config.name,
|
|
154
194
|
private: true,
|
|
155
195
|
version: "0.0.1",
|
|
156
196
|
type: "module",
|
|
157
197
|
scripts: {
|
|
158
|
-
|
|
198
|
+
develop: `shovel develop src/app.${ext} --platform ${config.platform}`,
|
|
159
199
|
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"
|
|
200
|
+
start: startCmd
|
|
167
201
|
},
|
|
202
|
+
dependencies,
|
|
168
203
|
devDependencies: config.typescript ? {
|
|
169
204
|
"@types/node": "^18.0.0",
|
|
170
205
|
typescript: "^5.0.0"
|
|
@@ -177,18 +212,23 @@ async function createProject(config, projectPath) {
|
|
|
177
212
|
const appFile = generateAppFile(config);
|
|
178
213
|
await writeFile(join(projectPath, `src/app.${ext}`), appFile);
|
|
179
214
|
if (config.typescript) {
|
|
215
|
+
const compilerOptions = {
|
|
216
|
+
target: "ES2022",
|
|
217
|
+
module: "ESNext",
|
|
218
|
+
moduleResolution: "bundler",
|
|
219
|
+
allowSyntheticDefaultImports: true,
|
|
220
|
+
esModuleInterop: true,
|
|
221
|
+
strict: true,
|
|
222
|
+
skipLibCheck: true,
|
|
223
|
+
forceConsistentCasingInFileNames: true,
|
|
224
|
+
lib: ["ES2022", "WebWorker"]
|
|
225
|
+
};
|
|
226
|
+
if (config.uiFramework === "crank") {
|
|
227
|
+
compilerOptions.jsx = "react-jsx";
|
|
228
|
+
compilerOptions.jsxImportSource = "@b9g/crank";
|
|
229
|
+
}
|
|
180
230
|
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
|
-
},
|
|
231
|
+
compilerOptions,
|
|
192
232
|
include: ["src/**/*"],
|
|
193
233
|
exclude: ["node_modules", "dist"]
|
|
194
234
|
};
|
|
@@ -196,9 +236,18 @@ async function createProject(config, projectPath) {
|
|
|
196
236
|
join(projectPath, "tsconfig.json"),
|
|
197
237
|
JSON.stringify(tsConfig, null, 2)
|
|
198
238
|
);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
239
|
+
const envDts = `/// <reference lib="WebWorker" />
|
|
240
|
+
|
|
241
|
+
// Shovel runs your code in a ServiceWorker-like environment.
|
|
242
|
+
// This augments the Worker types with ServiceWorker events
|
|
243
|
+
// so self.addEventListener("fetch", ...) etc. are properly typed.
|
|
244
|
+
interface WorkerGlobalScopeEventMap {
|
|
245
|
+
fetch: FetchEvent;
|
|
246
|
+
install: ExtendableEvent;
|
|
247
|
+
activate: ExtendableEvent;
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
await writeFile(join(projectPath, "src/env.d.ts"), envDts);
|
|
202
251
|
}
|
|
203
252
|
const readme = generateReadme(config);
|
|
204
253
|
await writeFile(join(projectPath, "README.md"), readme);
|
|
@@ -211,62 +260,6 @@ dist/
|
|
|
211
260
|
`;
|
|
212
261
|
await writeFile(join(projectPath, ".gitignore"), gitignore);
|
|
213
262
|
}
|
|
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
263
|
function generateAppFile(config) {
|
|
271
264
|
switch (config.template) {
|
|
272
265
|
case "hello-world":
|
|
@@ -352,59 +345,282 @@ self.addEventListener("fetch", (event) => {
|
|
|
352
345
|
});
|
|
353
346
|
`;
|
|
354
347
|
}
|
|
348
|
+
var css = ` * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
349
|
+
body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; background: #fafafa; }
|
|
350
|
+
main { max-width: 640px; margin: 4rem auto; padding: 2rem; }
|
|
351
|
+
h1 { color: #2563eb; margin-bottom: 1rem; }
|
|
352
|
+
code { background: #e5e7eb; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
353
|
+
a { color: #2563eb; }
|
|
354
|
+
ul { margin-top: 1rem; padding-left: 1.5rem; }
|
|
355
|
+
li { margin-bottom: 0.5rem; }
|
|
356
|
+
button { background: #2563eb; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
|
357
|
+
button:hover { background: #1d4ed8; }
|
|
358
|
+
#result { margin-top: 1rem; padding: 1rem; background: #f3f4f6; border-radius: 4px; }`;
|
|
355
359
|
function generateStaticSite(config) {
|
|
360
|
+
switch (config.uiFramework) {
|
|
361
|
+
case "vanilla":
|
|
362
|
+
return generateStaticSiteVanilla(config);
|
|
363
|
+
case "htmx":
|
|
364
|
+
return generateStaticSiteHtmx(config);
|
|
365
|
+
case "alpine":
|
|
366
|
+
return generateStaticSiteAlpine(config);
|
|
367
|
+
case "crank":
|
|
368
|
+
return generateStaticSiteCrank(config);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function generateStaticSiteVanilla(config) {
|
|
372
|
+
const ext = config.typescript ? "ts" : "js";
|
|
373
|
+
const t = config.typescript;
|
|
356
374
|
return `// ${config.name} - Static Site
|
|
357
|
-
//
|
|
375
|
+
// Renders HTML pages server-side
|
|
358
376
|
|
|
359
377
|
self.addEventListener("fetch", (event) => {
|
|
360
378
|
event.respondWith(handleRequest(event.request));
|
|
361
379
|
});
|
|
362
380
|
|
|
363
|
-
async function handleRequest(request${
|
|
381
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
364
382
|
const url = new URL(request.url);
|
|
365
|
-
let path = url.pathname;
|
|
366
383
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
384
|
+
if (url.pathname === "/") {
|
|
385
|
+
return new Response(renderPage("Home", \`
|
|
386
|
+
<h1>Welcome to ${config.name}</h1>
|
|
387
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
388
|
+
<p><a href="/about">About</a></p>
|
|
389
|
+
\`), {
|
|
390
|
+
headers: { "Content-Type": "text/html" },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (url.pathname === "/about") {
|
|
395
|
+
return new Response(renderPage("About", \`
|
|
396
|
+
<h1>About</h1>
|
|
397
|
+
<p>This is a static site built with <strong>Shovel</strong>.</p>
|
|
398
|
+
<p><a href="/">Home</a></p>
|
|
399
|
+
\`), {
|
|
400
|
+
headers: { "Content-Type": "text/html" },
|
|
401
|
+
});
|
|
370
402
|
}
|
|
371
403
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const publicDir = await directories.open("public");
|
|
375
|
-
const file = await publicDir.getFileHandle(path.slice(1)); // Remove leading /
|
|
376
|
-
const blob = await file.getFile();
|
|
404
|
+
return new Response("Not Found", { status: 404 });
|
|
405
|
+
}
|
|
377
406
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
407
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
408
|
+
return \`<!DOCTYPE html>
|
|
409
|
+
<html lang="en">
|
|
410
|
+
<head>
|
|
411
|
+
<meta charset="UTF-8">
|
|
412
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
413
|
+
<title>\${title} - ${config.name}</title>
|
|
414
|
+
<style>
|
|
415
|
+
${css}
|
|
416
|
+
</style>
|
|
417
|
+
</head>
|
|
418
|
+
<body>
|
|
419
|
+
<main>\${content}</main>
|
|
420
|
+
</body>
|
|
421
|
+
</html>\`;
|
|
422
|
+
}
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
function generateStaticSiteHtmx(config) {
|
|
426
|
+
const ext = config.typescript ? "ts" : "js";
|
|
427
|
+
const t = config.typescript;
|
|
428
|
+
return `// ${config.name} - Static Site with HTMX
|
|
429
|
+
// Server-rendered HTML with HTMX interactions
|
|
430
|
+
|
|
431
|
+
self.addEventListener("fetch", (event) => {
|
|
432
|
+
event.respondWith(handleRequest(event.request));
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
436
|
+
const url = new URL(request.url);
|
|
437
|
+
|
|
438
|
+
if (url.pathname === "/") {
|
|
439
|
+
return new Response(renderPage("Home", \`
|
|
440
|
+
<h1>Welcome to ${config.name}</h1>
|
|
441
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
442
|
+
<button hx-get="/greeting" hx-target="#result" hx-swap="innerHTML">Get Greeting</button>
|
|
443
|
+
<div id="result"></div>
|
|
444
|
+
<p style="margin-top: 1rem;"><a href="/about">About</a></p>
|
|
445
|
+
\`), {
|
|
446
|
+
headers: { "Content-Type": "text/html" },
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (url.pathname === "/greeting") {
|
|
451
|
+
const hour = new Date().getHours();
|
|
452
|
+
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
|
453
|
+
return new Response(\`<p>\${greeting}! The time is \${new Date().toLocaleTimeString()}.</p>\`, {
|
|
454
|
+
headers: { "Content-Type": "text/html" },
|
|
382
455
|
});
|
|
383
|
-
} catch {
|
|
384
|
-
// File not found - return 404
|
|
385
|
-
return new Response("Not Found", { status: 404 });
|
|
386
456
|
}
|
|
457
|
+
|
|
458
|
+
if (url.pathname === "/about") {
|
|
459
|
+
return new Response(renderPage("About", \`
|
|
460
|
+
<h1>About</h1>
|
|
461
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
|
|
462
|
+
<p><a href="/">Home</a></p>
|
|
463
|
+
\`), {
|
|
464
|
+
headers: { "Content-Type": "text/html" },
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return new Response("Not Found", { status: 404 });
|
|
387
469
|
}
|
|
388
470
|
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
471
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
472
|
+
return \`<!DOCTYPE html>
|
|
473
|
+
<html lang="en">
|
|
474
|
+
<head>
|
|
475
|
+
<meta charset="UTF-8">
|
|
476
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
477
|
+
<title>\${title} - ${config.name}</title>
|
|
478
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
479
|
+
<style>
|
|
480
|
+
${css}
|
|
481
|
+
</style>
|
|
482
|
+
</head>
|
|
483
|
+
<body>
|
|
484
|
+
<main>\${content}</main>
|
|
485
|
+
</body>
|
|
486
|
+
</html>\`;
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
function generateStaticSiteAlpine(config) {
|
|
491
|
+
const ext = config.typescript ? "ts" : "js";
|
|
492
|
+
const t = config.typescript;
|
|
493
|
+
return `// ${config.name} - Static Site with Alpine.js
|
|
494
|
+
// Server-rendered HTML with Alpine.js interactions
|
|
495
|
+
|
|
496
|
+
self.addEventListener("fetch", (event) => {
|
|
497
|
+
event.respondWith(handleRequest(event.request));
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
501
|
+
const url = new URL(request.url);
|
|
502
|
+
|
|
503
|
+
if (url.pathname === "/") {
|
|
504
|
+
return new Response(renderPage("Home", \`
|
|
505
|
+
<h1>Welcome to ${config.name}</h1>
|
|
506
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
507
|
+
<div x-data="{ count: 0 }">
|
|
508
|
+
<button @click="count++">Clicked: <span x-text="count"></span></button>
|
|
509
|
+
</div>
|
|
510
|
+
<p style="margin-top: 1rem;"><a href="/about">About</a></p>
|
|
511
|
+
\`), {
|
|
512
|
+
headers: { "Content-Type": "text/html" },
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (url.pathname === "/about") {
|
|
517
|
+
return new Response(renderPage("About", \`
|
|
518
|
+
<h1>About</h1>
|
|
519
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
|
|
520
|
+
<p><a href="/">Home</a></p>
|
|
521
|
+
\`), {
|
|
522
|
+
headers: { "Content-Type": "text/html" },
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return new Response("Not Found", { status: 404 });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
530
|
+
return \`<!DOCTYPE html>
|
|
531
|
+
<html lang="en">
|
|
532
|
+
<head>
|
|
533
|
+
<meta charset="UTF-8">
|
|
534
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
535
|
+
<title>\${title} - ${config.name}</title>
|
|
536
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
|
537
|
+
<style>
|
|
538
|
+
${css}
|
|
539
|
+
</style>
|
|
540
|
+
</head>
|
|
541
|
+
<body>
|
|
542
|
+
<main>\${content}</main>
|
|
543
|
+
</body>
|
|
544
|
+
</html>\`;
|
|
545
|
+
}
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
function generateStaticSiteCrank(config) {
|
|
549
|
+
const t = config.typescript;
|
|
550
|
+
return `import {renderer} from "@b9g/crank/html";
|
|
551
|
+
|
|
552
|
+
// ${config.name} - Static Site with Crank.js
|
|
553
|
+
// Server-rendered HTML with JSX components
|
|
554
|
+
|
|
555
|
+
const css = \`
|
|
556
|
+
${css}
|
|
557
|
+
\`;
|
|
558
|
+
|
|
559
|
+
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
560
|
+
return (
|
|
561
|
+
<html lang="en">
|
|
562
|
+
<head>
|
|
563
|
+
<meta charset="UTF-8" />
|
|
564
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
565
|
+
<title>{title} - ${config.name}</title>
|
|
566
|
+
<style>{css}</style>
|
|
567
|
+
</head>
|
|
568
|
+
<body>
|
|
569
|
+
<main>{children}</main>
|
|
570
|
+
</body>
|
|
571
|
+
</html>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
self.addEventListener("fetch", (event) => {
|
|
576
|
+
event.respondWith(handleRequest(event.request));
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
580
|
+
const url = new URL(request.url);
|
|
581
|
+
let html${t ? ": string" : ""};
|
|
582
|
+
|
|
583
|
+
if (url.pathname === "/") {
|
|
584
|
+
html = await renderer.render(
|
|
585
|
+
<Page title="Home">
|
|
586
|
+
<h1>Welcome to ${config.name}</h1>
|
|
587
|
+
<p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
|
|
588
|
+
<p><a href="/about">About</a></p>
|
|
589
|
+
</Page>
|
|
590
|
+
);
|
|
591
|
+
} else if (url.pathname === "/about") {
|
|
592
|
+
html = await renderer.render(
|
|
593
|
+
<Page title="About">
|
|
594
|
+
<h1>About</h1>
|
|
595
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
596
|
+
<p><a href="/">Home</a></p>
|
|
597
|
+
</Page>
|
|
598
|
+
);
|
|
599
|
+
} else {
|
|
600
|
+
return new Response("Not Found", { status: 404 });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
604
|
+
headers: { "Content-Type": "text/html" },
|
|
605
|
+
});
|
|
404
606
|
}
|
|
405
607
|
`;
|
|
406
608
|
}
|
|
407
609
|
function generateFullStack(config) {
|
|
610
|
+
switch (config.uiFramework) {
|
|
611
|
+
case "vanilla":
|
|
612
|
+
return generateFullStackVanilla(config);
|
|
613
|
+
case "htmx":
|
|
614
|
+
return generateFullStackHtmx(config);
|
|
615
|
+
case "alpine":
|
|
616
|
+
return generateFullStackAlpine(config);
|
|
617
|
+
case "crank":
|
|
618
|
+
return generateFullStackCrank(config);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function generateFullStackVanilla(config) {
|
|
622
|
+
const ext = config.typescript ? "ts" : "js";
|
|
623
|
+
const t = config.typescript;
|
|
408
624
|
return `import { Router } from "@b9g/router";
|
|
409
625
|
|
|
410
626
|
const router = new Router();
|
|
@@ -422,58 +638,272 @@ router.route("/api/echo").post(async (req) => {
|
|
|
422
638
|
return Response.json({ echo: body });
|
|
423
639
|
});
|
|
424
640
|
|
|
425
|
-
//
|
|
426
|
-
router.route("
|
|
427
|
-
|
|
428
|
-
|
|
641
|
+
// HTML pages
|
|
642
|
+
router.route("/").get(() => {
|
|
643
|
+
return new Response(renderPage("Home", \`
|
|
644
|
+
<h1>Welcome to ${config.name}</h1>
|
|
645
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
646
|
+
<ul>
|
|
647
|
+
<li><a href="/about">About</a></li>
|
|
648
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
649
|
+
</ul>
|
|
650
|
+
\`), {
|
|
651
|
+
headers: { "Content-Type": "text/html" },
|
|
652
|
+
});
|
|
653
|
+
});
|
|
429
654
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
655
|
+
router.route("/about").get(() => {
|
|
656
|
+
return new Response(renderPage("About", \`
|
|
657
|
+
<h1>About</h1>
|
|
658
|
+
<p>This is a full-stack app built with <strong>Shovel</strong>.</p>
|
|
659
|
+
<p><a href="/">Home</a></p>
|
|
660
|
+
\`), {
|
|
661
|
+
headers: { "Content-Type": "text/html" },
|
|
662
|
+
});
|
|
663
|
+
});
|
|
434
664
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
665
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
666
|
+
return \`<!DOCTYPE html>
|
|
667
|
+
<html lang="en">
|
|
668
|
+
<head>
|
|
669
|
+
<meta charset="UTF-8">
|
|
670
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
671
|
+
<title>\${title} - ${config.name}</title>
|
|
672
|
+
<style>
|
|
673
|
+
${css}
|
|
674
|
+
</style>
|
|
675
|
+
</head>
|
|
676
|
+
<body>
|
|
677
|
+
<main>\${content}</main>
|
|
678
|
+
</body>
|
|
679
|
+
</html>\`;
|
|
680
|
+
}
|
|
439
681
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
682
|
+
self.addEventListener("fetch", (event) => {
|
|
683
|
+
event.respondWith(router.handle(event.request));
|
|
684
|
+
});
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
function generateFullStackHtmx(config) {
|
|
688
|
+
const ext = config.typescript ? "ts" : "js";
|
|
689
|
+
const t = config.typescript;
|
|
690
|
+
return `import { Router } from "@b9g/router";
|
|
691
|
+
|
|
692
|
+
const router = new Router();
|
|
693
|
+
|
|
694
|
+
// API routes \u2014 return HTML fragments when HTMX requests, JSON otherwise
|
|
695
|
+
router.route("/api/hello").get((req) => {
|
|
696
|
+
const data = {
|
|
697
|
+
message: "Hello from the API!",
|
|
698
|
+
timestamp: new Date().toISOString(),
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
if (req.headers.get("HX-Request")) {
|
|
702
|
+
return new Response(\`<p>\${data.message}</p><p><small>\${data.timestamp}</small></p>\`, {
|
|
703
|
+
headers: { "Content-Type": "text/html" },
|
|
444
704
|
});
|
|
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
705
|
}
|
|
706
|
+
return Response.json(data);
|
|
458
707
|
});
|
|
459
708
|
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
709
|
+
router.route("/api/echo").post(async (req) => {
|
|
710
|
+
const body = await req.json();
|
|
711
|
+
return Response.json({ echo: body });
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// HTML pages
|
|
715
|
+
router.route("/").get(() => {
|
|
716
|
+
return new Response(renderPage("Home", \`
|
|
717
|
+
<h1>Welcome to ${config.name}</h1>
|
|
718
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
719
|
+
<button hx-get="/api/hello" hx-target="#result" hx-swap="innerHTML">Call API</button>
|
|
720
|
+
<div id="result"></div>
|
|
721
|
+
<ul>
|
|
722
|
+
<li><a href="/about">About</a></li>
|
|
723
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
724
|
+
</ul>
|
|
725
|
+
\`), {
|
|
726
|
+
headers: { "Content-Type": "text/html" },
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
router.route("/about").get(() => {
|
|
731
|
+
return new Response(renderPage("About", \`
|
|
732
|
+
<h1>About</h1>
|
|
733
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>HTMX</strong>.</p>
|
|
734
|
+
<p><a href="/">Home</a></p>
|
|
735
|
+
\`), {
|
|
736
|
+
headers: { "Content-Type": "text/html" },
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
741
|
+
return \`<!DOCTYPE html>
|
|
742
|
+
<html lang="en">
|
|
743
|
+
<head>
|
|
744
|
+
<meta charset="UTF-8">
|
|
745
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
746
|
+
<title>\${title} - ${config.name}</title>
|
|
747
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
748
|
+
<style>
|
|
749
|
+
${css}
|
|
750
|
+
</style>
|
|
751
|
+
</head>
|
|
752
|
+
<body>
|
|
753
|
+
<main>\${content}</main>
|
|
754
|
+
</body>
|
|
755
|
+
</html>\`;
|
|
475
756
|
}
|
|
476
757
|
|
|
758
|
+
self.addEventListener("fetch", (event) => {
|
|
759
|
+
event.respondWith(router.handle(event.request));
|
|
760
|
+
});
|
|
761
|
+
`;
|
|
762
|
+
}
|
|
763
|
+
function generateFullStackAlpine(config) {
|
|
764
|
+
const ext = config.typescript ? "ts" : "js";
|
|
765
|
+
const t = config.typescript;
|
|
766
|
+
return `import { Router } from "@b9g/router";
|
|
767
|
+
|
|
768
|
+
const router = new Router();
|
|
769
|
+
|
|
770
|
+
// API routes
|
|
771
|
+
router.route("/api/hello").get(() => {
|
|
772
|
+
return Response.json({
|
|
773
|
+
message: "Hello from the API!",
|
|
774
|
+
timestamp: new Date().toISOString(),
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
router.route("/api/echo").post(async (req) => {
|
|
779
|
+
const body = await req.json();
|
|
780
|
+
return Response.json({ echo: body });
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// HTML pages
|
|
784
|
+
router.route("/").get(() => {
|
|
785
|
+
return new Response(renderPage("Home", \`
|
|
786
|
+
<h1>Welcome to ${config.name}</h1>
|
|
787
|
+
<p>Edit <code>src/app.${ext}</code> to get started.</p>
|
|
788
|
+
<div x-data="{ result: null }">
|
|
789
|
+
<button @click="fetch('/api/hello').then(r => r.json()).then(d => result = d)">Call API</button>
|
|
790
|
+
<div id="result" x-show="result">
|
|
791
|
+
<p x-text="result?.message"></p>
|
|
792
|
+
<p><small x-text="result?.timestamp"></small></p>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
<ul>
|
|
796
|
+
<li><a href="/about">About</a></li>
|
|
797
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
798
|
+
</ul>
|
|
799
|
+
\`), {
|
|
800
|
+
headers: { "Content-Type": "text/html" },
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
router.route("/about").get(() => {
|
|
805
|
+
return new Response(renderPage("About", \`
|
|
806
|
+
<h1>About</h1>
|
|
807
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Alpine.js</strong>.</p>
|
|
808
|
+
<p><a href="/">Home</a></p>
|
|
809
|
+
\`), {
|
|
810
|
+
headers: { "Content-Type": "text/html" },
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
function renderPage(title${t ? ": string" : ""}, content${t ? ": string" : ""})${t ? ": string" : ""} {
|
|
815
|
+
return \`<!DOCTYPE html>
|
|
816
|
+
<html lang="en">
|
|
817
|
+
<head>
|
|
818
|
+
<meta charset="UTF-8">
|
|
819
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
820
|
+
<title>\${title} - ${config.name}</title>
|
|
821
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
|
822
|
+
<style>
|
|
823
|
+
${css}
|
|
824
|
+
</style>
|
|
825
|
+
</head>
|
|
826
|
+
<body>
|
|
827
|
+
<main>\${content}</main>
|
|
828
|
+
</body>
|
|
829
|
+
</html>\`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
self.addEventListener("fetch", (event) => {
|
|
833
|
+
event.respondWith(router.handle(event.request));
|
|
834
|
+
});
|
|
835
|
+
`;
|
|
836
|
+
}
|
|
837
|
+
function generateFullStackCrank(config) {
|
|
838
|
+
const t = config.typescript;
|
|
839
|
+
return `import { Router } from "@b9g/router";
|
|
840
|
+
import {renderer} from "@b9g/crank/html";
|
|
841
|
+
|
|
842
|
+
const router = new Router();
|
|
843
|
+
|
|
844
|
+
const css = \`
|
|
845
|
+
${css}
|
|
846
|
+
\`;
|
|
847
|
+
|
|
848
|
+
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
849
|
+
return (
|
|
850
|
+
<html lang="en">
|
|
851
|
+
<head>
|
|
852
|
+
<meta charset="UTF-8" />
|
|
853
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
854
|
+
<title>{title} - ${config.name}</title>
|
|
855
|
+
<style>{css}</style>
|
|
856
|
+
</head>
|
|
857
|
+
<body>
|
|
858
|
+
<main>{children}</main>
|
|
859
|
+
</body>
|
|
860
|
+
</html>
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// API routes
|
|
865
|
+
router.route("/api/hello").get(() => {
|
|
866
|
+
return Response.json({
|
|
867
|
+
message: "Hello from the API!",
|
|
868
|
+
timestamp: new Date().toISOString(),
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
router.route("/api/echo").post(async (req) => {
|
|
873
|
+
const body = await req.json();
|
|
874
|
+
return Response.json({ echo: body });
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// HTML pages
|
|
878
|
+
router.route("/").get(async () => {
|
|
879
|
+
const html = await renderer.render(
|
|
880
|
+
<Page title="Home">
|
|
881
|
+
<h1>Welcome to ${config.name}</h1>
|
|
882
|
+
<p>Edit <code>src/app.${t ? "tsx" : "jsx"}</code> to get started.</p>
|
|
883
|
+
<ul>
|
|
884
|
+
<li><a href="/about">About</a></li>
|
|
885
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
886
|
+
</ul>
|
|
887
|
+
</Page>
|
|
888
|
+
);
|
|
889
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
890
|
+
headers: { "Content-Type": "text/html" },
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
router.route("/about").get(async () => {
|
|
895
|
+
const html = await renderer.render(
|
|
896
|
+
<Page title="About">
|
|
897
|
+
<h1>About</h1>
|
|
898
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
899
|
+
<p><a href="/">Home</a></p>
|
|
900
|
+
</Page>
|
|
901
|
+
);
|
|
902
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
903
|
+
headers: { "Content-Type": "text/html" },
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
477
907
|
self.addEventListener("fetch", (event) => {
|
|
478
908
|
event.respondWith(router.handle(event.request));
|
|
479
909
|
});
|
|
@@ -483,25 +913,32 @@ function generateReadme(config) {
|
|
|
483
913
|
const templateDescriptions = {
|
|
484
914
|
"hello-world": "A minimal Shovel application",
|
|
485
915
|
api: "A REST API with JSON endpoints",
|
|
486
|
-
"static-site": "A static
|
|
487
|
-
"full-stack": "A full-stack app with
|
|
916
|
+
"static-site": "A static site with server-rendered pages",
|
|
917
|
+
"full-stack": "A full-stack app with HTML pages and API routes"
|
|
918
|
+
};
|
|
919
|
+
const frameworkDescriptions = {
|
|
920
|
+
vanilla: "",
|
|
921
|
+
htmx: " using [HTMX](https://htmx.org)",
|
|
922
|
+
alpine: " using [Alpine.js](https://alpinejs.dev)",
|
|
923
|
+
crank: " using [Crank.js](https://crank.js.org)"
|
|
488
924
|
};
|
|
925
|
+
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
489
926
|
return `# ${config.name}
|
|
490
927
|
|
|
491
|
-
${templateDescriptions[config.template]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
928
|
+
${templateDescriptions[config.template]}${frameworkDescriptions[config.uiFramework]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
492
929
|
|
|
493
930
|
## Getting Started
|
|
494
931
|
|
|
495
932
|
\`\`\`bash
|
|
496
933
|
npm install
|
|
497
|
-
npm run
|
|
934
|
+
npm run develop
|
|
498
935
|
\`\`\`
|
|
499
936
|
|
|
500
|
-
Open http://localhost:
|
|
937
|
+
Open http://localhost:7777
|
|
501
938
|
|
|
502
939
|
## Scripts
|
|
503
940
|
|
|
504
|
-
- \`npm run
|
|
941
|
+
- \`npm run develop\` - Start development server
|
|
505
942
|
- \`npm run build\` - Build for production
|
|
506
943
|
- \`npm start\` - Run production build
|
|
507
944
|
|
|
@@ -510,8 +947,8 @@ Open http://localhost:3000
|
|
|
510
947
|
\`\`\`
|
|
511
948
|
${config.name}/
|
|
512
949
|
\u251C\u2500\u2500 src/
|
|
513
|
-
\u2502 \u2514\u2500\u2500 app.${
|
|
514
|
-
|
|
950
|
+
\u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
|
|
951
|
+
\u251C\u2500\u2500 package.json
|
|
515
952
|
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md
|
|
516
953
|
\`\`\`
|
|
517
954
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/shovel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/bikeshaving/shovel.git"
|
|
9
|
+
},
|
|
6
10
|
"bin": {
|
|
7
11
|
"shovel": "bin/cli.js",
|
|
8
12
|
"create-shovel": "bin/create.js",
|
|
@@ -10,15 +14,15 @@
|
|
|
10
14
|
"create": "bin/create.js"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"@b9g/async-context": "^0.2.
|
|
14
|
-
"@b9g/cache": "^0.2.
|
|
15
|
-
"@b9g/filesystem": "^0.1.
|
|
16
|
-
"@b9g/http-errors": "^0.2.
|
|
17
|
-
"@b9g/node-webworker": "^0.2.
|
|
18
|
-
"@b9g/platform": "^0.1.
|
|
19
|
-
"@b9g/platform-bun": "^0.1.
|
|
20
|
-
"@b9g/platform-cloudflare": "^0.1.
|
|
21
|
-
"@b9g/platform-node": "^0.1.
|
|
17
|
+
"@b9g/async-context": "^0.2.1",
|
|
18
|
+
"@b9g/cache": "^0.2.2",
|
|
19
|
+
"@b9g/filesystem": "^0.1.10",
|
|
20
|
+
"@b9g/http-errors": "^0.2.1",
|
|
21
|
+
"@b9g/node-webworker": "^0.2.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",
|
|
22
26
|
"@clack/prompts": "^0.7.0",
|
|
23
27
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
24
28
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
@@ -29,18 +33,11 @@
|
|
|
29
33
|
"zod": "^3.23.0"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
|
-
"@b9g/assets": "^0.2.
|
|
33
|
-
"@b9g/cache": "^0.2.0",
|
|
36
|
+
"@b9g/assets": "^0.2.1",
|
|
34
37
|
"@b9g/crank": "^0.7.2",
|
|
35
|
-
"@logtape/file": "^1.0.0",
|
|
36
|
-
"@b9g/filesystem": "^0.1.8",
|
|
37
|
-
"@b9g/http-errors": "^0.2.0",
|
|
38
38
|
"@b9g/libuild": "^0.1.22",
|
|
39
|
-
"@b9g/
|
|
40
|
-
"@
|
|
41
|
-
"@b9g/platform-cloudflare": "^0.1.13",
|
|
42
|
-
"@b9g/platform-node": "^0.1.15",
|
|
43
|
-
"@b9g/router": "^0.2.0",
|
|
39
|
+
"@b9g/router": "^0.2.1",
|
|
40
|
+
"@logtape/file": "^1.0.0",
|
|
44
41
|
"@types/bun": "^1.3.4",
|
|
45
42
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
46
43
|
"@typescript-eslint/parser": "^8.0.0",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ServerBundler,
|
|
3
3
|
loadPlatformModule
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ABGHNBNM.js";
|
|
5
5
|
import {
|
|
6
6
|
findProjectRoot,
|
|
7
7
|
findWorkspaceRoot
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7GONPLNW.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/build.ts
|
|
11
11
|
import { resolve, join, dirname, basename } from "path";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ServerBundler,
|
|
3
3
|
loadPlatformModule
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ABGHNBNM.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULTS
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7GONPLNW.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/develop.ts
|
|
10
10
|
import { getLogger } from "@logtape/logtape";
|