@b9g/shovel 0.2.0-beta.3 → 0.2.0-beta.5

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.
Files changed (2) hide show
  1. package/bin/cli.js +123 -112
  2. package/package.json +15 -15
package/bin/cli.js CHANGED
@@ -23,10 +23,9 @@ import * as Platform from "@b9g/platform";
23
23
 
24
24
  // src/esbuild/watcher.ts
25
25
  import * as ESBuild from "esbuild";
26
- import { watch } from "fs";
27
26
  import { resolve, dirname as dirname2, join } from "path";
28
27
  import { readFileSync } from "fs";
29
- import { mkdir } from "fs/promises";
28
+ import { mkdir, unlink } from "fs/promises";
30
29
  import { assetsPlugin } from "@b9g/assets/plugin";
31
30
 
32
31
  // src/esbuild/import-meta-plugin.ts
@@ -36,8 +35,8 @@ import { pathToFileURL } from "url";
36
35
  function importMetaPlugin() {
37
36
  return {
38
37
  name: "import-meta-transform",
39
- setup(build3) {
40
- build3.onLoad({ filter: /\.[jt]sx?$/, namespace: "file" }, async (args) => {
38
+ setup(build2) {
39
+ build2.onLoad({ filter: /\.[jt]sx?$/, namespace: "file" }, async (args) => {
41
40
  if (args.path.includes("node_modules")) {
42
41
  return null;
43
42
  }
@@ -82,115 +81,128 @@ function importMetaPlugin() {
82
81
  import { getLogger } from "@logtape/logtape";
83
82
  var logger = getLogger(["watcher"]);
84
83
  var Watcher = class {
85
- #watcher;
86
- #building;
87
84
  #options;
85
+ #ctx;
86
+ #initialBuildComplete;
87
+ #initialBuildResolve;
88
+ #currentEntrypoint;
89
+ #previousEntrypoint;
88
90
  constructor(options) {
89
- this.#building = false;
90
91
  this.#options = options;
92
+ this.#initialBuildComplete = false;
93
+ this.#currentEntrypoint = "";
94
+ this.#previousEntrypoint = "";
91
95
  }
92
96
  /**
93
97
  * Start watching and building
98
+ * @returns Result with success status and the hashed entrypoint path
94
99
  */
95
100
  async start() {
96
101
  const entryPath = resolve(this.#options.entrypoint);
97
- await this.#build();
98
- const watchDir = dirname2(entryPath);
99
- logger.info("Watching for changes", { watchDir });
100
- this.#watcher = watch(
101
- watchDir,
102
- { recursive: true },
103
- (_eventType, filename) => {
104
- if (filename && (filename.endsWith(".js") || filename.endsWith(".ts") || filename.endsWith(".tsx"))) {
105
- const outDir = this.#options.outDir || "dist";
106
- if (filename.startsWith(outDir + "/") || filename.startsWith(outDir + "\\")) {
107
- return;
102
+ const outputDir = resolve(this.#options.outDir);
103
+ const workspaceRoot = this.#findWorkspaceRoot();
104
+ await mkdir(join(outputDir, "server"), { recursive: true });
105
+ await mkdir(join(outputDir, "static"), { recursive: true });
106
+ const initialBuildPromise = new Promise((resolve3) => {
107
+ this.#initialBuildResolve = resolve3;
108
+ });
109
+ this.#ctx = await ESBuild.context({
110
+ entryPoints: [entryPath],
111
+ bundle: true,
112
+ format: "esm",
113
+ target: "es2022",
114
+ platform: "node",
115
+ outdir: `${outputDir}/server`,
116
+ entryNames: "[name]-[hash]",
117
+ metafile: true,
118
+ absWorkingDir: workspaceRoot,
119
+ plugins: [
120
+ importMetaPlugin(),
121
+ assetsPlugin({
122
+ outDir: outputDir
123
+ }),
124
+ // Plugin to detect build completion (works with watch mode)
125
+ {
126
+ name: "build-notify",
127
+ setup: (build2) => {
128
+ build2.onStart(() => {
129
+ logger.info("Building", {
130
+ entrypoint: this.#options.entrypoint
131
+ });
132
+ });
133
+ build2.onEnd(async (result) => {
134
+ const success = result.errors.length === 0;
135
+ let outputPath = "";
136
+ if (result.metafile) {
137
+ const outputs = Object.keys(result.metafile.outputs);
138
+ const jsOutput = outputs.find((p) => p.endsWith(".js"));
139
+ if (jsOutput) {
140
+ outputPath = resolve(jsOutput);
141
+ }
142
+ }
143
+ if (success) {
144
+ logger.info("Build complete", { entrypoint: outputPath });
145
+ if (this.#currentEntrypoint && this.#currentEntrypoint !== outputPath) {
146
+ try {
147
+ await unlink(this.#currentEntrypoint);
148
+ await unlink(this.#currentEntrypoint + ".map").catch(
149
+ () => {
150
+ }
151
+ );
152
+ logger.debug("Cleaned up old build", {
153
+ oldEntrypoint: this.#currentEntrypoint
154
+ });
155
+ } catch {
156
+ }
157
+ }
158
+ } else {
159
+ logger.error("Build errors", { errors: result.errors });
160
+ }
161
+ this.#previousEntrypoint = this.#currentEntrypoint;
162
+ this.#currentEntrypoint = outputPath;
163
+ if (!this.#initialBuildComplete) {
164
+ this.#initialBuildComplete = true;
165
+ this.#initialBuildResolve?.({ success, entrypoint: outputPath });
166
+ } else {
167
+ this.#options.onBuild?.(success, outputPath);
168
+ }
169
+ });
108
170
  }
109
- this.#debouncedBuild();
110
171
  }
111
- }
112
- );
172
+ ],
173
+ sourcemap: "inline",
174
+ minify: false,
175
+ treeShaking: true
176
+ });
177
+ logger.info("Starting esbuild watch mode");
178
+ await this.#ctx.watch();
179
+ return initialBuildPromise;
113
180
  }
114
181
  /**
115
- * Stop watching
182
+ * Stop watching and dispose of esbuild context
116
183
  */
117
184
  async stop() {
118
- if (this.#watcher) {
119
- this.#watcher.close();
120
- this.#watcher = void 0;
185
+ if (this.#ctx) {
186
+ await this.#ctx.dispose();
187
+ this.#ctx = void 0;
121
188
  }
122
189
  }
123
- #timeout;
124
- #debouncedBuild() {
125
- if (this.#timeout) {
126
- clearTimeout(this.#timeout);
127
- }
128
- this.#timeout = setTimeout(() => {
129
- this.#build();
130
- }, 100);
131
- }
132
- async #build() {
133
- if (this.#building)
134
- return;
135
- this.#building = true;
136
- try {
137
- const entryPath = resolve(this.#options.entrypoint);
138
- const outputDir = resolve(this.#options.outDir);
139
- const version = Date.now();
140
- const initialCwd = process.cwd();
141
- let workspaceRoot = initialCwd;
142
- while (workspaceRoot !== dirname2(workspaceRoot)) {
143
- try {
144
- const packageJSON = JSON.parse(
145
- readFileSync(resolve(workspaceRoot, "package.json"), "utf8")
146
- );
147
- if (packageJSON.workspaces) {
148
- break;
149
- }
150
- } catch {
190
+ #findWorkspaceRoot() {
191
+ const initialCwd = process.cwd();
192
+ let workspaceRoot = initialCwd;
193
+ while (workspaceRoot !== dirname2(workspaceRoot)) {
194
+ try {
195
+ const packageJSON = JSON.parse(
196
+ readFileSync(resolve(workspaceRoot, "package.json"), "utf8")
197
+ );
198
+ if (packageJSON.workspaces) {
199
+ return workspaceRoot;
151
200
  }
152
- workspaceRoot = dirname2(workspaceRoot);
201
+ } catch {
153
202
  }
154
- if (workspaceRoot === dirname2(workspaceRoot)) {
155
- workspaceRoot = initialCwd;
156
- }
157
- logger.info("Building", { entryPath });
158
- logger.info("Workspace root", { workspaceRoot });
159
- await mkdir(join(outputDir, "server"), { recursive: true });
160
- await mkdir(join(outputDir, "assets"), { recursive: true });
161
- const result = await ESBuild.build({
162
- entryPoints: [entryPath],
163
- bundle: true,
164
- format: "esm",
165
- target: "es2022",
166
- platform: "node",
167
- outfile: `${outputDir}/server/app.js`,
168
- packages: "external",
169
- absWorkingDir: workspaceRoot,
170
- plugins: [
171
- importMetaPlugin(),
172
- assetsPlugin({
173
- outputDir: `${outputDir}/assets`,
174
- manifest: `${outputDir}/server/asset-manifest.json`
175
- })
176
- ],
177
- sourcemap: "inline",
178
- minify: false,
179
- treeShaking: true
180
- });
181
- if (result.errors.length > 0) {
182
- logger.error("Build errors", { errors: result.errors });
183
- this.#options.onBuild?.(false, version);
184
- } else {
185
- logger.info("Build complete", { version });
186
- this.#options.onBuild?.(true, version);
187
- }
188
- } catch (error) {
189
- logger.error("Build failed", { error });
190
- this.#options.onBuild?.(false, Date.now());
191
- } finally {
192
- this.#building = false;
203
+ workspaceRoot = dirname2(workspaceRoot);
193
204
  }
205
+ return initialCwd;
194
206
  }
195
207
  };
196
208
 
@@ -238,20 +250,20 @@ async function developCommand(entrypoint, options) {
238
250
  const watcher = new Watcher({
239
251
  entrypoint,
240
252
  outDir,
241
- onBuild: async (success, version) => {
253
+ onBuild: async (success, builtEntrypoint2) => {
242
254
  if (success && serviceWorker) {
243
- logger2.info("Reloading Workers", { version });
255
+ logger2.info("Reloading Workers", { entrypoint: builtEntrypoint2 });
244
256
  if (platformInstance && typeof platformInstance.reloadWorkers === "function") {
245
- await platformInstance.reloadWorkers(version);
257
+ await platformInstance.reloadWorkers(builtEntrypoint2);
246
258
  }
247
259
  logger2.info("Workers reloaded", {});
248
260
  }
249
261
  }
250
262
  });
251
- logger2.info("Building", { entrypoint });
252
- await watcher.start();
253
- logger2.info("Build complete, watching for changes", {});
254
- const builtEntrypoint = `${outDir}/server/app.js`;
263
+ const { success: buildSuccess, entrypoint: builtEntrypoint } = await watcher.start();
264
+ if (!buildSuccess) {
265
+ logger2.error("Initial build failed, watching for changes to retry", {});
266
+ }
255
267
  serviceWorker = await platformInstance.loadServiceWorker(builtEntrypoint, {
256
268
  hotReload: true,
257
269
  workerCount
@@ -277,8 +289,7 @@ async function developCommand(entrypoint, options) {
277
289
  process.on("SIGINT", () => shutdown("SIGINT"));
278
290
  process.on("SIGTERM", () => shutdown("SIGTERM"));
279
291
  } catch (error) {
280
- logger2.error("Failed to start development server", {
281
- error: error.message,
292
+ logger2.error("Failed to start development server:\n{stack}", {
282
293
  stack: error.stack
283
294
  });
284
295
  process.exit(1);
@@ -385,8 +396,7 @@ var BUILD_DEFAULTS = {
385
396
  };
386
397
  var BUILD_STRUCTURE = {
387
398
  serverDir: "server",
388
- staticDir: "static",
389
- assetsDir: "static/assets"
399
+ staticDir: "static"
390
400
  };
391
401
  async function buildForProduction({
392
402
  entrypoint,
@@ -414,7 +424,7 @@ async function buildForProduction({
414
424
  if (verbose) {
415
425
  logger5.info("Built app to", { outputDir: buildContext.outputDir });
416
426
  logger5.info("Server files", { dir: buildContext.serverDir });
417
- logger5.info("Asset files", { dir: buildContext.assetsDir });
427
+ logger5.info("Static files", { dir: join2(buildContext.outputDir, "static") });
418
428
  }
419
429
  }
420
430
  async function initializeBuild({
@@ -462,7 +472,6 @@ async function initializeBuild({
462
472
  await mkdir2(outputDir, { recursive: true });
463
473
  await mkdir2(join2(outputDir, BUILD_STRUCTURE.serverDir), { recursive: true });
464
474
  await mkdir2(join2(outputDir, BUILD_STRUCTURE.staticDir), { recursive: true });
465
- await mkdir2(join2(outputDir, BUILD_STRUCTURE.assetsDir), { recursive: true });
466
475
  } catch (error) {
467
476
  throw new Error(
468
477
  `Failed to create output directory structure: ${error.message}`
@@ -472,7 +481,6 @@ async function initializeBuild({
472
481
  entryPath,
473
482
  outputDir,
474
483
  serverDir: join2(outputDir, BUILD_STRUCTURE.serverDir),
475
- assetsDir: join2(outputDir, BUILD_STRUCTURE.assetsDir),
476
484
  workspaceRoot,
477
485
  platform,
478
486
  verbose,
@@ -517,8 +525,8 @@ async function findShovelPackageRoot() {
517
525
  }
518
526
  async function createBuildConfig({
519
527
  entryPath,
528
+ outputDir,
520
529
  serverDir,
521
- assetsDir,
522
530
  workspaceRoot,
523
531
  platform,
524
532
  workerCount
@@ -543,11 +551,15 @@ async function createBuildConfig({
543
551
  absWorkingDir: workspaceRoot || dirname3(entryPath),
544
552
  mainFields: ["module", "main"],
545
553
  conditions: ["import", "module"],
554
+ // Allow user code to import @b9g packages from shovel's packages directory
555
+ nodePaths: [
556
+ join2(shovelRoot, "packages"),
557
+ join2(shovelRoot, "node_modules")
558
+ ],
546
559
  plugins: [
547
560
  importMetaPlugin(),
548
561
  assetsPlugin2({
549
- outputDir: assetsDir,
550
- manifest: join2(serverDir, "asset-manifest.json")
562
+ outDir: outputDir
551
563
  })
552
564
  ],
553
565
  metafile: true,
@@ -614,8 +626,7 @@ async function createBuildConfig({
614
626
  plugins: isCloudflare ? [
615
627
  importMetaPlugin(),
616
628
  assetsPlugin2({
617
- outputDir: assetsDir,
618
- manifest: join2(serverDir, "asset-manifest.json")
629
+ outDir: outputDir
619
630
  })
620
631
  ] : [],
621
632
  // Assets already handled in user code build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.2.0-beta.5",
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": {
@@ -21,30 +21,30 @@
21
21
  "source-map": "^0.7.4"
22
22
  },
23
23
  "devDependencies": {
24
- "@b9g/assets": "^0.1.6",
24
+ "@b9g/assets": "^0.1.12",
25
25
  "@b9g/cache": "^0.1.4",
26
26
  "@b9g/crank": "^0.7.2",
27
- "@b9g/filesystem": "^0.1.5",
28
- "@b9g/http-errors": "^0.1.4",
27
+ "@b9g/filesystem": "^0.1.6",
28
+ "@b9g/http-errors": "^0.1.5",
29
29
  "@b9g/libuild": "^0.1.17",
30
- "@b9g/platform": "^0.1.6",
31
- "@b9g/platform-bun": "^0.1.6",
32
- "@b9g/platform-cloudflare": "^0.1.5",
33
- "@b9g/platform-node": "^0.1.8",
34
- "@b9g/router": "^0.1.6",
30
+ "@b9g/platform": "^0.1.9",
31
+ "@b9g/platform-bun": "^0.1.7",
32
+ "@b9g/platform-cloudflare": "^0.1.6",
33
+ "@b9g/platform-node": "^0.1.9",
34
+ "@b9g/router": "^0.1.7",
35
35
  "@types/bun": "^1.2.2",
36
36
  "mitata": "^1.0.34",
37
37
  "typescript": "^5.7.3"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@b9g/node-webworker": "^0.1.3",
41
- "@b9g/platform": "^0.1.6",
42
- "@b9g/platform-node": "^0.1.8",
43
- "@b9g/platform-cloudflare": "^0.1.5",
44
- "@b9g/platform-bun": "^0.1.6",
41
+ "@b9g/platform": "^0.1.9",
42
+ "@b9g/platform-node": "^0.1.9",
43
+ "@b9g/platform-cloudflare": "^0.1.6",
44
+ "@b9g/platform-bun": "^0.1.7",
45
45
  "@b9g/cache": "^0.1.4",
46
- "@b9g/filesystem": "^0.1.5",
47
- "@b9g/http-errors": "^0.1.4"
46
+ "@b9g/filesystem": "^0.1.6",
47
+ "@b9g/http-errors": "^0.1.5"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "@b9g/platform": {