@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.
@@ -0,0 +1,434 @@
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
+ findProjectRoot,
14
+ findWorkspaceRoot,
15
+ getNodeModulesPath
16
+ } from "./chunk-GRAFMTEH.js";
17
+
18
+ // src/commands/build.ts
19
+ import * as ESBuild from "esbuild";
20
+ import { builtinModules, createRequire } from "node:module";
21
+ import { resolve, join, dirname } from "path";
22
+ import { getLogger } from "@logtape/logtape";
23
+ import * as Platform from "@b9g/platform";
24
+ import { mkdir, readFile, writeFile } from "fs/promises";
25
+ var logger = getLogger(["shovel"]);
26
+ function validateDynamicImports(result, context, excludePatterns = []) {
27
+ const dynamicImportWarnings = (result.warnings || []).filter(
28
+ (w) => (w.text.includes("cannot be bundled") || w.text.includes("import() call") || w.text.includes("dynamic import")) && // Exclude intentional dynamic imports
29
+ !excludePatterns.some((pattern) => w.text.includes(pattern))
30
+ );
31
+ if (dynamicImportWarnings.length > 0) {
32
+ const locations = dynamicImportWarnings.map((w) => {
33
+ const loc = w.location;
34
+ const file = loc?.file || "unknown";
35
+ const line = loc?.line || "?";
36
+ return ` ${file}:${line} - ${w.text}`;
37
+ }).join("\n");
38
+ throw new Error(
39
+ `Build failed (${context}): Non-analyzable dynamic imports found:
40
+ ${locations}
41
+
42
+ Dynamic imports must use literal strings, not variables.
43
+ For config-driven providers, ensure they are registered in shovel.json.`
44
+ );
45
+ }
46
+ }
47
+ function validateExternals(result, allowedExternals, context) {
48
+ if (!result.metafile)
49
+ return;
50
+ const allowedSet = new Set(allowedExternals);
51
+ const hasNodeWildcard = allowedExternals.includes("node:*");
52
+ const unexpectedExternals = [];
53
+ for (const path of Object.keys(result.metafile.inputs)) {
54
+ if (!path.startsWith("<external>:"))
55
+ continue;
56
+ const moduleName = path.slice("<external>:".length);
57
+ const isAllowed = allowedSet.has(moduleName) || hasNodeWildcard && moduleName.startsWith("node:") || builtinModules.includes(moduleName);
58
+ if (!isAllowed && !unexpectedExternals.includes(moduleName)) {
59
+ unexpectedExternals.push(moduleName);
60
+ }
61
+ }
62
+ if (unexpectedExternals.length > 0) {
63
+ const externals = unexpectedExternals.map((e) => ` - ${e}`).join("\n");
64
+ throw new Error(
65
+ `Build failed (${context}): Unexpected external imports found:
66
+ ${externals}
67
+
68
+ These modules are not bundled and won't be available at runtime.
69
+ Either bundle them or add them to the platform's external list.`
70
+ );
71
+ }
72
+ }
73
+ var BUILD_DEFAULTS = {
74
+ format: "esm",
75
+ target: "es2022",
76
+ outputFile: "index.js",
77
+ sourcemap: false,
78
+ minify: false,
79
+ treeShaking: true
80
+ };
81
+ var BUILD_STRUCTURE = {
82
+ serverDir: "server",
83
+ publicDir: "public"
84
+ };
85
+ async function buildForProduction({
86
+ entrypoint,
87
+ outDir,
88
+ platform = "node",
89
+ workerCount = 1,
90
+ userBuildConfig
91
+ }) {
92
+ const buildContext = await initializeBuild({
93
+ entrypoint,
94
+ outDir,
95
+ platform,
96
+ workerCount
97
+ });
98
+ const buildConfig = await createBuildConfig({
99
+ entryPath: buildContext.entryPath,
100
+ outputDir: buildContext.outputDir,
101
+ serverDir: buildContext.serverDir,
102
+ projectRoot: buildContext.projectRoot,
103
+ platform: buildContext.platform,
104
+ shovelBuildConfig: userBuildConfig
105
+ });
106
+ const result = await ESBuild.build(buildConfig);
107
+ validateDynamicImports(result, "main bundle");
108
+ const external = buildConfig.external ?? ["node:*"];
109
+ validateExternals(result, external, "main bundle");
110
+ await generatePackageJSON({
111
+ ...buildContext,
112
+ entryPath: buildContext.entryPath
113
+ });
114
+ logger.debug("Built app to", { outputDir: buildContext.outputDir });
115
+ logger.debug("Server files", { dir: buildContext.serverDir });
116
+ logger.debug("Public files", { dir: join(buildContext.outputDir, "public") });
117
+ }
118
+ async function initializeBuild({
119
+ entrypoint,
120
+ outDir,
121
+ platform,
122
+ workerCount = 1
123
+ }) {
124
+ if (!entrypoint) {
125
+ throw new Error("Entry point is required");
126
+ }
127
+ if (!outDir) {
128
+ throw new Error("Output directory is required");
129
+ }
130
+ logger.debug("Entry:", { path: entrypoint });
131
+ logger.debug("Output:", { dir: outDir });
132
+ logger.debug("Target platform:", { platform });
133
+ const entryPath = resolve(entrypoint);
134
+ const outputDir = resolve(outDir);
135
+ try {
136
+ const stats = await readFile(entryPath, "utf8");
137
+ if (stats.length === 0) {
138
+ logger.warn("Entry point is empty", { entryPath });
139
+ }
140
+ } catch (error) {
141
+ throw new Error(`Entry point not found or not accessible: ${entryPath}`);
142
+ }
143
+ const validPlatforms = ["node", "bun", "cloudflare"];
144
+ if (!validPlatforms.includes(platform)) {
145
+ throw new Error(
146
+ `Invalid platform: ${platform}. Valid platforms: ${validPlatforms.join(", ")}`
147
+ );
148
+ }
149
+ const projectRoot = findProjectRoot(dirname(entryPath));
150
+ logger.debug("Entry:", { entryPath });
151
+ logger.debug("Output:", { outputDir });
152
+ logger.debug("Target platform:", { platform });
153
+ logger.debug("Project root:", { projectRoot });
154
+ try {
155
+ await mkdir(outputDir, { recursive: true });
156
+ await mkdir(join(outputDir, BUILD_STRUCTURE.serverDir), { recursive: true });
157
+ await mkdir(join(outputDir, BUILD_STRUCTURE.publicDir), { recursive: true });
158
+ } catch (error) {
159
+ throw new Error(`Failed to create output directory structure: ${error}`);
160
+ }
161
+ return {
162
+ entryPath,
163
+ outputDir,
164
+ serverDir: join(outputDir, BUILD_STRUCTURE.serverDir),
165
+ projectRoot,
166
+ platform,
167
+ workerCount
168
+ };
169
+ }
170
+ async function loadUserPlugins(plugins, projectRoot) {
171
+ const loadedPlugins = [];
172
+ for (const pluginConfig of plugins) {
173
+ const {
174
+ module: modulePath,
175
+ export: exportName = "default",
176
+ ...options
177
+ } = pluginConfig;
178
+ try {
179
+ const projectRequire = createRequire(join(projectRoot, "package.json"));
180
+ const resolvedPath = modulePath.startsWith(".") ? resolve(projectRoot, modulePath) : projectRequire.resolve(modulePath);
181
+ const mod = await import(resolvedPath);
182
+ const pluginFactory = exportName === "default" ? mod.default : mod[exportName];
183
+ if (typeof pluginFactory !== "function") {
184
+ throw new Error(
185
+ `Plugin export "${exportName}" from "${modulePath}" is not a function`
186
+ );
187
+ }
188
+ const hasOptions = Object.keys(options).length > 0;
189
+ const plugin = hasOptions ? pluginFactory(options) : pluginFactory();
190
+ loadedPlugins.push(plugin);
191
+ logger.debug("Loaded ESBuild plugin", {
192
+ module: modulePath,
193
+ export: exportName
194
+ });
195
+ } catch (error) {
196
+ throw new Error(
197
+ `Failed to load ESBuild plugin "${modulePath}": ${error instanceof Error ? error.message : error}`
198
+ );
199
+ }
200
+ }
201
+ return loadedPlugins;
202
+ }
203
+ async function createBuildConfig({
204
+ entryPath,
205
+ outputDir,
206
+ serverDir,
207
+ projectRoot,
208
+ platform: platformName,
209
+ shovelBuildConfig
210
+ }) {
211
+ const platform = await Platform.createPlatform(platformName);
212
+ const platformESBuildConfig = platform.getESBuildConfig();
213
+ const platformDefaults = platform.getDefaults();
214
+ const entryWrapper = platform.getEntryWrapper(entryPath);
215
+ const bundlesUserCodeInline = platformESBuildConfig.bundlesUserCodeInline ?? false;
216
+ const jsxOptions = await loadJSXConfig(projectRoot || dirname(entryPath));
217
+ const target = shovelBuildConfig?.target ?? BUILD_DEFAULTS.target;
218
+ const minify = shovelBuildConfig?.minify ?? BUILD_DEFAULTS.minify;
219
+ const sourcemap = shovelBuildConfig?.sourcemap ?? BUILD_DEFAULTS.sourcemap;
220
+ const treeShaking = shovelBuildConfig?.treeShaking ?? BUILD_DEFAULTS.treeShaking;
221
+ const userPlugins = shovelBuildConfig?.plugins?.length ? await loadUserPlugins(shovelBuildConfig.plugins, projectRoot) : [];
222
+ try {
223
+ const platformExternal = platformESBuildConfig.external ?? ["node:*"];
224
+ const userExternal = shovelBuildConfig?.external ?? [];
225
+ const external = [...platformExternal, ...userExternal];
226
+ if (!bundlesUserCodeInline) {
227
+ const workerEntryWrapper = platform.getEntryWrapper(
228
+ "./server.js",
229
+ // Relative import to sibling output file
230
+ { type: "worker", outDir: outputDir }
231
+ );
232
+ const workerBuildConfig = {
233
+ entryPoints: {
234
+ worker: "shovel:entry",
235
+ server: entryPath,
236
+ config: "shovel:config"
237
+ },
238
+ bundle: true,
239
+ format: BUILD_DEFAULTS.format,
240
+ target,
241
+ platform: platformESBuildConfig.platform ?? "node",
242
+ outdir: serverDir,
243
+ entryNames: "[name]",
244
+ absWorkingDir: projectRoot,
245
+ mainFields: ["module", "main"],
246
+ conditions: platformESBuildConfig.conditions ?? ["import", "module"],
247
+ // Resolve packages from the user's project node_modules
248
+ nodePaths: [getNodeModulesPath()],
249
+ // User plugins run first, then Shovel's core plugins
250
+ plugins: [
251
+ ...userPlugins,
252
+ createConfigPlugin(projectRoot, outputDir, { platformDefaults }),
253
+ createEntryPlugin(projectRoot, workerEntryWrapper),
254
+ importMetaPlugin(),
255
+ assetsPlugin({
256
+ outDir: outputDir,
257
+ clientBuild: {
258
+ jsx: jsxOptions.jsx,
259
+ jsxFactory: jsxOptions.jsxFactory,
260
+ jsxFragment: jsxOptions.jsxFragment,
261
+ jsxImportSource: jsxOptions.jsxImportSource
262
+ }
263
+ })
264
+ ],
265
+ metafile: true,
266
+ sourcemap,
267
+ minify,
268
+ treeShaking,
269
+ define: {
270
+ ...platformESBuildConfig.define ?? {},
271
+ // User-defined constants
272
+ ...shovelBuildConfig?.define ?? {},
273
+ // Inject output directory for runtime.outdir() resolution
274
+ __SHOVEL_OUTDIR__: JSON.stringify(outputDir),
275
+ // Inject git commit SHA for [git] placeholder
276
+ __SHOVEL_GIT__: JSON.stringify(getGitSHA(projectRoot))
277
+ },
278
+ // Path aliases from user config
279
+ alias: shovelBuildConfig?.alias,
280
+ // Mark ./server.js as external so it's imported at runtime (sibling output file)
281
+ external: [...external, "./server.js"]
282
+ };
283
+ applyJSXOptions(workerBuildConfig, jsxOptions);
284
+ const workerBuildResult = await ESBuild.build(workerBuildConfig);
285
+ validateDynamicImports(workerBuildResult, "worker bundle", [
286
+ "./server.js"
287
+ ]);
288
+ validateExternals(workerBuildResult, external, "worker bundle");
289
+ }
290
+ const buildConfig = {
291
+ stdin: {
292
+ contents: entryWrapper,
293
+ // Use serverDir so ./server.js resolves to the built user code
294
+ resolveDir: serverDir,
295
+ sourcefile: "virtual-entry.js"
296
+ },
297
+ bundle: true,
298
+ format: BUILD_DEFAULTS.format,
299
+ target,
300
+ platform: platformESBuildConfig.platform ?? "node",
301
+ // Inline bundling (Cloudflare): single-file (server.js contains everything)
302
+ // Separate bundling (Node/Bun): multi-file (index.js is entry, server.js is user code)
303
+ outfile: join(
304
+ serverDir,
305
+ bundlesUserCodeInline ? "server.js" : BUILD_DEFAULTS.outputFile
306
+ ),
307
+ absWorkingDir: projectRoot,
308
+ mainFields: ["module", "main"],
309
+ conditions: platformESBuildConfig.conditions ?? ["import", "module"],
310
+ // Resolve packages from the user's project node_modules
311
+ nodePaths: [getNodeModulesPath()],
312
+ // User plugins run first, then Shovel's core plugins
313
+ plugins: bundlesUserCodeInline ? [
314
+ ...userPlugins,
315
+ createConfigPlugin(projectRoot, outputDir, { platformDefaults }),
316
+ importMetaPlugin(),
317
+ assetsPlugin({
318
+ outDir: outputDir,
319
+ clientBuild: {
320
+ jsx: jsxOptions.jsx,
321
+ jsxFactory: jsxOptions.jsxFactory,
322
+ jsxFragment: jsxOptions.jsxFragment,
323
+ jsxImportSource: jsxOptions.jsxImportSource
324
+ }
325
+ })
326
+ ] : [createConfigPlugin(projectRoot, outputDir, { platformDefaults })],
327
+ // Config plugin needed for entry wrapper
328
+ metafile: true,
329
+ sourcemap,
330
+ minify,
331
+ treeShaking,
332
+ define: {
333
+ ...platformESBuildConfig.define ?? {},
334
+ // User-defined constants
335
+ ...shovelBuildConfig?.define ?? {},
336
+ // Inject output directory for runtime.outdir() resolution
337
+ __SHOVEL_OUTDIR__: JSON.stringify(outputDir),
338
+ // Inject git commit SHA for [git] placeholder
339
+ __SHOVEL_GIT__: JSON.stringify(getGitSHA(projectRoot))
340
+ },
341
+ // Path aliases from user config
342
+ alias: shovelBuildConfig?.alias,
343
+ external
344
+ };
345
+ if (bundlesUserCodeInline) {
346
+ applyJSXOptions(buildConfig, jsxOptions);
347
+ }
348
+ return buildConfig;
349
+ } catch (error) {
350
+ throw new Error(`Failed to create build configuration: ${error}`);
351
+ }
352
+ }
353
+ async function generatePackageJSON({
354
+ serverDir,
355
+ platform,
356
+ entryPath
357
+ }) {
358
+ const entryDir = dirname(entryPath);
359
+ const sourcePackageJsonPath = resolve(entryDir, "package.json");
360
+ try {
361
+ const packageJSONContent = await readFile(sourcePackageJsonPath, "utf8");
362
+ try {
363
+ JSON.parse(packageJSONContent);
364
+ } catch (parseError) {
365
+ throw new Error(`Invalid package.json format: ${parseError}`);
366
+ }
367
+ await writeFile(
368
+ join(serverDir, "package.json"),
369
+ packageJSONContent,
370
+ "utf8"
371
+ );
372
+ logger.debug("Copied package.json", { serverDir });
373
+ } catch (error) {
374
+ logger.debug("Could not copy package.json: {error}", { error });
375
+ try {
376
+ const generatedPackageJson = await generateExecutablePackageJSON(platform);
377
+ await writeFile(
378
+ join(serverDir, "package.json"),
379
+ JSON.stringify(generatedPackageJson, null, 2),
380
+ "utf8"
381
+ );
382
+ logger.debug("Generated package.json", { platform });
383
+ } catch (generateError) {
384
+ logger.debug("Could not generate package.json: {error}", {
385
+ error: generateError
386
+ });
387
+ }
388
+ }
389
+ }
390
+ async function generateExecutablePackageJSON(platform) {
391
+ const packageJSON = {
392
+ name: "shovel-executable",
393
+ version: "1.0.0",
394
+ type: "module",
395
+ private: true,
396
+ dependencies: {}
397
+ };
398
+ const isWorkspaceEnvironment = findWorkspaceRoot() !== null;
399
+ if (isWorkspaceEnvironment) {
400
+ packageJSON.dependencies = {};
401
+ } else {
402
+ switch (platform) {
403
+ case "node":
404
+ packageJSON.dependencies["@b9g/platform-node"] = "^0.1.0";
405
+ break;
406
+ case "bun":
407
+ packageJSON.dependencies["@b9g/platform-bun"] = "^0.1.0";
408
+ break;
409
+ case "cloudflare":
410
+ packageJSON.dependencies["@b9g/platform-cloudflare"] = "^0.1.0";
411
+ break;
412
+ default:
413
+ packageJSON.dependencies["@b9g/platform"] = "^0.1.0";
414
+ }
415
+ packageJSON.dependencies["@b9g/cache"] = "^0.1.0";
416
+ packageJSON.dependencies["@b9g/filesystem"] = "^0.1.0";
417
+ }
418
+ return packageJSON;
419
+ }
420
+ async function buildCommand(entrypoint, options, config) {
421
+ const platform = Platform.resolvePlatform({ ...options, config });
422
+ await buildForProduction({
423
+ entrypoint,
424
+ outDir: "dist",
425
+ platform,
426
+ workerCount: options.workers ? parseInt(options.workers, 10) : config.workers,
427
+ userBuildConfig: config.build
428
+ });
429
+ process.exit(0);
430
+ }
431
+ export {
432
+ buildCommand,
433
+ buildForProduction
434
+ };
@@ -0,0 +1,78 @@
1
+ import {
2
+ generateConfigModule,
3
+ generateStorageTypes,
4
+ loadRawConfig
5
+ } from "./chunk-GRAFMTEH.js";
6
+
7
+ // src/plugins/shovel.ts
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { join, isAbsolute } from "node:path";
10
+ function createConfigPlugin(projectRoot, outDir = "dist", options = {}) {
11
+ const rawConfig = loadRawConfig(projectRoot);
12
+ const absoluteOutDir = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
13
+ const configModuleCode = generateConfigModule(rawConfig, {
14
+ projectDir: projectRoot,
15
+ outDir: absoluteOutDir,
16
+ platformDefaults: options.platformDefaults
17
+ });
18
+ const typesCode = generateStorageTypes(rawConfig, {
19
+ platformDefaults: options.platformDefaults
20
+ });
21
+ if (typesCode) {
22
+ const outputDir = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
23
+ const serverOutDir = join(outputDir, "server");
24
+ mkdirSync(serverOutDir, { recursive: true });
25
+ const typesPath = join(serverOutDir, "shovel.d.ts");
26
+ writeFileSync(typesPath, typesCode);
27
+ }
28
+ return {
29
+ name: "shovel-config",
30
+ setup(build) {
31
+ build.onResolve({ filter: /^shovel:config$/ }, (args) => ({
32
+ path: args.path,
33
+ namespace: "shovel-config"
34
+ }));
35
+ build.onLoad({ filter: /.*/, namespace: "shovel-config" }, () => ({
36
+ contents: configModuleCode,
37
+ loader: "js",
38
+ resolveDir: projectRoot
39
+ }));
40
+ }
41
+ };
42
+ }
43
+ function createEntryPlugin(projectRoot, entryCode) {
44
+ return {
45
+ name: "shovel-entry",
46
+ setup(build) {
47
+ build.onResolve({ filter: /^shovel:entry$/ }, (args) => ({
48
+ path: args.path,
49
+ namespace: "shovel-entry"
50
+ }));
51
+ build.onLoad({ filter: /.*/, namespace: "shovel-entry" }, () => ({
52
+ contents: entryCode,
53
+ loader: "js",
54
+ resolveDir: projectRoot
55
+ }));
56
+ }
57
+ };
58
+ }
59
+
60
+ // src/utils/git-sha.ts
61
+ import { execSync } from "child_process";
62
+ function getGitSHA(cwd) {
63
+ try {
64
+ return execSync("git rev-parse HEAD", {
65
+ encoding: "utf8",
66
+ cwd,
67
+ stdio: ["pipe", "pipe", "pipe"]
68
+ }).trim();
69
+ } catch (_err) {
70
+ return "";
71
+ }
72
+ }
73
+
74
+ export {
75
+ createConfigPlugin,
76
+ createEntryPlugin,
77
+ getGitSHA
78
+ };