@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.
- package/README.md +1 -1
- package/bin/cli.js +380 -181
- package/bin/create.js +55 -13
- 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` | [
|
|
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 {
|
|
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(
|
|
39
|
-
|
|
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/
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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(
|
|
114
|
-
await mkdir(
|
|
115
|
-
const
|
|
116
|
-
|
|
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
|
-
|
|
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: (
|
|
137
|
-
|
|
285
|
+
setup: (build3) => {
|
|
286
|
+
build3.onStart(() => {
|
|
138
287
|
logger.info("Building", {
|
|
139
288
|
entrypoint: this.#options.entrypoint
|
|
140
289
|
});
|
|
141
290
|
});
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
210
|
-
const
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
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(
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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
|
|
347
|
-
import { resolve as
|
|
348
|
-
import { mkdir as
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
426
|
-
const outputDir =
|
|
656
|
+
const entryPath = resolve3(entrypoint);
|
|
657
|
+
const outputDir = resolve3(outDir);
|
|
427
658
|
try {
|
|
428
|
-
const stats = await
|
|
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
|
|
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("
|
|
677
|
+
logger5.info("Project root:", { projectRoot });
|
|
447
678
|
}
|
|
448
679
|
try {
|
|
449
|
-
await
|
|
450
|
-
await
|
|
451
|
-
await
|
|
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:
|
|
461
|
-
|
|
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
|
-
|
|
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:
|
|
528
|
-
absWorkingDir:
|
|
722
|
+
outfile: join5(serverDir, "server.js"),
|
|
723
|
+
absWorkingDir: projectRoot,
|
|
529
724
|
mainFields: ["module", "main"],
|
|
530
725
|
conditions: ["import", "module"],
|
|
531
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
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 =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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:
|
|
587
|
-
//
|
|
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:
|
|
780
|
+
outfile: join5(
|
|
597
781
|
serverDir,
|
|
598
782
|
isCloudflare ? "server.js" : BUILD_DEFAULTS.outputFile
|
|
599
783
|
),
|
|
600
|
-
absWorkingDir:
|
|
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
|
-
|
|
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
|
|
834
|
+
return createWorkerEntry(userEntryPath, workerCount, platform);
|
|
643
835
|
}
|
|
644
836
|
async function createWorkerEntry(userEntryPath, workerCount, platform) {
|
|
645
|
-
let currentDir =
|
|
837
|
+
let currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
646
838
|
let packageRoot = currentDir;
|
|
647
|
-
while (packageRoot !==
|
|
839
|
+
while (packageRoot !== dirname4(packageRoot)) {
|
|
648
840
|
try {
|
|
649
|
-
const packageJSONPath =
|
|
650
|
-
await
|
|
841
|
+
const packageJSONPath = join5(packageRoot, "package.json");
|
|
842
|
+
await readFile3(packageJSONPath, "utf8");
|
|
651
843
|
break;
|
|
652
|
-
} catch {
|
|
653
|
-
|
|
844
|
+
} catch (err) {
|
|
845
|
+
if (err.code !== "ENOENT") {
|
|
846
|
+
throw err;
|
|
847
|
+
}
|
|
848
|
+
packageRoot = dirname4(packageRoot);
|
|
654
849
|
}
|
|
655
850
|
}
|
|
656
|
-
let templatePath =
|
|
851
|
+
let templatePath = join5(packageRoot, "src/worker-entry.ts");
|
|
657
852
|
try {
|
|
658
|
-
await
|
|
659
|
-
} catch {
|
|
660
|
-
|
|
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
|
|
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
|
|
878
|
+
const analysis = await ESBuild3.analyzeMetafile(metafile);
|
|
681
879
|
logger5.info(analysis, {});
|
|
682
880
|
} catch (error) {
|
|
683
|
-
logger5.warn("Failed to analyze bundle", { error
|
|
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 =
|
|
688
|
-
const sourcePackageJsonPath =
|
|
885
|
+
const entryDir = dirname4(entryPath);
|
|
886
|
+
const sourcePackageJsonPath = resolve3(entryDir, "package.json");
|
|
689
887
|
try {
|
|
690
|
-
const packageJSONContent = await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
768
|
-
workerCount: options.workers ? parseInt(options.workers, 10) :
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
-
"@b9g/cache": "^0.1.
|
|
24
|
+
"@b9g/assets": "^0.1.15",
|
|
25
|
+
"@b9g/cache": "^0.1.5",
|
|
26
26
|
"@b9g/crank": "^0.7.2",
|
|
27
|
-
"@b9g/filesystem": "^0.1.
|
|
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.
|
|
31
|
-
"@b9g/platform-bun": "^0.1.
|
|
32
|
-
"@b9g/platform-cloudflare": "^0.1.
|
|
33
|
-
"@b9g/platform-node": "^0.1.
|
|
34
|
-
"@b9g/router": "^0.1.
|
|
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.
|
|
42
|
-
"@b9g/platform-node": "^0.1.
|
|
43
|
-
"@b9g/platform-cloudflare": "^0.1.
|
|
44
|
-
"@b9g/platform-bun": "^0.1.
|
|
45
|
-
"@b9g/cache": "^0.1.
|
|
46
|
-
"@b9g/filesystem": "^0.1.
|
|
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": {
|