@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/README.md CHANGED
@@ -1,65 +1,40 @@
1
- # Shovel
1
+ # Shovel.js 🪏
2
2
 
3
- **The ServiceWorker platform for server-side JavaScript.**
3
+ **The portable meta-framework built on web standards.**
4
4
 
5
- Same code. Any runtime. Node.js, Bun, Cloudflare Workers.
5
+ Shovel is a CLI platform for developing and deploying service workers as application servers.
6
6
 
7
7
  ```javascript
8
- // app.js
9
- self.addEventListener("fetch", (event) => {
10
- event.respondWith(new Response("Hello World"));
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
- npx @b9g/shovel develop app.js
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
- // app.js
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("/users/:id").get((request, {params}) => {
58
- return Response.json({id: params.id});
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.handler(event.request));
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 app.js
46
+ npx @b9g/shovel develop src/server.ts
72
47
 
73
48
  # Build for production
74
- npx @b9g/shovel build app.js --platform=node
75
- npx @b9g/shovel build app.js --platform=bun
76
- npx @b9g/shovel build app.js --platform=cloudflare
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 - response caching
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 buckets (local, S3, R2)
88
- const bucket = await self.buckets.open("uploads");
89
- const file = await bucket.getFileHandle("image.png");
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 { assetBase: "/assets" };
109
- import logo from "./logo.png" with { assetBase: "/assets" };
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
- - **Node/Bun**: Static file middleware or bucket storage
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
@@ -1,2 +1 @@
1
- #!/usr/bin/env sh
2
1
  export {};