@b9g/shovel 0.2.0-beta.10 → 0.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/README.md +301 -42
- package/bin/cli.js +29 -9
- package/bin/create.js +22 -22
- package/package.json +21 -13
- package/{activate-5LWUTBLL.js → src/_chunks/activate-TP6RQP47.js} +14 -11
- package/src/_chunks/build-V3IPZGKC.js +434 -0
- package/src/_chunks/chunk-ADR5RW57.js +78 -0
- package/src/_chunks/chunk-GRAFMTEH.js +1150 -0
- package/src/_chunks/chunk-JJFM7PO2.js +468 -0
- package/src/_chunks/develop-A7EU2ZDY.js +404 -0
- package/{info-PRYEMZS4.js → src/_chunks/info-TDUY3FZN.js} +1 -1
- package/build-NDUV2F2Z.js +0 -386
- package/chunk-CSH7M4MK.js +0 -861
- package/chunk-ILQUUH2L.js +0 -164
- package/develop-5ORIPB7M.js +0 -264
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createConfigPlugin,
|
|
3
|
+
createEntryPlugin,
|
|
4
|
+
getGitSHA
|
|
5
|
+
} from "./chunk-ADR5RW57.js";
|
|
6
|
+
import {
|
|
7
|
+
applyJSXOptions,
|
|
8
|
+
assetsPlugin,
|
|
9
|
+
importMetaPlugin,
|
|
10
|
+
loadJSXConfig
|
|
11
|
+
} from "./chunk-JJFM7PO2.js";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULTS,
|
|
14
|
+
findProjectRoot
|
|
15
|
+
} from "./chunk-GRAFMTEH.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/develop.ts
|
|
18
|
+
import { getLogger as getLogger2 } from "@logtape/logtape";
|
|
19
|
+
import * as Platform from "@b9g/platform";
|
|
20
|
+
|
|
21
|
+
// src/utils/watcher.ts
|
|
22
|
+
import * as ESBuild from "esbuild";
|
|
23
|
+
import { builtinModules } from "node:module";
|
|
24
|
+
import { resolve, join, dirname, basename } from "path";
|
|
25
|
+
import { mkdir } from "fs/promises";
|
|
26
|
+
import { watch, existsSync } from "fs";
|
|
27
|
+
import { getLogger } from "@logtape/logtape";
|
|
28
|
+
var logger = getLogger(["shovel"]);
|
|
29
|
+
var Watcher = class {
|
|
30
|
+
#options;
|
|
31
|
+
#ctx;
|
|
32
|
+
#projectRoot;
|
|
33
|
+
#initialBuildComplete;
|
|
34
|
+
#initialBuildResolve;
|
|
35
|
+
#currentEntrypoint;
|
|
36
|
+
#configWatchers;
|
|
37
|
+
#dirWatchers;
|
|
38
|
+
#userEntryPath;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.#options = options;
|
|
41
|
+
this.#projectRoot = findProjectRoot();
|
|
42
|
+
this.#initialBuildComplete = false;
|
|
43
|
+
this.#currentEntrypoint = "";
|
|
44
|
+
this.#configWatchers = [];
|
|
45
|
+
this.#dirWatchers = /* @__PURE__ */ new Map();
|
|
46
|
+
this.#userEntryPath = "";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start watching and building
|
|
50
|
+
* @returns Result with success status and the hashed entrypoint path
|
|
51
|
+
*/
|
|
52
|
+
async start() {
|
|
53
|
+
const entryPath = resolve(this.#projectRoot, this.#options.entrypoint);
|
|
54
|
+
this.#userEntryPath = entryPath;
|
|
55
|
+
const outputDir = resolve(this.#projectRoot, this.#options.outDir);
|
|
56
|
+
await mkdir(join(outputDir, "server"), { recursive: true });
|
|
57
|
+
const workerEntryWrapper = this.#options.platform.getEntryWrapper(
|
|
58
|
+
entryPath,
|
|
59
|
+
{ type: "worker", outDir: outputDir }
|
|
60
|
+
);
|
|
61
|
+
const jsxOptions = await loadJSXConfig(this.#projectRoot);
|
|
62
|
+
const initialBuildPromise = new Promise((resolve2) => {
|
|
63
|
+
this.#initialBuildResolve = resolve2;
|
|
64
|
+
});
|
|
65
|
+
const platformESBuildConfig = this.#options.platformESBuildConfig;
|
|
66
|
+
const external = platformESBuildConfig.external ?? ["node:*"];
|
|
67
|
+
const buildOptions = {
|
|
68
|
+
entryPoints: {
|
|
69
|
+
server: "shovel:entry",
|
|
70
|
+
config: "shovel:config"
|
|
71
|
+
},
|
|
72
|
+
bundle: true,
|
|
73
|
+
format: "esm",
|
|
74
|
+
target: "es2022",
|
|
75
|
+
platform: platformESBuildConfig.platform ?? "node",
|
|
76
|
+
outdir: `${outputDir}/server`,
|
|
77
|
+
// Worker gets stable name, server gets hash for cache busting
|
|
78
|
+
entryNames: "[name]",
|
|
79
|
+
metafile: true,
|
|
80
|
+
absWorkingDir: this.#projectRoot,
|
|
81
|
+
conditions: platformESBuildConfig.conditions ?? ["import", "module"],
|
|
82
|
+
plugins: [
|
|
83
|
+
createConfigPlugin(this.#projectRoot, this.#options.outDir, {
|
|
84
|
+
platformDefaults: this.#options.platform.getDefaults()
|
|
85
|
+
}),
|
|
86
|
+
createEntryPlugin(this.#projectRoot, workerEntryWrapper),
|
|
87
|
+
importMetaPlugin(),
|
|
88
|
+
assetsPlugin({
|
|
89
|
+
outDir: outputDir,
|
|
90
|
+
clientBuild: {
|
|
91
|
+
jsx: jsxOptions.jsx,
|
|
92
|
+
jsxFactory: jsxOptions.jsxFactory,
|
|
93
|
+
jsxFragment: jsxOptions.jsxFragment,
|
|
94
|
+
jsxImportSource: jsxOptions.jsxImportSource
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
// Plugin to detect build completion (works with watch mode)
|
|
98
|
+
{
|
|
99
|
+
name: "build-notify",
|
|
100
|
+
setup: (build) => {
|
|
101
|
+
build.onStart(() => {
|
|
102
|
+
logger.info("Building...");
|
|
103
|
+
});
|
|
104
|
+
build.onEnd(async (result) => {
|
|
105
|
+
let success = result.errors.length === 0;
|
|
106
|
+
const dynamicImportWarnings = (result.warnings || []).filter(
|
|
107
|
+
(w) => (w.text.includes("cannot be bundled") || w.text.includes("import() call") || w.text.includes("dynamic import")) && !w.text.includes("./server.js")
|
|
108
|
+
);
|
|
109
|
+
if (dynamicImportWarnings.length > 0) {
|
|
110
|
+
success = false;
|
|
111
|
+
for (const warning of dynamicImportWarnings) {
|
|
112
|
+
const loc = warning.location;
|
|
113
|
+
const file = loc?.file || "unknown";
|
|
114
|
+
const line = loc?.line || "?";
|
|
115
|
+
logger.error(
|
|
116
|
+
"Non-analyzable dynamic import at {file}:{line}: {text}",
|
|
117
|
+
{ file, line, text: warning.text }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
logger.error(
|
|
121
|
+
"Dynamic imports must use literal strings, not variables. For config-driven providers, ensure they are registered in shovel.json."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (result.metafile) {
|
|
125
|
+
const hasNodeWildcard = external.includes("node:*");
|
|
126
|
+
const allowedSet = new Set(external);
|
|
127
|
+
const unexpectedExternals = [];
|
|
128
|
+
for (const path of Object.keys(result.metafile.inputs)) {
|
|
129
|
+
if (!path.startsWith("<external>:"))
|
|
130
|
+
continue;
|
|
131
|
+
const moduleName = path.slice("<external>:".length);
|
|
132
|
+
const isAllowed = allowedSet.has(moduleName) || hasNodeWildcard && moduleName.startsWith("node:") || builtinModules.includes(moduleName);
|
|
133
|
+
if (!isAllowed && !unexpectedExternals.includes(moduleName)) {
|
|
134
|
+
unexpectedExternals.push(moduleName);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (unexpectedExternals.length > 0) {
|
|
138
|
+
success = false;
|
|
139
|
+
for (const ext of unexpectedExternals) {
|
|
140
|
+
logger.error("Unexpected external import: {module}", {
|
|
141
|
+
module: ext
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
logger.error(
|
|
145
|
+
"These modules are not bundled and won't be available at runtime."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
let outputPath = "";
|
|
150
|
+
if (result.metafile) {
|
|
151
|
+
const outputs = Object.keys(result.metafile.outputs);
|
|
152
|
+
const serverOutput = outputs.find(
|
|
153
|
+
(p) => p.endsWith("server.js")
|
|
154
|
+
);
|
|
155
|
+
if (serverOutput) {
|
|
156
|
+
outputPath = resolve(this.#projectRoot, serverOutput);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (success) {
|
|
160
|
+
logger.debug("Build complete", { entrypoint: outputPath });
|
|
161
|
+
} else {
|
|
162
|
+
logger.error("Build errors: {errors}", { errors: result.errors });
|
|
163
|
+
}
|
|
164
|
+
this.#currentEntrypoint = outputPath;
|
|
165
|
+
if (result.metafile) {
|
|
166
|
+
this.#updateSourceWatchers(result.metafile);
|
|
167
|
+
}
|
|
168
|
+
if (!this.#initialBuildComplete) {
|
|
169
|
+
this.#initialBuildComplete = true;
|
|
170
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
171
|
+
this.#initialBuildResolve?.({ success, entrypoint: outputPath });
|
|
172
|
+
} else {
|
|
173
|
+
await this.#options.onBuild?.(success, outputPath);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
define: {
|
|
180
|
+
...platformESBuildConfig.define ?? {},
|
|
181
|
+
// Inject output directory for [outdir] placeholder resolution
|
|
182
|
+
__SHOVEL_OUTDIR__: JSON.stringify(outputDir),
|
|
183
|
+
// Inject git commit SHA for [git] placeholder
|
|
184
|
+
__SHOVEL_GIT__: JSON.stringify(getGitSHA(this.#projectRoot))
|
|
185
|
+
},
|
|
186
|
+
// Mark ./server.js as external so it's imported at runtime (sibling output file)
|
|
187
|
+
external: [...external, "./server.js"],
|
|
188
|
+
sourcemap: "inline",
|
|
189
|
+
minify: false,
|
|
190
|
+
treeShaking: true
|
|
191
|
+
};
|
|
192
|
+
applyJSXOptions(buildOptions, jsxOptions);
|
|
193
|
+
this.#ctx = await ESBuild.context(buildOptions);
|
|
194
|
+
logger.debug("Starting esbuild watch mode");
|
|
195
|
+
await this.#ctx.watch();
|
|
196
|
+
this.#watchConfigFiles();
|
|
197
|
+
return initialBuildPromise;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Watch shovel.json and package.json for changes
|
|
201
|
+
* Triggers rebuild when config changes
|
|
202
|
+
*/
|
|
203
|
+
#watchConfigFiles() {
|
|
204
|
+
const configFiles = ["shovel.json", "package.json"];
|
|
205
|
+
for (const filename of configFiles) {
|
|
206
|
+
const filepath = join(this.#projectRoot, filename);
|
|
207
|
+
if (!existsSync(filepath))
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const watcher = watch(filepath, { persistent: false }, (event) => {
|
|
211
|
+
if (event === "change") {
|
|
212
|
+
logger.info(`Config changed: ${filename}, rebuilding...`);
|
|
213
|
+
this.#ctx?.rebuild().catch((err) => {
|
|
214
|
+
logger.error("Rebuild failed: {error}", { error: err });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
this.#configWatchers.push(watcher);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
logger.warn("Failed to watch {file}: {error}", {
|
|
221
|
+
file: filename,
|
|
222
|
+
error: err
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Update native directory watchers for source files from metafile.
|
|
229
|
+
* Uses fs.watch on directories for instant inotify/fsevents detection
|
|
230
|
+
* as a complement to esbuild's polling-based watch mode.
|
|
231
|
+
*
|
|
232
|
+
* Watching directories instead of files handles:
|
|
233
|
+
* - File deletion and recreation (directory watcher survives)
|
|
234
|
+
* - Concurrent modifications (one watcher per directory)
|
|
235
|
+
* - Fewer file descriptors (one per directory vs one per file)
|
|
236
|
+
*/
|
|
237
|
+
#updateSourceWatchers(metafile) {
|
|
238
|
+
const newDirFiles = /* @__PURE__ */ new Map();
|
|
239
|
+
if (this.#userEntryPath) {
|
|
240
|
+
const entryDir = dirname(this.#userEntryPath);
|
|
241
|
+
const entryFile = basename(this.#userEntryPath);
|
|
242
|
+
if (!newDirFiles.has(entryDir)) {
|
|
243
|
+
newDirFiles.set(entryDir, /* @__PURE__ */ new Set());
|
|
244
|
+
}
|
|
245
|
+
newDirFiles.get(entryDir).add(entryFile);
|
|
246
|
+
logger.debug("Explicitly watching user entry file: {path}", {
|
|
247
|
+
path: this.#userEntryPath
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
for (const inputPath of Object.keys(metafile.inputs)) {
|
|
251
|
+
if (inputPath.startsWith("<") || inputPath.startsWith("shovel")) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const fullPath = resolve(this.#projectRoot, inputPath);
|
|
255
|
+
const dir = dirname(fullPath);
|
|
256
|
+
const file = basename(fullPath);
|
|
257
|
+
if (!newDirFiles.has(dir)) {
|
|
258
|
+
newDirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
259
|
+
}
|
|
260
|
+
newDirFiles.get(dir).add(file);
|
|
261
|
+
}
|
|
262
|
+
for (const [dir, entry] of this.#dirWatchers) {
|
|
263
|
+
if (!newDirFiles.has(dir)) {
|
|
264
|
+
entry.watcher.close();
|
|
265
|
+
this.#dirWatchers.delete(dir);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const [dir, files] of newDirFiles) {
|
|
269
|
+
const existing = this.#dirWatchers.get(dir);
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.files = files;
|
|
272
|
+
} else {
|
|
273
|
+
if (!existsSync(dir))
|
|
274
|
+
continue;
|
|
275
|
+
try {
|
|
276
|
+
const watcher = watch(dir, { persistent: false }, (event, filename) => {
|
|
277
|
+
const entry = this.#dirWatchers.get(dir);
|
|
278
|
+
if (!entry)
|
|
279
|
+
return;
|
|
280
|
+
const isTrackedFile = filename ? entry.files.has(filename) : true;
|
|
281
|
+
if (isTrackedFile) {
|
|
282
|
+
logger.debug("Native watcher detected change: {file}", {
|
|
283
|
+
file: filename ? join(dir, filename) : dir
|
|
284
|
+
});
|
|
285
|
+
this.#ctx?.rebuild().catch((err) => {
|
|
286
|
+
logger.error("Rebuild failed: {error}", { error: err });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
this.#dirWatchers.set(dir, { watcher, files });
|
|
291
|
+
} catch (err) {
|
|
292
|
+
logger.debug("Failed to watch directory {dir}: {error}", {
|
|
293
|
+
dir,
|
|
294
|
+
error: err
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const totalFiles = Array.from(this.#dirWatchers.values()).reduce(
|
|
300
|
+
(sum, entry) => sum + entry.files.size,
|
|
301
|
+
0
|
|
302
|
+
);
|
|
303
|
+
const watchedDirs = Array.from(this.#dirWatchers.keys());
|
|
304
|
+
logger.debug(
|
|
305
|
+
"Watching {fileCount} source files in {dirCount} directories with native fs.watch",
|
|
306
|
+
{ fileCount: totalFiles, dirCount: this.#dirWatchers.size }
|
|
307
|
+
);
|
|
308
|
+
logger.debug("Watched directories: {dirs}", {
|
|
309
|
+
dirs: watchedDirs.slice(0, 5).join(", ") + (watchedDirs.length > 5 ? "..." : "")
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Stop watching and dispose of esbuild context
|
|
314
|
+
*/
|
|
315
|
+
async stop() {
|
|
316
|
+
for (const watcher of this.#configWatchers) {
|
|
317
|
+
watcher.close();
|
|
318
|
+
}
|
|
319
|
+
this.#configWatchers = [];
|
|
320
|
+
for (const entry of this.#dirWatchers.values()) {
|
|
321
|
+
entry.watcher.close();
|
|
322
|
+
}
|
|
323
|
+
this.#dirWatchers.clear();
|
|
324
|
+
if (this.#ctx) {
|
|
325
|
+
await this.#ctx.dispose();
|
|
326
|
+
this.#ctx = void 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/commands/develop.ts
|
|
332
|
+
var logger2 = getLogger2(["shovel"]);
|
|
333
|
+
async function developCommand(entrypoint, options, config) {
|
|
334
|
+
try {
|
|
335
|
+
const platformName = Platform.resolvePlatform({ ...options, config });
|
|
336
|
+
const workerCount = getWorkerCount(options, config);
|
|
337
|
+
logger2.debug("Platform: {platform}", { platform: platformName });
|
|
338
|
+
logger2.debug("Worker count: {workerCount}", { workerCount });
|
|
339
|
+
const platformInstance = await Platform.createPlatform(platformName, {
|
|
340
|
+
port: parseInt(options.port || String(DEFAULTS.SERVER.PORT), 10),
|
|
341
|
+
host: options.host || DEFAULTS.SERVER.HOST
|
|
342
|
+
});
|
|
343
|
+
const platformESBuildConfig = platformInstance.getESBuildConfig();
|
|
344
|
+
logger2.info("Starting development server");
|
|
345
|
+
let serviceWorker;
|
|
346
|
+
const outDir = "dist";
|
|
347
|
+
const watcher = new Watcher({
|
|
348
|
+
entrypoint,
|
|
349
|
+
outDir,
|
|
350
|
+
platform: platformInstance,
|
|
351
|
+
platformESBuildConfig,
|
|
352
|
+
onBuild: async (success, builtEntrypoint2) => {
|
|
353
|
+
if (success && serviceWorker) {
|
|
354
|
+
if (platformInstance && typeof platformInstance.reloadWorkers === "function") {
|
|
355
|
+
await platformInstance.reloadWorkers(builtEntrypoint2);
|
|
356
|
+
logger2.info("Reloaded");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const { success: buildSuccess, entrypoint: builtEntrypoint } = await watcher.start();
|
|
362
|
+
if (!buildSuccess || !builtEntrypoint) {
|
|
363
|
+
logger2.error("Initial build failed, watching for changes to retry");
|
|
364
|
+
await new Promise(() => {
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
serviceWorker = await platformInstance.loadServiceWorker(builtEntrypoint, {
|
|
368
|
+
hotReload: true,
|
|
369
|
+
workerCount
|
|
370
|
+
});
|
|
371
|
+
const server = platformInstance.createServer(serviceWorker.handleRequest, {
|
|
372
|
+
port: parseInt(options.port || String(DEFAULTS.SERVER.PORT), 10),
|
|
373
|
+
host: options.host || DEFAULTS.SERVER.HOST
|
|
374
|
+
});
|
|
375
|
+
await server.listen();
|
|
376
|
+
logger2.info("Server running at http://{host}:{port}", {
|
|
377
|
+
host: options.host,
|
|
378
|
+
port: options.port
|
|
379
|
+
});
|
|
380
|
+
const shutdown = async (signal) => {
|
|
381
|
+
logger2.debug("Shutting down ({signal})", { signal });
|
|
382
|
+
await watcher.stop();
|
|
383
|
+
await serviceWorker?.dispose();
|
|
384
|
+
await platformInstance.dispose();
|
|
385
|
+
await server.close();
|
|
386
|
+
logger2.debug("Shutdown complete");
|
|
387
|
+
process.exit(0);
|
|
388
|
+
};
|
|
389
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
390
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
logger2.error("Failed to start development server: {error}", { error });
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function getWorkerCount(options, config) {
|
|
397
|
+
if (options.workers) {
|
|
398
|
+
return parseInt(options.workers, 10);
|
|
399
|
+
}
|
|
400
|
+
return config?.workers ?? DEFAULTS.WORKERS;
|
|
401
|
+
}
|
|
402
|
+
export {
|
|
403
|
+
developCommand
|
|
404
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/commands/info.ts
|
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
|
3
3
|
import { detectRuntime, detectDevelopmentPlatform } from "@b9g/platform";
|
|
4
|
-
var logger = getLogger(["
|
|
4
|
+
var logger = getLogger(["shovel"]);
|
|
5
5
|
async function infoCommand() {
|
|
6
6
|
logger.info("Shovel Platform Information", {});
|
|
7
7
|
logger.info("---", {});
|