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

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 +88 -107
  2. package/package.json +4 -4
package/bin/cli.js CHANGED
@@ -23,7 +23,6 @@ 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
28
  import { mkdir } from "fs/promises";
@@ -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,102 @@ 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 = false;
87
+ #initialBuildSuccess = false;
88
+ #initialBuildResolve;
88
89
  constructor(options) {
89
- this.#building = false;
90
90
  this.#options = options;
91
91
  }
92
92
  /**
93
93
  * Start watching and building
94
+ * @returns true if initial build succeeded, false if it failed
94
95
  */
95
96
  async start() {
96
97
  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;
98
+ const outputDir = resolve(this.#options.outDir);
99
+ const workspaceRoot = this.#findWorkspaceRoot();
100
+ await mkdir(join(outputDir, "server"), { recursive: true });
101
+ await mkdir(join(outputDir, "static"), { recursive: true });
102
+ const initialBuildPromise = new Promise((resolve3) => {
103
+ this.#initialBuildResolve = resolve3;
104
+ });
105
+ this.#ctx = await ESBuild.context({
106
+ entryPoints: [entryPath],
107
+ bundle: true,
108
+ format: "esm",
109
+ target: "es2022",
110
+ platform: "node",
111
+ outfile: `${outputDir}/server/app.js`,
112
+ // No packages: "external" - bundle everything for dev/prod parity
113
+ absWorkingDir: workspaceRoot,
114
+ plugins: [
115
+ importMetaPlugin(),
116
+ assetsPlugin({
117
+ outDir: outputDir
118
+ }),
119
+ // Plugin to detect build completion (works with watch mode)
120
+ {
121
+ name: "build-notify",
122
+ setup: (build2) => {
123
+ build2.onStart(() => {
124
+ logger.info("Building", {
125
+ entrypoint: this.#options.entrypoint
126
+ });
127
+ });
128
+ build2.onEnd((result) => {
129
+ const version = Date.now();
130
+ const success = result.errors.length === 0;
131
+ if (success) {
132
+ logger.info("Build complete", { version });
133
+ } else {
134
+ logger.error("Build errors", { errors: result.errors });
135
+ }
136
+ if (!this.#initialBuildComplete) {
137
+ this.#initialBuildComplete = true;
138
+ this.#initialBuildSuccess = success;
139
+ this.#initialBuildResolve?.(success);
140
+ } else {
141
+ this.#options.onBuild?.(success, version);
142
+ }
143
+ });
108
144
  }
109
- this.#debouncedBuild();
110
145
  }
111
- }
112
- );
146
+ ],
147
+ sourcemap: "inline",
148
+ minify: false,
149
+ treeShaking: true
150
+ });
151
+ logger.info("Starting esbuild watch mode");
152
+ await this.#ctx.watch();
153
+ return initialBuildPromise;
113
154
  }
114
155
  /**
115
- * Stop watching
156
+ * Stop watching and dispose of esbuild context
116
157
  */
117
158
  async stop() {
118
- if (this.#watcher) {
119
- this.#watcher.close();
120
- this.#watcher = void 0;
159
+ if (this.#ctx) {
160
+ await this.#ctx.dispose();
161
+ this.#ctx = void 0;
121
162
  }
122
163
  }
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 {
164
+ #findWorkspaceRoot() {
165
+ const initialCwd = process.cwd();
166
+ let workspaceRoot = initialCwd;
167
+ while (workspaceRoot !== dirname2(workspaceRoot)) {
168
+ try {
169
+ const packageJSON = JSON.parse(
170
+ readFileSync(resolve(workspaceRoot, "package.json"), "utf8")
171
+ );
172
+ if (packageJSON.workspaces) {
173
+ return workspaceRoot;
151
174
  }
152
- workspaceRoot = dirname2(workspaceRoot);
175
+ } catch {
153
176
  }
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;
177
+ workspaceRoot = dirname2(workspaceRoot);
193
178
  }
179
+ return initialCwd;
194
180
  }
195
181
  };
196
182
 
@@ -248,9 +234,10 @@ async function developCommand(entrypoint, options) {
248
234
  }
249
235
  }
250
236
  });
251
- logger2.info("Building", { entrypoint });
252
- await watcher.start();
253
- logger2.info("Build complete, watching for changes", {});
237
+ const buildSuccess = await watcher.start();
238
+ if (!buildSuccess) {
239
+ logger2.error("Initial build failed, watching for changes to retry", {});
240
+ }
254
241
  const builtEntrypoint = `${outDir}/server/app.js`;
255
242
  serviceWorker = await platformInstance.loadServiceWorker(builtEntrypoint, {
256
243
  hotReload: true,
@@ -277,8 +264,7 @@ async function developCommand(entrypoint, options) {
277
264
  process.on("SIGINT", () => shutdown("SIGINT"));
278
265
  process.on("SIGTERM", () => shutdown("SIGTERM"));
279
266
  } catch (error) {
280
- logger2.error("Failed to start development server", {
281
- error: error.message,
267
+ logger2.error("Failed to start development server:\n{stack}", {
282
268
  stack: error.stack
283
269
  });
284
270
  process.exit(1);
@@ -385,8 +371,7 @@ var BUILD_DEFAULTS = {
385
371
  };
386
372
  var BUILD_STRUCTURE = {
387
373
  serverDir: "server",
388
- staticDir: "static",
389
- assetsDir: "static/assets"
374
+ staticDir: "static"
390
375
  };
391
376
  async function buildForProduction({
392
377
  entrypoint,
@@ -414,7 +399,7 @@ async function buildForProduction({
414
399
  if (verbose) {
415
400
  logger5.info("Built app to", { outputDir: buildContext.outputDir });
416
401
  logger5.info("Server files", { dir: buildContext.serverDir });
417
- logger5.info("Asset files", { dir: buildContext.assetsDir });
402
+ logger5.info("Static files", { dir: join2(buildContext.outputDir, "static") });
418
403
  }
419
404
  }
420
405
  async function initializeBuild({
@@ -462,7 +447,6 @@ async function initializeBuild({
462
447
  await mkdir2(outputDir, { recursive: true });
463
448
  await mkdir2(join2(outputDir, BUILD_STRUCTURE.serverDir), { recursive: true });
464
449
  await mkdir2(join2(outputDir, BUILD_STRUCTURE.staticDir), { recursive: true });
465
- await mkdir2(join2(outputDir, BUILD_STRUCTURE.assetsDir), { recursive: true });
466
450
  } catch (error) {
467
451
  throw new Error(
468
452
  `Failed to create output directory structure: ${error.message}`
@@ -472,7 +456,6 @@ async function initializeBuild({
472
456
  entryPath,
473
457
  outputDir,
474
458
  serverDir: join2(outputDir, BUILD_STRUCTURE.serverDir),
475
- assetsDir: join2(outputDir, BUILD_STRUCTURE.assetsDir),
476
459
  workspaceRoot,
477
460
  platform,
478
461
  verbose,
@@ -517,8 +500,8 @@ async function findShovelPackageRoot() {
517
500
  }
518
501
  async function createBuildConfig({
519
502
  entryPath,
503
+ outputDir,
520
504
  serverDir,
521
- assetsDir,
522
505
  workspaceRoot,
523
506
  platform,
524
507
  workerCount
@@ -546,8 +529,7 @@ async function createBuildConfig({
546
529
  plugins: [
547
530
  importMetaPlugin(),
548
531
  assetsPlugin2({
549
- outputDir: assetsDir,
550
- manifest: join2(serverDir, "asset-manifest.json")
532
+ outDir: outputDir
551
533
  })
552
534
  ],
553
535
  metafile: true,
@@ -614,8 +596,7 @@ async function createBuildConfig({
614
596
  plugins: isCloudflare ? [
615
597
  importMetaPlugin(),
616
598
  assetsPlugin2({
617
- outputDir: assetsDir,
618
- manifest: join2(serverDir, "asset-manifest.json")
599
+ outDir: outputDir
619
600
  })
620
601
  ] : [],
621
602
  // 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.4",
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,13 +21,13 @@
21
21
  "source-map": "^0.7.4"
22
22
  },
23
23
  "devDependencies": {
24
- "@b9g/assets": "^0.1.6",
24
+ "@b9g/assets": "^0.1.9",
25
25
  "@b9g/cache": "^0.1.4",
26
26
  "@b9g/crank": "^0.7.2",
27
27
  "@b9g/filesystem": "^0.1.5",
28
28
  "@b9g/http-errors": "^0.1.4",
29
29
  "@b9g/libuild": "^0.1.17",
30
- "@b9g/platform": "^0.1.6",
30
+ "@b9g/platform": "^0.1.7",
31
31
  "@b9g/platform-bun": "^0.1.6",
32
32
  "@b9g/platform-cloudflare": "^0.1.5",
33
33
  "@b9g/platform-node": "^0.1.8",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@b9g/node-webworker": "^0.1.3",
41
- "@b9g/platform": "^0.1.6",
41
+ "@b9g/platform": "^0.1.7",
42
42
  "@b9g/platform-node": "^0.1.8",
43
43
  "@b9g/platform-cloudflare": "^0.1.5",
44
44
  "@b9g/platform-bun": "^0.1.6",