@b9g/shovel 0.2.0-beta.11 → 0.2.0-beta.12

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 CHANGED
@@ -124,8 +124,7 @@ router.use(cors({ origin: "https://example.com" }));
124
124
  ### CLI Changes
125
125
 
126
126
  - `shovel develop` - Development server with hot reload (note: `dev` alias removed)
127
- - `shovel build` - Production build
128
- - `shovel activate` - Static site generation
127
+ - `shovel build` - Production build (use `--lifecycle` flag to run lifecycle events)
129
128
  - Removed `--verbose` flags (use logging config instead)
130
129
 
131
130
  ### Infrastructure
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  Shovel is a CLI platform for developing and deploying service workers as application servers.
6
6
 
7
7
  ```javascript
8
+ // src/server.ts
8
9
  import {Router} from "@b9g/router";
9
10
  const router = new Router();
10
11
 
@@ -16,12 +17,12 @@ self.addEventListener("fetch", (ev) => {
16
17
  ```
17
18
 
18
19
  ```bash
19
- shovel develop app.js
20
+ shovel develop src/server.ts
20
21
  ```
21
22
  ## Quick Start
22
23
 
23
24
  ```javascript
24
- // app.js
25
+ // src/server.js
25
26
  import {Router} from "@b9g/router";
26
27
 
27
28
  const router = new Router();
@@ -42,12 +43,12 @@ self.addEventListener("fetch", (event) => {
42
43
  npm create @b9g/shovel my-app
43
44
 
44
45
  # Development with hot reload
45
- npx @b9g/shovel develop app.js
46
+ npx @b9g/shovel develop src/server.ts
46
47
 
47
48
  # Build for production
48
- npx @b9g/shovel build app.js --platform=node
49
- npx @b9g/shovel build app.js --platform=bun
50
- 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
51
52
  ```
52
53
 
53
54
 
@@ -57,7 +58,7 @@ Shovel is obsessively standards-first. All Shovel APIs use web standards, and Sh
57
58
  | API | Standard | Purpose |
58
59
  |-----|----------|--------------|
59
60
  | `fetch()` | [Fetch](https://fetch.spec.whatwg.org) | Networking |
60
- | `"install"`, `"activate"`, `"fetch"` events | [Service Workers](https://w3c.github.io/ServiceWorker/) | Server lifecycle |
61
+ | `install`, `activate`, `fetch` events | [Service Workers](https://w3c.github.io/ServiceWorker/) | Server lifecycle |
61
62
  | `AsyncContext.Variable` | [TC39 Stage 2](https://github.com/tc39/proposal-async-context) | Request-scoped state |
62
63
  | `self.caches` | [Cache API](https://w3c.github.io/ServiceWorker/#cache-interface) | Response caching |
63
64
  | `self.directories` | [FileSystem API](https://fs.spec.whatwg.org/) | Storage (local, S3, R2) |
@@ -92,13 +93,12 @@ Each storage type is:
92
93
  - **Configured uniformly** - all are configured by `shovel.json`
93
94
  - **Platform-aware** - sensible defaults per platform, override what you need
94
95
 
95
- This pattern means your app logic stays clean. Swap Redis for memory cache, S3 for local filesystem, Postgres for SQLite - change the config, not the code.
96
-
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
97
 
98
98
  ## Platform APIs
99
99
 
100
100
  ```javascript
101
- // Cache API - response caching
101
+ // Cache API - Request/Response-based caching
102
102
  const cache = await self.caches.open("my-cache");
103
103
  await cache.put(request, response.clone());
104
104
  const cached = await cache.match(request);
@@ -124,8 +124,8 @@ requestId.run(crypto.randomUUID(), async () => {
124
124
  Import any file and get its production URL with content hashing:
125
125
 
126
126
  ```javascript
127
- import styles from "./styles.css" with { assetBase: "/assets" };
128
- 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"};
129
129
 
130
130
  // styles = "/assets/styles-a1b2c3d4.css"
131
131
  // logo = "/assets/logo-e5f6g7h8.png"
@@ -137,8 +137,8 @@ At build time, Shovel:
137
137
  - Transforms imports to return the final URLs
138
138
 
139
139
  Assets are served via the platform's best option:
140
- - **Cloudflare**: Workers Assets (edge-cached, zero config)
141
140
  - **Node/Bun**: Static file middleware or directory storage
141
+ - **Cloudflare**: Workers Assets (edge-cached, zero config)
142
142
 
143
143
  ## Configuration
144
144
 
package/bin/cli.d.ts CHANGED
@@ -1,2 +1 @@
1
- #!/usr/bin/env sh
2
1
  export {};
package/bin/cli.js CHANGED
@@ -5,10 +5,11 @@ import {
5
5
  DEFAULTS,
6
6
  findProjectRoot,
7
7
  loadConfig
8
- } from "../src/_chunks/chunk-GRAFMTEH.js";
8
+ } from "../src/_chunks/chunk-PTLNYIRW.js";
9
9
 
10
10
  // bin/cli.ts
11
11
  import { resolve } from "path";
12
+ import { spawnSync } from "child_process";
12
13
  import { configureLogging } from "@b9g/platform/runtime";
13
14
  import { Command } from "commander";
14
15
  import pkg from "../package.json" with { type: "json" };
@@ -36,27 +37,39 @@ await configureLogging({
36
37
  });
37
38
  var program = new Command();
38
39
  program.name("shovel").description("Shovel CLI").version(pkg.version);
40
+ function checkPlatformReexec(options) {
41
+ const platform = options.platform ?? config.platform;
42
+ const isBun = typeof globalThis.Bun !== "undefined";
43
+ if (platform === "bun" && !isBun) {
44
+ const result = spawnSync("bun", process.argv.slice(1), { stdio: "inherit" });
45
+ process.exit(result.status ?? 1);
46
+ }
47
+ if (platform === "node" && isBun) {
48
+ const result = Bun.spawnSync(["node", ...process.argv.slice(1)], {
49
+ stdout: "inherit",
50
+ stderr: "inherit",
51
+ stdin: "inherit"
52
+ });
53
+ process.exit(result.exitCode ?? 1);
54
+ }
55
+ }
39
56
  program.command("develop <entrypoint>").description("Start development server with hot reload").option("-p, --port <port>", "Port to listen on", DEFAULTS.SERVER.PORT).option("-h, --host <host>", "Host to bind to", DEFAULTS.SERVER.HOST).option(
40
57
  "-w, --workers <count>",
41
58
  "Number of workers (default: CPU cores)",
42
59
  DEFAULTS.WORKERS
43
60
  ).option("--platform <name>", "Runtime platform (node, cloudflare, bun)").action(async (entrypoint, options) => {
44
- const { developCommand } = await import("../src/_chunks/develop-A7EU2ZDY.js");
61
+ checkPlatformReexec(options);
62
+ const { developCommand } = await import("../src/_chunks/develop-2R6YVDI7.js");
45
63
  await developCommand(entrypoint, options, config);
46
64
  });
47
- program.command("build <entrypoint>").description("Build app for production").option("-w, --workers <count>", "Worker count (defaults to 1)", void 0).option("--platform <name>", "Runtime platform (node, cloudflare, bun)").action(async (entrypoint, options) => {
48
- const { buildCommand } = await import("../src/_chunks/build-V3IPZGKC.js");
49
- await buildCommand(entrypoint, options, config);
50
- });
51
- program.command("activate <entrypoint>").description(
52
- "Activate ServiceWorker (for static site generation in activate event)"
53
- ).option("--platform <name>", "Runtime platform (node, cloudflare, bun)").option(
54
- "-w, --workers <count>",
55
- "Number of workers",
56
- DEFAULTS.WORKERS.toString()
65
+ program.command("build <entrypoint>").description("Build app for production").option("--platform <name>", "Runtime platform (node, cloudflare, bun)").option(
66
+ "--lifecycle [stage]",
67
+ "Run ServiceWorker lifecycle after build (install or activate, default: activate)"
57
68
  ).action(async (entrypoint, options) => {
58
- const { activateCommand } = await import("../src/_chunks/activate-TP6RQP47.js");
59
- await activateCommand(entrypoint, options, config);
69
+ checkPlatformReexec(options);
70
+ const { buildCommand } = await import("../src/_chunks/build-CZHJ4EAF.js");
71
+ await buildCommand(entrypoint, options, config);
72
+ process.exit(0);
60
73
  });
61
74
  program.command("info").description("Display platform and runtime information").action(async () => {
62
75
  const { infoCommand } = await import("../src/_chunks/info-TDUY3FZN.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.0-beta.11",
3
+ "version": "0.2.0-beta.12",
4
4
  "description": "ServiceWorker-first universal deployment platform. Write ServiceWorker apps once, deploy anywhere (Node/Bun/Cloudflare). Registry-based multi-app orchestration.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -27,13 +27,13 @@
27
27
  "@b9g/cache": "^0.2.0-beta.0",
28
28
  "@b9g/crank": "^0.7.2",
29
29
  "@logtape/file": "^1.0.0",
30
- "@b9g/filesystem": "^0.1.7",
30
+ "@b9g/filesystem": "^0.1.8",
31
31
  "@b9g/http-errors": "^0.2.0-beta.0",
32
32
  "@b9g/libuild": "^0.1.22",
33
- "@b9g/platform": "^0.1.12",
34
- "@b9g/platform-bun": "^0.1.10",
35
- "@b9g/platform-cloudflare": "^0.1.10",
36
- "@b9g/platform-node": "^0.1.12",
33
+ "@b9g/platform": "^0.1.13",
34
+ "@b9g/platform-bun": "^0.1.11",
35
+ "@b9g/platform-cloudflare": "^0.1.11",
36
+ "@b9g/platform-node": "^0.1.13",
37
37
  "@b9g/router": "^0.2.0-beta.1",
38
38
  "@types/bun": "^1.3.4",
39
39
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -46,12 +46,12 @@
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@b9g/node-webworker": "^0.2.0-beta.1",
49
- "@b9g/platform": "^0.1.12",
50
- "@b9g/platform-node": "^0.1.12",
51
- "@b9g/platform-cloudflare": "^0.1.10",
52
- "@b9g/platform-bun": "^0.1.10",
49
+ "@b9g/platform": "^0.1.13",
50
+ "@b9g/platform-node": "^0.1.13",
51
+ "@b9g/platform-cloudflare": "^0.1.11",
52
+ "@b9g/platform-bun": "^0.1.11",
53
53
  "@b9g/cache": "^0.2.0-beta.0",
54
- "@b9g/filesystem": "^0.1.7",
54
+ "@b9g/filesystem": "^0.1.8",
55
55
  "@b9g/http-errors": "^0.2.0-beta.0"
56
56
  },
57
57
  "peerDependenciesMeta": {
@@ -0,0 +1,159 @@
1
+ import {
2
+ ServerBundler
3
+ } from "./chunk-4DKAY5MA.js";
4
+ import {
5
+ findProjectRoot,
6
+ findWorkspaceRoot
7
+ } from "./chunk-PTLNYIRW.js";
8
+
9
+ // src/commands/build.ts
10
+ import { resolve, join, dirname } from "path";
11
+ import { getLogger } from "@logtape/logtape";
12
+ import * as Platform from "@b9g/platform";
13
+ import { readFile, writeFile } from "fs/promises";
14
+ var logger = getLogger(["shovel"]);
15
+ async function buildForProduction({
16
+ entrypoint,
17
+ outDir,
18
+ platform = "node",
19
+ userBuildConfig,
20
+ lifecycle
21
+ }) {
22
+ const entryPath = resolve(entrypoint);
23
+ const outputDir = resolve(outDir);
24
+ const serverDir = join(outputDir, "server");
25
+ const projectRoot = findProjectRoot(dirname(entryPath));
26
+ logger.debug("Entry:", { entryPath });
27
+ logger.debug("Output:", { outputDir });
28
+ logger.debug("Target platform:", { platform });
29
+ logger.debug("Project root:", { projectRoot });
30
+ const platformInstance = await Platform.createPlatform(platform);
31
+ const platformESBuildConfig = platformInstance.getESBuildConfig();
32
+ const bundler = new ServerBundler({
33
+ entrypoint,
34
+ outDir,
35
+ platform: platformInstance,
36
+ platformESBuildConfig,
37
+ userBuildConfig,
38
+ lifecycle
39
+ });
40
+ const { success, outputs } = await bundler.build();
41
+ if (!success) {
42
+ throw new Error("Build failed");
43
+ }
44
+ await generatePackageJSON({ serverDir, platform, entryPath });
45
+ logger.debug("Built app to", { outputDir });
46
+ logger.debug("Server files", { dir: serverDir });
47
+ logger.debug("Public files", { dir: join(outputDir, "public") });
48
+ logger.info("Build complete: {path}", {
49
+ path: outputs.index || outputs.worker
50
+ });
51
+ return {
52
+ platform: platformInstance,
53
+ workerPath: outputs.worker
54
+ };
55
+ }
56
+ async function generatePackageJSON({
57
+ serverDir,
58
+ platform,
59
+ entryPath
60
+ }) {
61
+ const entryDir = dirname(entryPath);
62
+ const sourcePackageJsonPath = resolve(entryDir, "package.json");
63
+ try {
64
+ const packageJSONContent = await readFile(sourcePackageJsonPath, "utf8");
65
+ try {
66
+ JSON.parse(packageJSONContent);
67
+ } catch (parseError) {
68
+ throw new Error(`Invalid package.json format: ${parseError}`);
69
+ }
70
+ await writeFile(
71
+ join(serverDir, "package.json"),
72
+ packageJSONContent,
73
+ "utf8"
74
+ );
75
+ logger.debug("Copied package.json", { serverDir });
76
+ } catch (error) {
77
+ logger.debug("Could not copy package.json: {error}", { error });
78
+ try {
79
+ const generatedPackageJson = await generateExecutablePackageJSON(platform);
80
+ await writeFile(
81
+ join(serverDir, "package.json"),
82
+ JSON.stringify(generatedPackageJson, null, 2),
83
+ "utf8"
84
+ );
85
+ logger.debug("Generated package.json", { platform });
86
+ } catch (generateError) {
87
+ logger.debug("Could not generate package.json: {error}", {
88
+ error: generateError
89
+ });
90
+ }
91
+ }
92
+ }
93
+ async function generateExecutablePackageJSON(platform) {
94
+ const packageJSON = {
95
+ name: "shovel-executable",
96
+ version: "1.0.0",
97
+ type: "module",
98
+ private: true,
99
+ dependencies: {}
100
+ };
101
+ const isWorkspaceEnvironment = findWorkspaceRoot() !== null;
102
+ if (isWorkspaceEnvironment) {
103
+ packageJSON.dependencies = {};
104
+ } else {
105
+ switch (platform) {
106
+ case "node":
107
+ packageJSON.dependencies["@b9g/platform-node"] = "^0.1.0";
108
+ break;
109
+ case "bun":
110
+ packageJSON.dependencies["@b9g/platform-bun"] = "^0.1.0";
111
+ break;
112
+ case "cloudflare":
113
+ packageJSON.dependencies["@b9g/platform-cloudflare"] = "^0.1.0";
114
+ break;
115
+ default:
116
+ packageJSON.dependencies["@b9g/platform"] = "^0.1.0";
117
+ }
118
+ packageJSON.dependencies["@b9g/cache"] = "^0.1.0";
119
+ packageJSON.dependencies["@b9g/filesystem"] = "^0.1.0";
120
+ }
121
+ return packageJSON;
122
+ }
123
+ async function buildCommand(entrypoint, options, config) {
124
+ const platform = Platform.resolvePlatform({ ...options, config });
125
+ let lifecycleOption;
126
+ if (options.lifecycle) {
127
+ const stage = typeof options.lifecycle === "string" ? options.lifecycle : "activate";
128
+ if (stage !== "install" && stage !== "activate") {
129
+ throw new Error(
130
+ `Invalid lifecycle stage: ${stage}. Must be "install" or "activate".`
131
+ );
132
+ }
133
+ lifecycleOption = { stage };
134
+ }
135
+ const { platform: platformInstance, workerPath } = await buildForProduction({
136
+ entrypoint,
137
+ outDir: "dist",
138
+ platform,
139
+ userBuildConfig: config.build,
140
+ lifecycle: lifecycleOption
141
+ });
142
+ if (lifecycleOption) {
143
+ if (!workerPath) {
144
+ throw new Error("No worker entry point found in build outputs");
145
+ }
146
+ logger.info("Running ServiceWorker lifecycle: {stage}", {
147
+ stage: lifecycleOption.stage
148
+ });
149
+ await platformInstance.serviceWorker.register(workerPath);
150
+ await platformInstance.serviceWorker.ready;
151
+ await platformInstance.serviceWorker.terminate();
152
+ logger.info("Lifecycle complete");
153
+ }
154
+ await platformInstance.dispose();
155
+ }
156
+ export {
157
+ buildCommand,
158
+ buildForProduction
159
+ };