@b9g/shovel 0.2.0-beta.2 → 0.2.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -172
- package/README.md +313 -54
- package/bin/cli.d.ts +0 -1
- package/bin/cli.js +62 -786
- package/bin/create.js +79 -37
- package/package.json +33 -37
- package/src/_chunks/build-IWPEM2EW.js +160 -0
- package/src/_chunks/chunk-PTLNYIRW.js +1158 -0
- package/src/_chunks/chunk-VWH6D26D.js +1062 -0
- package/src/_chunks/develop-VHR5FLGQ.js +85 -0
- package/src/_chunks/info-TDUY3FZN.js +13 -0
- package/src/worker-entry.d.ts +0 -7
- package/src/worker-entry.js +0 -37
package/README.md
CHANGED
|
@@ -1,65 +1,40 @@
|
|
|
1
|
-
# Shovel
|
|
1
|
+
# Shovel.js
|
|
2
2
|
|
|
3
|
-
**The
|
|
3
|
+
**The portable meta-framework built on web standards.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Shovel is a CLI platform for developing and deploying service workers as application servers.
|
|
6
6
|
|
|
7
7
|
```javascript
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// src/server.ts
|
|
9
|
+
import {Router} from "@b9g/router";
|
|
10
|
+
const router = new Router();
|
|
11
|
+
|
|
12
|
+
router.route("/").get(() => new Response("Hello world"));
|
|
13
|
+
|
|
14
|
+
self.addEventListener("fetch", (ev) => {
|
|
15
|
+
ev.respondWith(router.handle(ev.request));
|
|
11
16
|
});
|
|
12
17
|
```
|
|
13
18
|
|
|
14
19
|
```bash
|
|
15
|
-
|
|
20
|
+
shovel develop src/server.ts
|
|
16
21
|
```
|
|
17
|
-
|
|
18
|
-
## Why Shovel?
|
|
19
|
-
|
|
20
|
-
Browsers have ServiceWorker. Cloudflare has Workers. Node.js and Bun have... Express?
|
|
21
|
-
|
|
22
|
-
Shovel brings the ServiceWorker programming model to server-side JavaScript. Write your app once using web standards, deploy it anywhere.
|
|
23
|
-
|
|
24
|
-
## Web Standards
|
|
25
|
-
|
|
26
|
-
Shovel implements web platform APIs that server-side JavaScript is missing:
|
|
27
|
-
|
|
28
|
-
| API | Standard | What it does |
|
|
29
|
-
|-----|----------|--------------|
|
|
30
|
-
| `fetch` event | [Service Workers](https://w3c.github.io/ServiceWorker/) | Request handling |
|
|
31
|
-
| `self.caches` | [Cache API](https://w3c.github.io/ServiceWorker/#cache-interface) | Response caching |
|
|
32
|
-
| `self.buckets` | [FileSystemDirectoryHandle](https://fs.spec.whatwg.org/#api-filesystemdirectoryhandle) | Storage (local, S3, R2) |
|
|
33
|
-
| `self.cookieStore` | [Cookie Store API](https://wicg.github.io/cookie-store/) | Cookie management |
|
|
34
|
-
| `URLPattern` | [URLPattern](https://urlpattern.spec.whatwg.org/) | Route matching (100% WPT) |
|
|
35
|
-
| `AsyncContext.Variable` | [TC39 Stage 2](https://github.com/tc39/proposal-async-context) | Request-scoped state |
|
|
36
|
-
|
|
37
|
-
Your code uses standards. Shovel makes them work everywhere.
|
|
38
|
-
|
|
39
|
-
## True Portability
|
|
40
|
-
|
|
41
|
-
Shovel is a complete meta-framework. Same code, any runtime, any rendering strategy:
|
|
42
|
-
|
|
43
|
-
- **Server runtimes**: Node.js, Bun, Cloudflare Workers for development and production
|
|
44
|
-
- **Browser ServiceWorkers**: The same app can run as a PWA service worker
|
|
45
|
-
- **Universal rendering**: Dynamic, static, or client-side - link and deploy assets automatically
|
|
46
|
-
|
|
47
22
|
## Quick Start
|
|
48
23
|
|
|
49
24
|
```javascript
|
|
50
|
-
//
|
|
25
|
+
// src/server.js
|
|
51
26
|
import {Router} from "@b9g/router";
|
|
52
27
|
|
|
53
28
|
const router = new Router();
|
|
54
29
|
|
|
55
30
|
router.route("/").get(() => new Response("Hello World"));
|
|
56
31
|
|
|
57
|
-
router.route("/
|
|
58
|
-
return Response
|
|
32
|
+
router.route("/greet/:name").get((request, {params}) => {
|
|
33
|
+
return new Response(`Hello ${params.name}`);
|
|
59
34
|
});
|
|
60
35
|
|
|
61
36
|
self.addEventListener("fetch", (event) => {
|
|
62
|
-
event.respondWith(router.
|
|
37
|
+
event.respondWith(router.handle(event.request));
|
|
63
38
|
});
|
|
64
39
|
```
|
|
65
40
|
|
|
@@ -68,25 +43,69 @@ self.addEventListener("fetch", (event) => {
|
|
|
68
43
|
npm create @b9g/shovel my-app
|
|
69
44
|
|
|
70
45
|
# Development with hot reload
|
|
71
|
-
npx @b9g/shovel develop
|
|
46
|
+
npx @b9g/shovel develop src/server.ts
|
|
72
47
|
|
|
73
48
|
# Build for production
|
|
74
|
-
npx @b9g/shovel build
|
|
75
|
-
npx @b9g/shovel build
|
|
76
|
-
npx @b9g/shovel build
|
|
49
|
+
npx @b9g/shovel build src/server.ts --platform=node
|
|
50
|
+
npx @b9g/shovel build src/server.ts --platform=bun
|
|
51
|
+
npx @b9g/shovel build src/server.ts --platform=cloudflare
|
|
77
52
|
```
|
|
78
53
|
|
|
54
|
+
|
|
55
|
+
## Web Standards
|
|
56
|
+
Shovel is obsessively standards-first. All Shovel APIs use web standards, and Shovel implements/shims useful standards when they're missing.
|
|
57
|
+
|
|
58
|
+
| API | Standard | Purpose |
|
|
59
|
+
|-----|----------|--------------|
|
|
60
|
+
| `fetch()` | [Fetch](https://fetch.spec.whatwg.org) | Networking |
|
|
61
|
+
| `install`, `activate`, `fetch` events | [Service Workers](https://w3c.github.io/ServiceWorker/) | Server lifecycle |
|
|
62
|
+
| `AsyncContext.Variable` | [TC39 Stage 2](https://github.com/tc39/proposal-async-context) | Request-scoped state |
|
|
63
|
+
| `self.caches` | [Cache API](https://w3c.github.io/ServiceWorker/#cache-interface) | Response caching |
|
|
64
|
+
| `self.directories` | [FileSystem API](https://fs.spec.whatwg.org/) | Storage (local, S3, R2) |
|
|
65
|
+
| `self.cookieStore` | [CookieStore API](https://cookiestore.spec.whatwg.org) | Cookie management |
|
|
66
|
+
| `URLPattern` | [URLPattern](https://urlpattern.spec.whatwg.org/) | Route matching |
|
|
67
|
+
|
|
68
|
+
Your code uses standards. Shovel makes them work everywhere.
|
|
69
|
+
|
|
70
|
+
## Meta-Framework
|
|
71
|
+
|
|
72
|
+
Shovel is a meta-framework: it provides and implements primitives rather than opinions. Instead of dictating how you build, it gives you portable building blocks that work everywhere.
|
|
73
|
+
|
|
74
|
+
## True Portability
|
|
75
|
+
|
|
76
|
+
Same code, any runtime, any rendering strategy:
|
|
77
|
+
|
|
78
|
+
- **Server runtimes**: Node.js, Bun, Cloudflare Workers
|
|
79
|
+
- **Browser ServiceWorkers**: The same app can run as a PWA
|
|
80
|
+
- **Universal rendering**: Dynamic, static, or client-side
|
|
81
|
+
|
|
82
|
+
The core abstraction is the **ServiceWorker-style storage pattern**. Globals provide a consistent API for common web concerns:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const cache = await self.caches.open("sessions"); // Cache API
|
|
86
|
+
const dir = await self.directories.open("uploads"); // FileSystem API
|
|
87
|
+
const db = self.databases.get("main"); // Zen DB (opened on activate)
|
|
88
|
+
const logger = self.loggers.get(["app", "requests"]); // LogTape
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each storage type is:
|
|
92
|
+
- **Lazy** - connections created on first `open()`, cached thereafter
|
|
93
|
+
- **Configured uniformly** - all are configured by `shovel.json`
|
|
94
|
+
- **Platform-aware** - sensible defaults per platform, override what you need
|
|
95
|
+
|
|
96
|
+
This pattern means your app logic stays clean. Swap in Redis for caches, S3 for local filesystem, Postgres for SQLite - change the config, not the code.
|
|
97
|
+
|
|
79
98
|
## Platform APIs
|
|
80
99
|
|
|
81
100
|
```javascript
|
|
82
|
-
// Cache API -
|
|
101
|
+
// Cache API - Request/Response-based caching
|
|
83
102
|
const cache = await self.caches.open("my-cache");
|
|
84
103
|
await cache.put(request, response.clone());
|
|
85
104
|
const cached = await cache.match(request);
|
|
86
105
|
|
|
87
|
-
// File System Access - storage
|
|
88
|
-
const
|
|
89
|
-
const file = await
|
|
106
|
+
// File System Access - storage directories (local, S3, R2)
|
|
107
|
+
const directory = await self.directories.open("uploads");
|
|
108
|
+
const file = await directory.getFileHandle("image.png");
|
|
90
109
|
const contents = await (await file.getFile()).arrayBuffer();
|
|
91
110
|
|
|
92
111
|
// Cookie Store - cookie management
|
|
@@ -105,8 +124,8 @@ requestId.run(crypto.randomUUID(), async () => {
|
|
|
105
124
|
Import any file and get its production URL with content hashing:
|
|
106
125
|
|
|
107
126
|
```javascript
|
|
108
|
-
import styles from "./styles.css" with {
|
|
109
|
-
import logo from "./logo.png" with {
|
|
127
|
+
import styles from "./styles.css" with {assetBase: "/assets"};
|
|
128
|
+
import logo from "./logo.png" with {assetBase: "/assets"};
|
|
110
129
|
|
|
111
130
|
// styles = "/assets/styles-a1b2c3d4.css"
|
|
112
131
|
// logo = "/assets/logo-e5f6g7h8.png"
|
|
@@ -118,8 +137,249 @@ At build time, Shovel:
|
|
|
118
137
|
- Transforms imports to return the final URLs
|
|
119
138
|
|
|
120
139
|
Assets are served via the platform's best option:
|
|
140
|
+
- **Node/Bun**: Static file middleware or directory storage
|
|
121
141
|
- **Cloudflare**: Workers Assets (edge-cached, zero config)
|
|
122
|
-
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
Configure Shovel using `shovel.json` in your project root.
|
|
146
|
+
|
|
147
|
+
### Philosophy
|
|
148
|
+
|
|
149
|
+
Shovel's configuration follows these principles:
|
|
150
|
+
|
|
151
|
+
1. **Platform Defaults, User Overrides** - Each platform provides sensible defaults. You only configure what you want to change.
|
|
152
|
+
|
|
153
|
+
2. **Uniform Interface** - Caches, directories, databases, and loggers all use the same `{ module, export, ...options }` pattern. No magic strings or builtin aliases.
|
|
154
|
+
|
|
155
|
+
3. **Layered Resolution** - For any cache or directory name:
|
|
156
|
+
- If config specifies `module`/`export` → use that
|
|
157
|
+
- Otherwise → use platform default
|
|
158
|
+
|
|
159
|
+
4. **Platform Re-exports** - Each platform exports `DefaultCache` representing what makes sense for that environment:
|
|
160
|
+
- Cloudflare: Native Cache API
|
|
161
|
+
- Bun/Node: MemoryCache
|
|
162
|
+
|
|
163
|
+
5. **Transparency** - Config is what you see. Every backend is an explicit module path, making it easy to debug and trace.
|
|
164
|
+
|
|
165
|
+
### Basic Config
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"port": "PORT || 3000",
|
|
170
|
+
"host": "HOST || localhost",
|
|
171
|
+
"workers": "WORKERS ?? 1",
|
|
172
|
+
"caches": {
|
|
173
|
+
"sessions": {
|
|
174
|
+
"module": "@b9g/cache-redis",
|
|
175
|
+
"export": "RedisCache",
|
|
176
|
+
"url": "REDIS_URL"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"directories": {
|
|
180
|
+
"uploads": {
|
|
181
|
+
"module": "@b9g/filesystem-s3",
|
|
182
|
+
"export": "S3Directory",
|
|
183
|
+
"bucket": "S3_BUCKET"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"databases": {
|
|
187
|
+
"main": {
|
|
188
|
+
"module": "@b9g/zen/bun",
|
|
189
|
+
"url": "DATABASE_URL"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"logging": {
|
|
193
|
+
"loggers": [
|
|
194
|
+
{"category": ["app"], "level": "info", "sinks": ["console"]}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Caches
|
|
201
|
+
|
|
202
|
+
Configure cache backends using `module` and `export`:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"caches": {
|
|
207
|
+
"api-responses": {
|
|
208
|
+
"module": "@b9g/cache/memory",
|
|
209
|
+
"export": "MemoryCache"
|
|
210
|
+
},
|
|
211
|
+
"sessions": {
|
|
212
|
+
"module": "@b9g/cache-redis",
|
|
213
|
+
"export": "RedisCache",
|
|
214
|
+
"url": "REDIS_URL"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- **Default**: Platform's `DefaultCache` when no config specified (MemoryCache on Bun/Node, native on Cloudflare)
|
|
221
|
+
- **Pattern matching**: Use wildcards like `"api-*"` to match multiple cache names
|
|
222
|
+
- **Empty config**: `"my-cache": {}` uses platform default explicitly
|
|
223
|
+
|
|
224
|
+
### Directories
|
|
225
|
+
|
|
226
|
+
Configure directory backends. Platforms provide defaults for well-known directories (`server`, `public`, `tmp`):
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"directories": {
|
|
231
|
+
"uploads": {
|
|
232
|
+
"module": "@b9g/filesystem-s3",
|
|
233
|
+
"export": "S3Directory",
|
|
234
|
+
"bucket": "MY_BUCKET",
|
|
235
|
+
"region": "us-east-1"
|
|
236
|
+
},
|
|
237
|
+
"data": {
|
|
238
|
+
"module": "@b9g/filesystem/node-fs",
|
|
239
|
+
"export": "NodeFSDirectory",
|
|
240
|
+
"path": "./data"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- **Well-known defaults**: `server` (dist/server), `public` (dist/public), `tmp` (OS temp)
|
|
247
|
+
- **Custom directories**: Must be explicitly configured
|
|
248
|
+
|
|
249
|
+
### Logging
|
|
250
|
+
|
|
251
|
+
Shovel uses [LogTape](https://logtape.org/) for logging:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const logger = self.loggers.get(["shovel", "myapp"]);
|
|
255
|
+
logger.info`Request received: ${request.url}`;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Zero-config logging**: Use the `["shovel", ...]` category hierarchy to inherit Shovel's default logging (info level to console). No configuration needed.
|
|
259
|
+
|
|
260
|
+
For custom configuration, use `shovel.json`:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"logging": {
|
|
265
|
+
"sinks": {
|
|
266
|
+
"file": {
|
|
267
|
+
"module": "@logtape/logtape",
|
|
268
|
+
"export": "getFileSink",
|
|
269
|
+
"path": "./logs/app.log"
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"loggers": [
|
|
273
|
+
{"category": ["myapp"], "level": "info", "sinks": ["console"]},
|
|
274
|
+
{"category": ["myapp", "db"], "level": "debug", "sinks": ["file"]}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- **Console sink is implicit** - always available as `"console"`
|
|
281
|
+
- **Category hierarchy** - `["myapp", "db"]` inherits from `["myapp"]`
|
|
282
|
+
- **parentSinks** - use `"override"` to replace parent sinks instead of inheriting
|
|
283
|
+
|
|
284
|
+
### Databases
|
|
285
|
+
|
|
286
|
+
Configure database drivers using the same `module`/`export` pattern:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"databases": {
|
|
291
|
+
"main": {
|
|
292
|
+
"module": "@b9g/zen/bun",
|
|
293
|
+
"url": "DATABASE_URL"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Open databases in `activate` (for migrations), then use `get()` in requests:
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
self.addEventListener("activate", (event) => {
|
|
303
|
+
event.waitUntil(self.databases.open("main", 1, (e) => {
|
|
304
|
+
e.waitUntil(runMigrations(e));
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
self.addEventListener("fetch", (event) => {
|
|
309
|
+
const db = self.databases.get("main");
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Expression Syntax
|
|
314
|
+
|
|
315
|
+
Configuration values support a domain-specific expression language that generates JavaScript code evaluated at runtime.
|
|
316
|
+
|
|
317
|
+
#### Environment Variables
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
$VAR → process.env.VAR
|
|
321
|
+
$VAR || fallback → process.env.VAR || "fallback"
|
|
322
|
+
$VAR ?? fallback → process.env.VAR ?? "fallback"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### Bracket Placeholders
|
|
326
|
+
|
|
327
|
+
| Placeholder | Description | Resolution |
|
|
328
|
+
|-------------|-------------|------------|
|
|
329
|
+
| `[outdir]` | Build output directory | Build time |
|
|
330
|
+
| `[tmpdir]` | OS temp directory | Runtime |
|
|
331
|
+
| `[git]` | Git commit SHA | Build time |
|
|
332
|
+
|
|
333
|
+
The bracket syntax mirrors esbuild/webpack output filename templating (`[name]`, `[hash]`).
|
|
334
|
+
|
|
335
|
+
#### Operators
|
|
336
|
+
|
|
337
|
+
| Operator | Example | Description |
|
|
338
|
+
|----------|---------|-------------|
|
|
339
|
+
| `\|\|` | `$VAR \|\| default` | Logical OR (falsy fallback) |
|
|
340
|
+
| `??` | `$VAR ?? default` | Nullish coalescing |
|
|
341
|
+
| `&&` | `$A && $B` | Logical AND |
|
|
342
|
+
| `? :` | `$ENV === prod ? a : b` | Ternary conditional |
|
|
343
|
+
| `===`, `!==` | `$ENV === production` | Strict equality |
|
|
344
|
+
| `!` | `!$DISABLED` | Logical NOT |
|
|
345
|
+
|
|
346
|
+
#### Path Expressions
|
|
347
|
+
|
|
348
|
+
Path expressions support path segments and relative resolution:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
$DATADIR/uploads → joins env var with path segment
|
|
352
|
+
[outdir]/server → joins build output with path segment
|
|
353
|
+
./data → resolved to absolute path at build time
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Example
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"port": "$PORT || 3000",
|
|
361
|
+
"host": "$HOST || 0.0.0.0",
|
|
362
|
+
"directories": {
|
|
363
|
+
"server": { "path": "[outdir]/server" },
|
|
364
|
+
"public": { "path": "[outdir]/public" },
|
|
365
|
+
"tmp": { "path": "[tmpdir]" },
|
|
366
|
+
"data": { "path": "./data" },
|
|
367
|
+
"cache": { "path": "($CACHE_DIR || [tmpdir])/myapp" }
|
|
368
|
+
},
|
|
369
|
+
"cache": {
|
|
370
|
+
"provider": "$NODE_ENV === production ? redis : memory"
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Dynamic values (containing `$VAR` or `[tmpdir]`) use getters to ensure evaluation at access time, not module load time.
|
|
376
|
+
|
|
377
|
+
### Access in Code
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
import {config} from "shovel:config";
|
|
381
|
+
console.log(config.port); // Resolved value
|
|
382
|
+
```
|
|
123
383
|
|
|
124
384
|
## Packages
|
|
125
385
|
|
|
@@ -128,7 +388,7 @@ Assets are served via the platform's best option:
|
|
|
128
388
|
| `@b9g/shovel` | CLI for development and deployment |
|
|
129
389
|
| `@b9g/platform` | Core runtime and platform APIs |
|
|
130
390
|
| `@b9g/platform-node` | Node.js adapter |
|
|
131
|
-
| `@b9g/platform-bun` | Bun adapter |
|
|
391
|
+
| `@b9g/platform-bun` | Bun.js adapter |
|
|
132
392
|
| `@b9g/platform-cloudflare` | Cloudflare Workers adapter |
|
|
133
393
|
| `@b9g/router` | URLPattern-based routing with middleware |
|
|
134
394
|
| `@b9g/cache` | Cache API implementation |
|
|
@@ -136,7 +396,6 @@ Assets are served via the platform's best option:
|
|
|
136
396
|
| `@b9g/match-pattern` | URLPattern with extensions (100% WPT) |
|
|
137
397
|
| `@b9g/async-context` | AsyncContext.Variable implementation |
|
|
138
398
|
| `@b9g/http-errors` | Standard HTTP error classes |
|
|
139
|
-
| `@b9g/auth` | OAuth2/PKCE and CORS middleware |
|
|
140
399
|
| `@b9g/assets` | Static asset handling |
|
|
141
400
|
|
|
142
401
|
## License
|
package/bin/cli.d.ts
CHANGED