@b9g/shovel 0.2.0-beta.7 → 0.2.0-beta.9

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 (4) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +380 -181
  3. package/bin/create.js +55 -13
  4. package/package.json +16 -16
package/README.md CHANGED
@@ -29,7 +29,7 @@ Shovel implements web platform APIs that server-side JavaScript is missing:
29
29
  |-----|----------|--------------|
30
30
  | `fetch` event | [Service Workers](https://w3c.github.io/ServiceWorker/) | Request handling |
31
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) |
32
+ | `self.buckets` | [FileSystem API](https://fs.spec.whatwg.org/) | Storage (local, S3, R2) |
33
33
  | `self.cookieStore` | [Cookie Store API](https://wicg.github.io/cookie-store/) | Cookie management |
34
34
  | `URLPattern` | [URLPattern](https://urlpattern.spec.whatwg.org/) | Route matching (100% WPT) |
35
35
  | `AsyncContext.Variable` | [TC39 Stage 2](https://github.com/tc39/proposal-async-context) | Request-scoped state |
package/bin/cli.js CHANGED
@@ -20,11 +20,11 @@ var DEFAULTS = {
20
20
  import { configure, getConsoleSink, getLogger as getLogger2 } from "@logtape/logtape";
21
21
  import { AsyncContext } from "@b9g/async-context";
22
22
  import * as Platform from "@b9g/platform";
23
+ import { loadConfig } from "@b9g/platform/config";
23
24
 
24
25
  // src/esbuild/watcher.ts
25
26
  import * as ESBuild from "esbuild";
26
- import { existsSync } from "fs";
27
- import { resolve, join, dirname as dirname2 } from "path";
27
+ import { resolve, join as join3 } from "path";
28
28
  import { mkdir } from "fs/promises";
29
29
  import { assetsPlugin } from "@b9g/assets/plugin";
30
30
 
@@ -35,8 +35,8 @@ import { pathToFileURL } from "url";
35
35
  function importMetaPlugin() {
36
36
  return {
37
37
  name: "import-meta-transform",
38
- setup(build2) {
39
- build2.onLoad({ filter: /\.[jt]sx?$/, namespace: "file" }, async (args) => {
38
+ setup(build3) {
39
+ build3.onLoad({ filter: /\.[jt]sx?$/, namespace: "file" }, async (args) => {
40
40
  if (args.path.includes("node_modules")) {
41
41
  return null;
42
42
  }
@@ -77,19 +77,161 @@ function importMetaPlugin() {
77
77
  };
78
78
  }
79
79
 
80
- // src/esbuild/watcher.ts
81
- import { getLogger } from "@logtape/logtape";
82
- var logger = getLogger(["watcher"]);
83
- function findProjectRoot() {
84
- let dir = process.cwd();
80
+ // src/esbuild/jsx-config.ts
81
+ import { readFile as readFile2 } from "fs/promises";
82
+ import { join, dirname as dirname2 } from "path";
83
+ import { existsSync } from "fs";
84
+ var CRANK_JSX_DEFAULTS = {
85
+ jsx: "automatic",
86
+ jsxImportSource: "@b9g/crank"
87
+ };
88
+ async function findTsConfig(startDir) {
89
+ let dir = startDir;
85
90
  while (dir !== dirname2(dir)) {
86
- if (existsSync(join(dir, "package.json"))) {
87
- return dir;
91
+ const tsconfigPath = join(dir, "tsconfig.json");
92
+ if (existsSync(tsconfigPath)) {
93
+ return tsconfigPath;
88
94
  }
89
95
  dir = dirname2(dir);
90
96
  }
91
- return process.cwd();
97
+ return null;
98
+ }
99
+ async function parseTsConfig(tsconfigPath) {
100
+ const content = await readFile2(tsconfigPath, "utf8");
101
+ const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
102
+ const config = JSON.parse(stripped);
103
+ if (config.extends) {
104
+ const baseDir = dirname2(tsconfigPath);
105
+ let extendsPath = config.extends;
106
+ if (extendsPath.startsWith(".")) {
107
+ extendsPath = join(baseDir, extendsPath);
108
+ } else {
109
+ extendsPath = join(baseDir, "node_modules", extendsPath);
110
+ }
111
+ if (!extendsPath.endsWith(".json")) {
112
+ extendsPath += ".json";
113
+ }
114
+ if (existsSync(extendsPath)) {
115
+ const baseConfig = await parseTsConfig(extendsPath);
116
+ return {
117
+ ...baseConfig,
118
+ ...config,
119
+ compilerOptions: {
120
+ ...baseConfig.compilerOptions,
121
+ ...config.compilerOptions
122
+ }
123
+ };
124
+ }
125
+ }
126
+ return config;
127
+ }
128
+ function mapTsConfigToEsbuild(compilerOptions) {
129
+ const options = {};
130
+ if (compilerOptions.jsx) {
131
+ switch (compilerOptions.jsx) {
132
+ case "react":
133
+ case "react-native":
134
+ options.jsx = "transform";
135
+ break;
136
+ case "react-jsx":
137
+ case "react-jsxdev":
138
+ options.jsx = "automatic";
139
+ break;
140
+ case "preserve":
141
+ options.jsx = "preserve";
142
+ break;
143
+ }
144
+ }
145
+ if (compilerOptions.jsxFactory) {
146
+ options.jsxFactory = compilerOptions.jsxFactory;
147
+ }
148
+ if (compilerOptions.jsxFragmentFactory) {
149
+ options.jsxFragment = compilerOptions.jsxFragmentFactory;
150
+ }
151
+ if (compilerOptions.jsxImportSource) {
152
+ options.jsxImportSource = compilerOptions.jsxImportSource;
153
+ }
154
+ return options;
155
+ }
156
+ async function loadJSXConfig(projectRoot) {
157
+ const tsconfigPath = await findTsConfig(projectRoot);
158
+ if (tsconfigPath) {
159
+ try {
160
+ const config = await parseTsConfig(tsconfigPath);
161
+ const compilerOptions = config.compilerOptions || {};
162
+ const hasJsxConfig = compilerOptions.jsx || compilerOptions.jsxFactory || compilerOptions.jsxFragmentFactory || compilerOptions.jsxImportSource;
163
+ if (hasJsxConfig) {
164
+ const tsOptions = mapTsConfigToEsbuild(compilerOptions);
165
+ return {
166
+ ...CRANK_JSX_DEFAULTS,
167
+ ...tsOptions
168
+ };
169
+ }
170
+ } catch (err) {
171
+ if (!(err instanceof SyntaxError) || !/^(Unexpected token|Expected|JSON)/i.test(
172
+ String(err.message)
173
+ )) {
174
+ throw err;
175
+ }
176
+ }
177
+ }
178
+ return { ...CRANK_JSX_DEFAULTS };
179
+ }
180
+ function applyJSXOptions(buildOptions, jsxOptions) {
181
+ if (jsxOptions.jsx) {
182
+ buildOptions.jsx = jsxOptions.jsx;
183
+ }
184
+ if (jsxOptions.jsxFactory) {
185
+ buildOptions.jsxFactory = jsxOptions.jsxFactory;
186
+ }
187
+ if (jsxOptions.jsxFragment) {
188
+ buildOptions.jsxFragment = jsxOptions.jsxFragment;
189
+ }
190
+ if (jsxOptions.jsxImportSource) {
191
+ buildOptions.jsxImportSource = jsxOptions.jsxImportSource;
192
+ }
193
+ if (jsxOptions.jsxSideEffects !== void 0) {
194
+ buildOptions.jsxSideEffects = jsxOptions.jsxSideEffects;
195
+ }
92
196
  }
197
+
198
+ // src/utils/project.ts
199
+ import { existsSync as existsSync2, readFileSync } from "fs";
200
+ import { dirname as dirname3, join as join2 } from "path";
201
+ function findProjectRoot(startDir = process.cwd()) {
202
+ let dir = startDir;
203
+ while (dir !== dirname3(dir)) {
204
+ if (existsSync2(join2(dir, "package.json"))) {
205
+ return dir;
206
+ }
207
+ dir = dirname3(dir);
208
+ }
209
+ return startDir;
210
+ }
211
+ function findWorkspaceRoot(startDir = process.cwd()) {
212
+ let dir = startDir;
213
+ while (dir !== dirname3(dir)) {
214
+ const packageJsonPath = join2(dir, "package.json");
215
+ if (existsSync2(packageJsonPath)) {
216
+ try {
217
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
218
+ if (packageJson.workspaces) {
219
+ return dir;
220
+ }
221
+ } catch {
222
+ }
223
+ }
224
+ dir = dirname3(dir);
225
+ }
226
+ return null;
227
+ }
228
+ function getNodeModulesPath(startDir) {
229
+ return join2(findProjectRoot(startDir), "node_modules");
230
+ }
231
+
232
+ // src/esbuild/watcher.ts
233
+ import { getLogger } from "@logtape/logtape";
234
+ var logger = getLogger(["watcher"]);
93
235
  var Watcher = class {
94
236
  #options;
95
237
  #ctx;
@@ -110,12 +252,13 @@ var Watcher = class {
110
252
  async start() {
111
253
  const entryPath = resolve(this.#projectRoot, this.#options.entrypoint);
112
254
  const outputDir = resolve(this.#projectRoot, this.#options.outDir);
113
- await mkdir(join(outputDir, "server"), { recursive: true });
114
- await mkdir(join(outputDir, "static"), { recursive: true });
115
- const initialBuildPromise = new Promise((resolve3) => {
116
- this.#initialBuildResolve = resolve3;
255
+ await mkdir(join3(outputDir, "server"), { recursive: true });
256
+ await mkdir(join3(outputDir, "static"), { recursive: true });
257
+ const jsxOptions = await loadJSXConfig(this.#projectRoot);
258
+ const initialBuildPromise = new Promise((resolve4) => {
259
+ this.#initialBuildResolve = resolve4;
117
260
  });
118
- this.#ctx = await ESBuild.context({
261
+ const buildOptions = {
119
262
  entryPoints: [entryPath],
120
263
  bundle: true,
121
264
  format: "esm",
@@ -128,19 +271,43 @@ var Watcher = class {
128
271
  plugins: [
129
272
  importMetaPlugin(),
130
273
  assetsPlugin({
131
- outDir: outputDir
274
+ outDir: outputDir,
275
+ clientBuild: {
276
+ jsx: jsxOptions.jsx,
277
+ jsxFactory: jsxOptions.jsxFactory,
278
+ jsxFragment: jsxOptions.jsxFragment,
279
+ jsxImportSource: jsxOptions.jsxImportSource
280
+ }
132
281
  }),
133
282
  // Plugin to detect build completion (works with watch mode)
134
283
  {
135
284
  name: "build-notify",
136
- setup: (build2) => {
137
- build2.onStart(() => {
285
+ setup: (build3) => {
286
+ build3.onStart(() => {
138
287
  logger.info("Building", {
139
288
  entrypoint: this.#options.entrypoint
140
289
  });
141
290
  });
142
- build2.onEnd(async (result) => {
143
- const success = result.errors.length === 0;
291
+ build3.onEnd(async (result) => {
292
+ let success = result.errors.length === 0;
293
+ const dynamicImportWarnings = (result.warnings || []).filter(
294
+ (w) => w.text.includes("cannot be bundled") || w.text.includes("import() call") || w.text.includes("dynamic import")
295
+ );
296
+ if (dynamicImportWarnings.length > 0) {
297
+ success = false;
298
+ for (const warning of dynamicImportWarnings) {
299
+ const loc = warning.location;
300
+ const file = loc?.file || "unknown";
301
+ const line = loc?.line || "?";
302
+ logger.error(
303
+ "Non-analyzable dynamic import at {file}:{line}: {text}",
304
+ { file, line, text: warning.text }
305
+ );
306
+ }
307
+ logger.error(
308
+ "Dynamic imports must use literal strings, not variables. For config-driven providers, ensure they are registered in shovel.json."
309
+ );
310
+ }
144
311
  let outputPath = "";
145
312
  if (result.metafile) {
146
313
  const outputs = Object.keys(result.metafile.outputs);
@@ -152,7 +319,7 @@ var Watcher = class {
152
319
  if (success) {
153
320
  logger.info("Build complete", { entrypoint: outputPath });
154
321
  } else {
155
- logger.error("Build errors", { errors: result.errors });
322
+ logger.error("Build errors: {errors}", { errors: result.errors });
156
323
  }
157
324
  this.#currentEntrypoint = outputPath;
158
325
  if (!this.#initialBuildComplete) {
@@ -168,7 +335,9 @@ var Watcher = class {
168
335
  sourcemap: "inline",
169
336
  minify: false,
170
337
  treeShaking: true
171
- });
338
+ };
339
+ applyJSXOptions(buildOptions, jsxOptions);
340
+ this.#ctx = await ESBuild.context(buildOptions);
172
341
  logger.info("Starting esbuild watch mode");
173
342
  await this.#ctx.watch();
174
343
  return initialBuildPromise;
@@ -206,21 +375,18 @@ await configure({
206
375
  var logger2 = getLogger2(["cli"]);
207
376
  async function developCommand(entrypoint, options) {
208
377
  try {
209
- const platformName = Platform.resolvePlatform(options);
210
- const workerCount = getWorkerCount(options);
378
+ const projectRoot = findProjectRoot();
379
+ const config = loadConfig(projectRoot);
380
+ const platformName = Platform.resolvePlatform({ ...options, config });
381
+ const workerCount = getWorkerCount(options, config);
211
382
  if (options.verbose) {
212
383
  Platform.displayPlatformInfo(platformName);
213
384
  logger2.info("Worker configuration", { workerCount });
214
385
  }
215
- const platformConfig = {
216
- hotReload: true,
386
+ const platformInstance = await Platform.createPlatform(platformName, {
217
387
  port: parseInt(options.port) || DEFAULTS.SERVER.PORT,
218
388
  host: options.host || DEFAULTS.SERVER.HOST
219
- };
220
- const platformInstance = await Platform.createPlatform(
221
- platformName,
222
- platformConfig
223
- );
389
+ });
224
390
  logger2.info("Starting development server", {});
225
391
  logger2.info("Workers", { workerCount });
226
392
  let serviceWorker;
@@ -228,6 +394,7 @@ async function developCommand(entrypoint, options) {
228
394
  const watcher = new Watcher({
229
395
  entrypoint,
230
396
  outDir,
397
+ config,
231
398
  onBuild: async (success, builtEntrypoint2) => {
232
399
  if (success && serviceWorker) {
233
400
  logger2.info("Reloading Workers", { entrypoint: builtEntrypoint2 });
@@ -273,40 +440,44 @@ async function developCommand(entrypoint, options) {
273
440
  process.exit(1);
274
441
  }
275
442
  }
276
- function getWorkerCount(options) {
443
+ function getWorkerCount(options, config) {
277
444
  if (options.workers) {
278
445
  return parseInt(options.workers);
279
446
  }
280
447
  if (process.env.WORKER_COUNT) {
281
448
  return parseInt(process.env.WORKER_COUNT);
282
449
  }
450
+ if (config?.workers) {
451
+ return config.workers;
452
+ }
283
453
  return DEFAULTS.WORKERS;
284
454
  }
285
455
 
286
456
  // src/commands/activate.ts
287
457
  import { getLogger as getLogger3 } from "@logtape/logtape";
288
458
  import * as Platform2 from "@b9g/platform";
459
+ import * as ESBuild2 from "esbuild";
460
+ import { resolve as resolve2, join as join4 } from "path";
461
+ import { mkdir as mkdir2 } from "fs/promises";
462
+ import { assetsPlugin as assetsPlugin2 } from "@b9g/assets/plugin";
289
463
  var logger3 = getLogger3(["cli"]);
290
464
  async function activateCommand(entrypoint, options) {
291
465
  try {
292
466
  const platformName = Platform2.resolvePlatform(options);
293
467
  const workerCount = getWorkerCount2(options);
294
- if (options.verbose) {
295
- Platform2.displayPlatformInfo(platformName);
296
- logger3.info("Worker configuration", { workerCount });
297
- }
298
- const platformConfig = {
299
- hotReload: false
300
- };
301
- const platformInstance = await Platform2.createPlatform(
302
- platformName,
303
- platformConfig
304
- );
468
+ logger3.debug("Platform: {platform}", { platform: platformName });
469
+ logger3.debug("Worker count: {workerCount}", { workerCount });
470
+ logger3.info("Building ServiceWorker for activation");
471
+ const builtEntrypoint = await buildForActivate(entrypoint);
472
+ const platformInstance = await Platform2.createPlatform(platformName);
305
473
  logger3.info("Activating ServiceWorker", {});
306
- const serviceWorker = await platformInstance.loadServiceWorker(entrypoint, {
307
- hotReload: false,
308
- workerCount
309
- });
474
+ const serviceWorker = await platformInstance.loadServiceWorker(
475
+ builtEntrypoint,
476
+ {
477
+ hotReload: false,
478
+ workerCount
479
+ }
480
+ );
310
481
  logger3.info(
311
482
  "ServiceWorker activated - check dist/ for generated content",
312
483
  {}
@@ -314,13 +485,50 @@ async function activateCommand(entrypoint, options) {
314
485
  await serviceWorker.dispose();
315
486
  await platformInstance.dispose();
316
487
  } catch (error) {
317
- logger3.error("ServiceWorker activation failed", { error: error.message });
318
- if (options.verbose) {
319
- logger3.error("Stack trace", { stack: error.stack });
320
- }
488
+ logger3.error("ServiceWorker activation failed: {error}", { error });
321
489
  process.exit(1);
322
490
  }
323
491
  }
492
+ async function buildForActivate(entrypoint) {
493
+ const entryPath = resolve2(entrypoint);
494
+ const outputDir = resolve2("dist");
495
+ const serverDir = join4(outputDir, "server");
496
+ await mkdir2(serverDir, { recursive: true });
497
+ await mkdir2(join4(outputDir, "static"), { recursive: true });
498
+ const projectRoot = findProjectRoot();
499
+ const jsxOptions = await loadJSXConfig(projectRoot);
500
+ const outfile = join4(serverDir, "server.js");
501
+ const buildConfig = {
502
+ entryPoints: [entryPath],
503
+ bundle: true,
504
+ format: "esm",
505
+ target: "es2022",
506
+ platform: "node",
507
+ outfile,
508
+ absWorkingDir: projectRoot,
509
+ mainFields: ["module", "main"],
510
+ conditions: ["import", "module"],
511
+ nodePaths: [getNodeModulesPath()],
512
+ plugins: [
513
+ importMetaPlugin(),
514
+ assetsPlugin2({
515
+ outDir: outputDir,
516
+ clientBuild: {
517
+ jsx: jsxOptions.jsx,
518
+ jsxFactory: jsxOptions.jsxFactory,
519
+ jsxFragment: jsxOptions.jsxFragment,
520
+ jsxImportSource: jsxOptions.jsxImportSource
521
+ }
522
+ })
523
+ ],
524
+ external: ["node:*"]
525
+ };
526
+ applyJSXOptions(buildConfig, jsxOptions);
527
+ logger3.debug("Building entrypoint: {entryPath}", { entryPath, outfile });
528
+ await ESBuild2.build(buildConfig);
529
+ logger3.debug("Build complete: {outfile}", { outfile });
530
+ return outfile;
531
+ }
324
532
  function getWorkerCount2(options) {
325
533
  if (options.workers) {
326
534
  return parseInt(options.workers);
@@ -343,13 +551,15 @@ async function infoCommand() {
343
551
  }
344
552
 
345
553
  // src/commands/build.ts
346
- import * as ESBuild2 from "esbuild";
347
- import { resolve as resolve2, join as join2, dirname as dirname3 } from "path";
348
- import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
554
+ import * as ESBuild3 from "esbuild";
555
+ import { resolve as resolve3, join as join5, dirname as dirname4 } from "path";
556
+ import { mkdir as mkdir3, readFile as readFile3, writeFile } from "fs/promises";
349
557
  import { fileURLToPath } from "url";
350
- import { assetsPlugin as assetsPlugin2 } from "@b9g/assets/plugin";
558
+ import { assetsPlugin as assetsPlugin3 } from "@b9g/assets/plugin";
351
559
  import { configure as configure2, getConsoleSink as getConsoleSink2, getLogger as getLogger5 } from "@logtape/logtape";
352
560
  import { AsyncContext as AsyncContext2 } from "@b9g/async-context";
561
+ import * as Platform3 from "@b9g/platform";
562
+ import { loadConfig as loadConfig2 } from "@b9g/platform/config";
353
563
  await configure2({
354
564
  reset: true,
355
565
  contextLocalStorage: new AsyncContext2.Variable(),
@@ -363,6 +573,26 @@ await configure2({
363
573
  ]
364
574
  });
365
575
  var logger5 = getLogger5(["cli"]);
576
+ function validateDynamicImports(result, context2) {
577
+ const dynamicImportWarnings = (result.warnings || []).filter(
578
+ (w) => w.text.includes("cannot be bundled") || w.text.includes("import() call") || w.text.includes("dynamic import")
579
+ );
580
+ if (dynamicImportWarnings.length > 0) {
581
+ const locations = dynamicImportWarnings.map((w) => {
582
+ const loc = w.location;
583
+ const file = loc?.file || "unknown";
584
+ const line = loc?.line || "?";
585
+ return ` ${file}:${line} - ${w.text}`;
586
+ }).join("\n");
587
+ throw new Error(
588
+ `Build failed (${context2}): Non-analyzable dynamic imports found:
589
+ ${locations}
590
+
591
+ Dynamic imports must use literal strings, not variables.
592
+ For config-driven providers, ensure they are registered in shovel.json.`
593
+ );
594
+ }
595
+ }
366
596
  var BUILD_DEFAULTS = {
367
597
  format: "esm",
368
598
  target: "es2022",
@@ -390,7 +620,8 @@ async function buildForProduction({
390
620
  workerCount
391
621
  });
392
622
  const buildConfig = await createBuildConfig(buildContext);
393
- const result = await ESBuild2.build(buildConfig);
623
+ const result = await ESBuild3.build(buildConfig);
624
+ validateDynamicImports(result, "main bundle");
394
625
  if (verbose && result.metafile) {
395
626
  await logBundleAnalysis(result.metafile);
396
627
  }
@@ -401,7 +632,7 @@ async function buildForProduction({
401
632
  if (verbose) {
402
633
  logger5.info("Built app to", { outputDir: buildContext.outputDir });
403
634
  logger5.info("Server files", { dir: buildContext.serverDir });
404
- logger5.info("Static files", { dir: join2(buildContext.outputDir, "static") });
635
+ logger5.info("Static files", { dir: join5(buildContext.outputDir, "static") });
405
636
  }
406
637
  }
407
638
  async function initializeBuild({
@@ -422,10 +653,10 @@ async function initializeBuild({
422
653
  logger5.info("Output:", { dir: outDir });
423
654
  logger5.info("Target platform:", { platform });
424
655
  }
425
- const entryPath = resolve2(entrypoint);
426
- const outputDir = resolve2(outDir);
656
+ const entryPath = resolve3(entrypoint);
657
+ const outputDir = resolve3(outDir);
427
658
  try {
428
- const stats = await readFile2(entryPath, "utf8");
659
+ const stats = await readFile3(entryPath, "utf8");
429
660
  if (stats.length === 0) {
430
661
  logger5.warn("Entry point is empty", { entryPath });
431
662
  }
@@ -438,17 +669,17 @@ async function initializeBuild({
438
669
  `Invalid platform: ${platform}. Valid platforms: ${validPlatforms.join(", ")}`
439
670
  );
440
671
  }
441
- const workspaceRoot = await findWorkspaceRoot();
672
+ const projectRoot = findProjectRoot();
442
673
  if (verbose) {
443
674
  logger5.info("Entry:", { entryPath });
444
675
  logger5.info("Output:", { outputDir });
445
676
  logger5.info("Target platform:", { platform });
446
- logger5.info("Workspace root:", { workspaceRoot });
677
+ logger5.info("Project root:", { projectRoot });
447
678
  }
448
679
  try {
449
- await mkdir2(outputDir, { recursive: true });
450
- await mkdir2(join2(outputDir, BUILD_STRUCTURE.serverDir), { recursive: true });
451
- await mkdir2(join2(outputDir, BUILD_STRUCTURE.staticDir), { recursive: true });
680
+ await mkdir3(outputDir, { recursive: true });
681
+ await mkdir3(join5(outputDir, BUILD_STRUCTURE.serverDir), { recursive: true });
682
+ await mkdir3(join5(outputDir, BUILD_STRUCTURE.staticDir), { recursive: true });
452
683
  } catch (error) {
453
684
  throw new Error(
454
685
  `Failed to create output directory structure: ${error.message}`
@@ -457,58 +688,23 @@ async function initializeBuild({
457
688
  return {
458
689
  entryPath,
459
690
  outputDir,
460
- serverDir: join2(outputDir, BUILD_STRUCTURE.serverDir),
461
- workspaceRoot,
691
+ serverDir: join5(outputDir, BUILD_STRUCTURE.serverDir),
692
+ projectRoot,
462
693
  platform,
463
694
  verbose,
464
695
  workerCount
465
696
  };
466
697
  }
467
- async function findWorkspaceRoot() {
468
- let workspaceRoot = process.cwd();
469
- while (workspaceRoot !== dirname3(workspaceRoot)) {
470
- try {
471
- const packageJSON = JSON.parse(
472
- await readFile2(resolve2(workspaceRoot, "package.json"), "utf8")
473
- );
474
- if (packageJSON.workspaces) {
475
- return workspaceRoot;
476
- }
477
- } catch {
478
- }
479
- workspaceRoot = dirname3(workspaceRoot);
480
- }
481
- return workspaceRoot;
482
- }
483
- async function findShovelPackageRoot() {
484
- let currentDir = dirname3(fileURLToPath(import.meta.url));
485
- let packageRoot = currentDir;
486
- while (packageRoot !== dirname3(packageRoot)) {
487
- try {
488
- const packageJSONPath = join2(packageRoot, "package.json");
489
- const content = await readFile2(packageJSONPath, "utf8");
490
- const pkg2 = JSON.parse(content);
491
- if (pkg2.name === "@b9g/shovel" || pkg2.name === "shovel") {
492
- if (packageRoot.endsWith("/dist") || packageRoot.endsWith("\\dist")) {
493
- return dirname3(packageRoot);
494
- }
495
- return packageRoot;
496
- }
497
- } catch {
498
- }
499
- packageRoot = dirname3(packageRoot);
500
- }
501
- return currentDir;
502
- }
503
698
  async function createBuildConfig({
504
699
  entryPath,
505
700
  outputDir,
506
701
  serverDir,
507
- workspaceRoot,
702
+ projectRoot,
508
703
  platform,
509
704
  workerCount
510
705
  }) {
511
706
  const isCloudflare = platform === "cloudflare" || platform === "cloudflare-workers";
707
+ const jsxOptions = await loadJSXConfig(projectRoot || dirname4(entryPath));
512
708
  try {
513
709
  const virtualEntry = await createVirtualEntry(
514
710
  entryPath,
@@ -516,7 +712,6 @@ async function createBuildConfig({
516
712
  workerCount
517
713
  );
518
714
  const external = ["node:*"];
519
- const shovelRoot = await findShovelPackageRoot();
520
715
  if (!isCloudflare) {
521
716
  const userBuildConfig = {
522
717
  entryPoints: [entryPath],
@@ -524,19 +719,22 @@ async function createBuildConfig({
524
719
  format: BUILD_DEFAULTS.format,
525
720
  target: BUILD_DEFAULTS.target,
526
721
  platform: "node",
527
- outfile: join2(serverDir, "server.js"),
528
- absWorkingDir: workspaceRoot || dirname3(entryPath),
722
+ outfile: join5(serverDir, "server.js"),
723
+ absWorkingDir: projectRoot,
529
724
  mainFields: ["module", "main"],
530
725
  conditions: ["import", "module"],
531
- // Allow user code to import @b9g packages from shovel's packages directory
532
- nodePaths: [
533
- join2(shovelRoot, "packages"),
534
- join2(shovelRoot, "node_modules")
535
- ],
726
+ // Resolve packages from the user's project node_modules
727
+ nodePaths: [getNodeModulesPath()],
536
728
  plugins: [
537
729
  importMetaPlugin(),
538
- assetsPlugin2({
539
- outDir: outputDir
730
+ assetsPlugin3({
731
+ outDir: outputDir,
732
+ clientBuild: {
733
+ jsx: jsxOptions.jsx,
734
+ jsxFactory: jsxOptions.jsxFactory,
735
+ jsxFragment: jsxOptions.jsxFragment,
736
+ jsxImportSource: jsxOptions.jsxImportSource
737
+ }
540
738
  })
541
739
  ],
542
740
  metafile: true,
@@ -548,43 +746,29 @@ async function createBuildConfig({
548
746
  define: platform === "node" ? { "import.meta.env": "process.env" } : {},
549
747
  external
550
748
  };
551
- await ESBuild2.build(userBuildConfig);
552
- const runtimeSourcePath = join2(
553
- shovelRoot,
554
- "packages/platform/dist/src/runtime.js"
749
+ applyJSXOptions(userBuildConfig, jsxOptions);
750
+ const userBuildResult = await ESBuild3.build(userBuildConfig);
751
+ validateDynamicImports(userBuildResult, "user code");
752
+ const runtimeSourcePath = join5(
753
+ getNodeModulesPath(),
754
+ "@b9g/platform/dist/src/runtime.js"
555
755
  );
556
- const workerDestPath = join2(serverDir, "worker.js");
557
- try {
558
- await ESBuild2.build({
559
- entryPoints: [runtimeSourcePath],
560
- bundle: true,
561
- format: "esm",
562
- target: "es2022",
563
- platform: "node",
564
- outfile: workerDestPath,
565
- external: ["node:*"]
566
- });
567
- } catch (error) {
568
- const installedRuntimePath = join2(
569
- shovelRoot,
570
- "node_modules/@b9g/platform/dist/src/runtime.js"
571
- );
572
- await ESBuild2.build({
573
- entryPoints: [installedRuntimePath],
574
- bundle: true,
575
- format: "esm",
576
- target: "es2022",
577
- platform: "node",
578
- outfile: workerDestPath,
579
- external: ["node:*"]
580
- });
581
- }
756
+ const workerDestPath = join5(serverDir, "worker.js");
757
+ await ESBuild3.build({
758
+ entryPoints: [runtimeSourcePath],
759
+ bundle: true,
760
+ format: "esm",
761
+ target: "es2022",
762
+ platform: "node",
763
+ outfile: workerDestPath,
764
+ external: ["node:*"]
765
+ });
582
766
  }
583
767
  const buildConfig = {
584
768
  stdin: {
585
769
  contents: virtualEntry,
586
- resolveDir: shovelRoot,
587
- // Use Shovel root to resolve @b9g packages
770
+ resolveDir: projectRoot,
771
+ // Resolve packages from user's project
588
772
  sourcefile: "virtual-entry.js"
589
773
  },
590
774
  bundle: true,
@@ -593,17 +777,25 @@ async function createBuildConfig({
593
777
  platform: isCloudflare ? "browser" : "node",
594
778
  // Cloudflare: single-file architecture (server.js contains everything)
595
779
  // Node/Bun: multi-file architecture (index.js is entry, server.js is user code)
596
- outfile: join2(
780
+ outfile: join5(
597
781
  serverDir,
598
782
  isCloudflare ? "server.js" : BUILD_DEFAULTS.outputFile
599
783
  ),
600
- absWorkingDir: workspaceRoot || dirname3(entryPath),
784
+ absWorkingDir: projectRoot,
601
785
  mainFields: ["module", "main"],
602
786
  conditions: ["import", "module"],
787
+ // Resolve packages from the user's project node_modules
788
+ nodePaths: [getNodeModulesPath()],
603
789
  plugins: isCloudflare ? [
604
790
  importMetaPlugin(),
605
- assetsPlugin2({
606
- outDir: outputDir
791
+ assetsPlugin3({
792
+ outDir: outputDir,
793
+ clientBuild: {
794
+ jsx: jsxOptions.jsx,
795
+ jsxFactory: jsxOptions.jsxFactory,
796
+ jsxFragment: jsxOptions.jsxFragment,
797
+ jsxImportSource: jsxOptions.jsxImportSource
798
+ }
607
799
  })
608
800
  ] : [],
609
801
  // Assets already handled in user code build
@@ -617,6 +809,7 @@ async function createBuildConfig({
617
809
  external
618
810
  };
619
811
  if (isCloudflare) {
812
+ applyJSXOptions(buildConfig, jsxOptions);
620
813
  await configureCloudflareTarget(buildConfig);
621
814
  }
622
815
  return buildConfig;
@@ -634,32 +827,37 @@ async function configureCloudflareTarget(buildConfig) {
634
827
  async function createVirtualEntry(userEntryPath, platform, workerCount = 1) {
635
828
  const isCloudflare = platform === "cloudflare" || platform === "cloudflare-workers";
636
829
  if (isCloudflare) {
637
- return `
638
- // Import user's ServiceWorker code
830
+ return `// Import user's ServiceWorker code
639
831
  import "${userEntryPath}";
640
832
  `;
641
833
  }
642
- return await createWorkerEntry(userEntryPath, workerCount, platform);
834
+ return createWorkerEntry(userEntryPath, workerCount, platform);
643
835
  }
644
836
  async function createWorkerEntry(userEntryPath, workerCount, platform) {
645
- let currentDir = dirname3(fileURLToPath(import.meta.url));
837
+ let currentDir = dirname4(fileURLToPath(import.meta.url));
646
838
  let packageRoot = currentDir;
647
- while (packageRoot !== dirname3(packageRoot)) {
839
+ while (packageRoot !== dirname4(packageRoot)) {
648
840
  try {
649
- const packageJSONPath = join2(packageRoot, "package.json");
650
- await readFile2(packageJSONPath, "utf8");
841
+ const packageJSONPath = join5(packageRoot, "package.json");
842
+ await readFile3(packageJSONPath, "utf8");
651
843
  break;
652
- } catch {
653
- packageRoot = dirname3(packageRoot);
844
+ } catch (err) {
845
+ if (err.code !== "ENOENT") {
846
+ throw err;
847
+ }
848
+ packageRoot = dirname4(packageRoot);
654
849
  }
655
850
  }
656
- let templatePath = join2(packageRoot, "src/worker-entry.ts");
851
+ let templatePath = join5(packageRoot, "src/worker-entry.ts");
657
852
  try {
658
- await readFile2(templatePath, "utf8");
659
- } catch {
660
- templatePath = join2(packageRoot, "src/worker-entry.js");
853
+ await readFile3(templatePath, "utf8");
854
+ } catch (err) {
855
+ if (err.code !== "ENOENT") {
856
+ throw err;
857
+ }
858
+ templatePath = join5(packageRoot, "src/worker-entry.js");
661
859
  }
662
- const transpileResult = await ESBuild2.build({
860
+ const transpileResult = await ESBuild3.build({
663
861
  entryPoints: [templatePath],
664
862
  bundle: false,
665
863
  // Just transpile - bundling happens in final build
@@ -677,24 +875,24 @@ async function createWorkerEntry(userEntryPath, workerCount, platform) {
677
875
  async function logBundleAnalysis(metafile) {
678
876
  try {
679
877
  logger5.info("Bundle analysis:", {});
680
- const analysis = await ESBuild2.analyzeMetafile(metafile);
878
+ const analysis = await ESBuild3.analyzeMetafile(metafile);
681
879
  logger5.info(analysis, {});
682
880
  } catch (error) {
683
- logger5.warn("Failed to analyze bundle", { error: error.message });
881
+ logger5.warn("Failed to analyze bundle: {error}", { error });
684
882
  }
685
883
  }
686
884
  async function generatePackageJSON({ serverDir, platform, verbose, entryPath }) {
687
- const entryDir = dirname3(entryPath);
688
- const sourcePackageJsonPath = resolve2(entryDir, "package.json");
885
+ const entryDir = dirname4(entryPath);
886
+ const sourcePackageJsonPath = resolve3(entryDir, "package.json");
689
887
  try {
690
- const packageJSONContent = await readFile2(sourcePackageJsonPath, "utf8");
888
+ const packageJSONContent = await readFile3(sourcePackageJsonPath, "utf8");
691
889
  try {
692
890
  JSON.parse(packageJSONContent);
693
891
  } catch (parseError) {
694
892
  throw new Error(`Invalid package.json format: ${parseError.message}`);
695
893
  }
696
894
  await writeFile(
697
- join2(serverDir, "package.json"),
895
+ join5(serverDir, "package.json"),
698
896
  packageJSONContent,
699
897
  "utf8"
700
898
  );
@@ -703,12 +901,12 @@ async function generatePackageJSON({ serverDir, platform, verbose, entryPath })
703
901
  }
704
902
  } catch (error) {
705
903
  if (verbose) {
706
- logger5.warn("Could not copy package.json", { error: error.message });
904
+ logger5.warn("Could not copy package.json: {error}", { error });
707
905
  }
708
906
  try {
709
907
  const generatedPackageJson = await generateExecutablePackageJSON(platform);
710
908
  await writeFile(
711
- join2(serverDir, "package.json"),
909
+ join5(serverDir, "package.json"),
712
910
  JSON.stringify(generatedPackageJson, null, 2),
713
911
  "utf8"
714
912
  );
@@ -720,10 +918,9 @@ async function generatePackageJSON({ serverDir, platform, verbose, entryPath })
720
918
  }
721
919
  } catch (generateError) {
722
920
  if (verbose) {
723
- logger5.warn("Could not generate package.json", {
724
- error: generateError.message
921
+ logger5.warn("Could not generate package.json: {error}", {
922
+ error: generateError
725
923
  });
726
- logger5.warn("Generation error details", { error: generateError });
727
924
  }
728
925
  }
729
926
  }
@@ -736,8 +933,7 @@ async function generateExecutablePackageJSON(platform) {
736
933
  private: true,
737
934
  dependencies: {}
738
935
  };
739
- const workspaceRoot = await findWorkspaceRoot();
740
- const isWorkspaceEnvironment = workspaceRoot !== null;
936
+ const isWorkspaceEnvironment = findWorkspaceRoot() !== null;
741
937
  if (isWorkspaceEnvironment) {
742
938
  packageJSON.dependencies = {};
743
939
  } else {
@@ -760,12 +956,15 @@ async function generateExecutablePackageJSON(platform) {
760
956
  return packageJSON;
761
957
  }
762
958
  async function buildCommand(entrypoint, options) {
959
+ const projectRoot = findProjectRoot();
960
+ const config = loadConfig2(projectRoot);
961
+ const platform = Platform3.resolvePlatform({ ...options, config });
763
962
  await buildForProduction({
764
963
  entrypoint,
765
964
  outDir: "dist",
766
965
  verbose: options.verbose || false,
767
- platform: options.platform || "node",
768
- workerCount: options.workers ? parseInt(options.workers, 10) : 1
966
+ platform,
967
+ workerCount: options.workers ? parseInt(options.workers, 10) : config.workers
769
968
  });
770
969
  process.exit(0);
771
970
  }
package/bin/create.js CHANGED
@@ -8,7 +8,7 @@ import picocolors from "picocolors";
8
8
  import { mkdir, writeFile } from "fs/promises";
9
9
  import { join, resolve } from "path";
10
10
  import { existsSync } from "fs";
11
- var { cyan, green, yellow: _yellow, red, dim, bold } = picocolors;
11
+ var { cyan, green, red, dim, bold } = picocolors;
12
12
  async function main() {
13
13
  console.info("");
14
14
  intro(cyan("\u{1F680} Create Shovel App"));
@@ -200,28 +200,49 @@ function getRequestInfo(request: Request) {
200
200
 
201
201
  async function parseBody(request: Request) {
202
202
  const contentType = request.headers.get('content-type') || '';
203
-
203
+
204
204
  if (contentType.includes('application/json')) {
205
205
  try {
206
206
  return await request.json();
207
- } catch {
207
+ } catch (err) {
208
+ // Only ignore JSON parse errors, rethrow others
209
+ if (
210
+ !(err instanceof SyntaxError) ||
211
+ !/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
212
+ ) {
213
+ throw err;
214
+ }
208
215
  return null;
209
216
  }
210
217
  }
211
-
218
+
212
219
  if (contentType.includes('application/x-www-form-urlencoded')) {
213
220
  try {
214
221
  const formData = await request.formData();
215
222
  return Object.fromEntries(formData.entries());
216
- } catch {
223
+ } catch (err) {
224
+ // Only ignore form data parse errors, rethrow others
225
+ if (
226
+ !(err instanceof TypeError) ||
227
+ !String(err.message).includes('FormData')
228
+ ) {
229
+ throw err;
230
+ }
217
231
  return null;
218
232
  }
219
233
  }
220
-
234
+
221
235
  try {
222
236
  const text = await request.text();
223
237
  return text || null;
224
- } catch {
238
+ } catch (err) {
239
+ // Only ignore body already consumed errors, rethrow others
240
+ if (
241
+ !(err instanceof TypeError) ||
242
+ !String(err.message).includes('body')
243
+ ) {
244
+ throw err;
245
+ }
225
246
  return null;
226
247
  }
227
248
  }
@@ -237,28 +258,49 @@ function getRequestInfo(request) {
237
258
 
238
259
  async function parseBody(request) {
239
260
  const contentType = request.headers.get('content-type') || '';
240
-
261
+
241
262
  if (contentType.includes('application/json')) {
242
263
  try {
243
264
  return await request.json();
244
- } catch {
265
+ } catch (err) {
266
+ // Only ignore JSON parse errors, rethrow others
267
+ if (
268
+ !(err instanceof SyntaxError) ||
269
+ !/^(Unexpected token|Expected|JSON)/i.test(String(err.message))
270
+ ) {
271
+ throw err;
272
+ }
245
273
  return null;
246
274
  }
247
275
  }
248
-
276
+
249
277
  if (contentType.includes('application/x-www-form-urlencoded')) {
250
278
  try {
251
279
  const formData = await request.formData();
252
280
  return Object.fromEntries(formData.entries());
253
- } catch {
281
+ } catch (err) {
282
+ // Only ignore form data parse errors, rethrow others
283
+ if (
284
+ !(err instanceof TypeError) ||
285
+ !String(err.message).includes('FormData')
286
+ ) {
287
+ throw err;
288
+ }
254
289
  return null;
255
290
  }
256
291
  }
257
-
292
+
258
293
  try {
259
294
  const text = await request.text();
260
295
  return text || null;
261
- } catch {
296
+ } catch (err) {
297
+ // Only ignore body already consumed errors, rethrow others
298
+ if (
299
+ !(err instanceof TypeError) ||
300
+ !String(err.message).includes('body')
301
+ ) {
302
+ throw err;
303
+ }
262
304
  return null;
263
305
  }
264
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.9",
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": {
@@ -10,7 +10,7 @@
10
10
  "create": "bin/create.js"
11
11
  },
12
12
  "dependencies": {
13
- "@b9g/async-context": "^0.1.1",
13
+ "@b9g/async-context": "^0.1.4",
14
14
  "@clack/prompts": "^0.7.0",
15
15
  "@logtape/logtape": "^1.2.0",
16
16
  "commander": "^13.1.0",
@@ -21,29 +21,29 @@
21
21
  "source-map": "^0.7.4"
22
22
  },
23
23
  "devDependencies": {
24
- "@b9g/assets": "^0.1.13",
25
- "@b9g/cache": "^0.1.4",
24
+ "@b9g/assets": "^0.1.15",
25
+ "@b9g/cache": "^0.1.5",
26
26
  "@b9g/crank": "^0.7.2",
27
- "@b9g/filesystem": "^0.1.6",
27
+ "@b9g/filesystem": "^0.1.7",
28
28
  "@b9g/http-errors": "^0.1.5",
29
29
  "@b9g/libuild": "^0.1.17",
30
- "@b9g/platform": "^0.1.10",
31
- "@b9g/platform-bun": "^0.1.8",
32
- "@b9g/platform-cloudflare": "^0.1.7",
33
- "@b9g/platform-node": "^0.1.10",
34
- "@b9g/router": "^0.1.8",
30
+ "@b9g/platform": "^0.1.11",
31
+ "@b9g/platform-bun": "^0.1.9",
32
+ "@b9g/platform-cloudflare": "^0.1.9",
33
+ "@b9g/platform-node": "^0.1.11",
34
+ "@b9g/router": "^0.1.10",
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.10",
42
- "@b9g/platform-node": "^0.1.10",
43
- "@b9g/platform-cloudflare": "^0.1.7",
44
- "@b9g/platform-bun": "^0.1.8",
45
- "@b9g/cache": "^0.1.4",
46
- "@b9g/filesystem": "^0.1.6",
41
+ "@b9g/platform": "^0.1.11",
42
+ "@b9g/platform-node": "^0.1.11",
43
+ "@b9g/platform-cloudflare": "^0.1.9",
44
+ "@b9g/platform-bun": "^0.1.9",
45
+ "@b9g/cache": "^0.1.5",
46
+ "@b9g/filesystem": "^0.1.7",
47
47
  "@b9g/http-errors": "^0.1.5"
48
48
  },
49
49
  "peerDependenciesMeta": {