@buenojs/bueno 0.8.0

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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,643 @@
1
+ /**
2
+ * Production Bundler Implementation
3
+ *
4
+ * Provides zero-config bundling using Bun's native Bun.build() API.
5
+ * Supports React, Vue, Svelte, and Solid frameworks with automatic detection.
6
+ *
7
+ * Features:
8
+ * - Auto framework detection from package.json
9
+ * - Code splitting by route
10
+ * - CSS extraction and optimization
11
+ * - Asset optimization
12
+ * - Source map generation
13
+ * - Build manifest for SSR integration
14
+ */
15
+
16
+ import { createLogger, type Logger } from "../logger/index.js";
17
+ import { watch } from "fs";
18
+ import type { FSWatcher } from "fs";
19
+ import type {
20
+ BundlerConfig,
21
+ PartialBundlerConfig,
22
+ BuildResult,
23
+ BuildOutput,
24
+ BuildError,
25
+ BuildWarning,
26
+ BuildManifest,
27
+ BundleAnalysis,
28
+ BuildWatchCallback,
29
+ BundlerState,
30
+ FrontendFramework,
31
+ FrameworkBuildConfig,
32
+ FrameworkDetectionResult,
33
+ PackageDependencies,
34
+ } from "./types.js";
35
+ import { getFrameworkConfig, getFrameworkMeta } from "./frameworks/index.js";
36
+
37
+ // ============= Constants =============
38
+
39
+ const DEFAULT_OUT_DIR = "dist";
40
+ const DEFAULT_ENV_PREFIX = "PUBLIC_";
41
+ const FRAMEWORK_INDICATORS: Record<FrontendFramework, string[]> = {
42
+ react: ["react", "react-dom"],
43
+ vue: ["vue"],
44
+ svelte: ["svelte"],
45
+ solid: ["solid-js"],
46
+ };
47
+
48
+ // ============= Framework Detection =============
49
+
50
+ /**
51
+ * Detect framework from package.json dependencies
52
+ */
53
+ function detectFramework(rootDir: string): FrameworkDetectionResult {
54
+ try {
55
+ const packageJsonPath = `${rootDir}/package.json`;
56
+ const packageJsonFile = Bun.file(packageJsonPath);
57
+
58
+ if (!packageJsonFile.exists()) {
59
+ return {
60
+ framework: "react",
61
+ detected: false,
62
+ source: "config",
63
+ };
64
+ }
65
+
66
+ // Read package.json synchronously
67
+ const packageJson = JSON.parse(require("fs").readFileSync(packageJsonPath, "utf-8"));
68
+ const dependencies: PackageDependencies = {
69
+ ...packageJson.dependencies,
70
+ ...packageJson.devDependencies,
71
+ };
72
+
73
+ // Check for each framework in order of specificity
74
+ // Solid and Svelte are more specific than React/Vue
75
+ const frameworkOrder: FrontendFramework[] = ["solid", "svelte", "vue", "react"];
76
+
77
+ for (const framework of frameworkOrder) {
78
+ const indicators = FRAMEWORK_INDICATORS[framework];
79
+ if (indicators.some((pkg) => dependencies[pkg])) {
80
+ return {
81
+ framework,
82
+ detected: true,
83
+ source: "package.json",
84
+ };
85
+ }
86
+ }
87
+
88
+ // Default to React if no framework detected
89
+ return {
90
+ framework: "react",
91
+ detected: false,
92
+ source: "config",
93
+ };
94
+ } catch {
95
+ return {
96
+ framework: "react",
97
+ detected: false,
98
+ source: "config",
99
+ };
100
+ }
101
+ }
102
+
103
+ // ============= Bundler Class =============
104
+
105
+ export class Bundler {
106
+ private config: BundlerConfig;
107
+ private state: BundlerState;
108
+ private logger: Logger;
109
+ private watcher: FSWatcher | null = null;
110
+
111
+ constructor(config: PartialBundlerConfig) {
112
+ this.config = this.normalizeConfig(config);
113
+ this.logger = createLogger({
114
+ level: "debug",
115
+ pretty: true,
116
+ context: { component: "Bundler" },
117
+ });
118
+
119
+ // Detect framework
120
+ const rootDir = this.config.rootDir || process.cwd();
121
+ const frameworkResult =
122
+ this.config.framework === "auto"
123
+ ? detectFramework(rootDir)
124
+ : {
125
+ framework: this.config.framework as FrontendFramework,
126
+ detected: true,
127
+ source: "config" as const,
128
+ };
129
+
130
+ this.state = {
131
+ building: false,
132
+ lastResult: null,
133
+ watching: false,
134
+ framework: frameworkResult.framework,
135
+ };
136
+
137
+ // Update config with detected framework
138
+ this.config.framework = frameworkResult.framework;
139
+
140
+ if (frameworkResult.detected) {
141
+ this.logger.info(`Detected framework: ${frameworkResult.framework}`, {
142
+ source: frameworkResult.source,
143
+ });
144
+ } else {
145
+ this.logger.info(`Using default framework: ${frameworkResult.framework}`);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Normalize partial config to full config with defaults
151
+ */
152
+ private normalizeConfig(config: PartialBundlerConfig): BundlerConfig {
153
+ const rootDir = config.rootDir || process.cwd();
154
+ return {
155
+ entryPoints: config.entryPoints,
156
+ outDir: config.outDir ?? DEFAULT_OUT_DIR,
157
+ framework: config.framework ?? "auto",
158
+ minify: config.minify ?? true,
159
+ sourcemap: config.sourcemap ?? "linked",
160
+ splitting: config.splitting ?? true,
161
+ treeshaking: config.treeshaking ?? true,
162
+ envPrefix: config.envPrefix ?? DEFAULT_ENV_PREFIX,
163
+ define: config.define ?? {},
164
+ external: config.external ?? [],
165
+ target: config.target ?? "browser",
166
+ format: config.format ?? "esm",
167
+ rootDir,
168
+ publicPath: config.publicPath,
169
+ manifest: config.manifest ?? true,
170
+ mode: config.mode,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Get current bundler state
176
+ */
177
+ getState(): BundlerState {
178
+ return { ...this.state };
179
+ }
180
+
181
+ /**
182
+ * Get bundler configuration
183
+ */
184
+ getConfig(): BundlerConfig {
185
+ return { ...this.config };
186
+ }
187
+
188
+ /**
189
+ * Get detected framework
190
+ */
191
+ getFramework(): FrontendFramework {
192
+ return this.state.framework!;
193
+ }
194
+
195
+ /**
196
+ * Get framework-specific build configuration
197
+ */
198
+ getFrameworkConfig(framework: FrontendFramework): FrameworkBuildConfig {
199
+ return getFrameworkConfig(framework);
200
+ }
201
+
202
+ /**
203
+ * Build for production
204
+ */
205
+ async build(): Promise<BuildResult> {
206
+ const startTime = Date.now();
207
+
208
+ if (this.state.building) {
209
+ this.logger.warn("Build already in progress");
210
+ return {
211
+ success: false,
212
+ outputs: [],
213
+ errors: [{ message: "Build already in progress" }],
214
+ warnings: [],
215
+ duration: 0,
216
+ totalSize: 0,
217
+ };
218
+ }
219
+
220
+ this.state.building = true;
221
+ this.logger.info("Starting production build...");
222
+
223
+ try {
224
+ // Get framework configuration
225
+ const framework = this.state.framework!;
226
+ const frameworkConfig = this.getFrameworkConfig(framework);
227
+
228
+ // Prepare entry points
229
+ const entryPoints = Array.isArray(this.config.entryPoints)
230
+ ? this.config.entryPoints
231
+ : [this.config.entryPoints];
232
+
233
+ // Collect environment variables with prefix
234
+ const envVars = this.collectEnvVars();
235
+
236
+ // Merge defines
237
+ const define = {
238
+ ...frameworkConfig.define,
239
+ ...this.config.define,
240
+ ...envVars,
241
+ };
242
+
243
+ // Build using Bun.build()
244
+ const buildResult = await Bun.build({
245
+ entrypoints: entryPoints.map((e) =>
246
+ e.startsWith("/") ? e : `${this.config.rootDir}/${e}`
247
+ ),
248
+ outdir: this.config.outDir,
249
+ minify: this.config.minify,
250
+ splitting: this.config.splitting,
251
+ sourcemap: this.config.sourcemap === "none" ? "external" : this.config.sourcemap,
252
+ define,
253
+ external: [...this.config.external, ...frameworkConfig.external],
254
+ target: this.config.target,
255
+ format: this.config.format,
256
+ // JSX configuration
257
+ jsx: frameworkConfig.jsxRuntime === "automatic"
258
+ ? { runtime: "automatic", importSource: frameworkConfig.jsxImportSource }
259
+ : { runtime: "classic" },
260
+ // Public path for assets
261
+ publicPath: this.config.publicPath,
262
+ // Generate manifest
263
+ metafile: true,
264
+ });
265
+
266
+ const duration = Date.now() - startTime;
267
+
268
+ if (!buildResult.success) {
269
+ const result: BuildResult = {
270
+ success: false,
271
+ outputs: [],
272
+ errors: buildResult.logs
273
+ .filter((log) => log.level === "error")
274
+ .map((log) => this.parseBuildError(log)),
275
+ warnings: buildResult.logs
276
+ .filter((log) => log.level === "warning")
277
+ .map((log) => this.parseBuildWarning(log)),
278
+ duration,
279
+ totalSize: 0,
280
+ };
281
+
282
+ this.state.lastResult = result;
283
+ this.logger.error(`Build failed in ${duration}ms`, result.errors);
284
+ return result;
285
+ }
286
+
287
+ // Process outputs
288
+ const outputs = this.processBuildOutputs(buildResult.outputs);
289
+
290
+ // Generate manifest
291
+ const manifest = this.config.manifest
292
+ ? await this.generateManifest(buildResult, outputs, duration)
293
+ : undefined;
294
+
295
+ // Calculate total size
296
+ const totalSize = outputs.reduce((sum, output) => sum + output.size, 0);
297
+
298
+ const result: BuildResult = {
299
+ success: true,
300
+ outputs,
301
+ errors: [],
302
+ warnings: buildResult.logs
303
+ .filter((log) => log.level === "warning")
304
+ .map((log) => this.parseBuildWarning(log)),
305
+ duration,
306
+ manifest,
307
+ totalSize,
308
+ };
309
+
310
+ this.state.lastResult = result;
311
+ this.logger.info(`Build completed in ${duration}ms`, {
312
+ outputs: outputs.length,
313
+ totalSize: `${(totalSize / 1024).toFixed(2)} KB`,
314
+ });
315
+
316
+ return result;
317
+ } catch (error) {
318
+ const duration = Date.now() - startTime;
319
+ const result: BuildResult = {
320
+ success: false,
321
+ outputs: [],
322
+ errors: [
323
+ {
324
+ message: error instanceof Error ? error.message : "Unknown build error",
325
+ stack: error instanceof Error ? error.stack : undefined,
326
+ },
327
+ ],
328
+ warnings: [],
329
+ duration,
330
+ totalSize: 0,
331
+ };
332
+
333
+ this.state.lastResult = result;
334
+ this.logger.error(`Build failed in ${duration}ms`, error);
335
+ return result;
336
+ } finally {
337
+ this.state.building = false;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Watch mode for development
343
+ */
344
+ watch(callback: BuildWatchCallback): void {
345
+ if (this.state.watching) {
346
+ this.logger.warn("Watch mode already active");
347
+ return;
348
+ }
349
+
350
+ this.state.watching = true;
351
+ this.logger.info("Starting watch mode...");
352
+
353
+ // Initial build
354
+ this.build().then(callback);
355
+
356
+ // Watch for file changes using fs.watch
357
+ const entryPoints = Array.isArray(this.config.entryPoints)
358
+ ? this.config.entryPoints
359
+ : [this.config.entryPoints];
360
+
361
+ const srcDir = `${this.config.rootDir}/src`;
362
+
363
+ this.watcher = watch(
364
+ srcDir,
365
+ { recursive: true },
366
+ async (event: "rename" | "change", filename: string | null) => {
367
+ if (!filename) return;
368
+
369
+ const filePath = `${srcDir}/${filename}`;
370
+ this.logger.debug(`File changed: ${filePath}`);
371
+
372
+ // Check if changed file is relevant
373
+ if (this.isRelevantFile(filePath, entryPoints)) {
374
+ const result = await this.build();
375
+ callback(result);
376
+ }
377
+ }
378
+ );
379
+
380
+ this.logger.info("Watching for file changes...");
381
+ }
382
+
383
+ /**
384
+ * Stop watch mode
385
+ */
386
+ stopWatch(): void {
387
+ if (this.watcher) {
388
+ this.watcher.close();
389
+ this.watcher = null;
390
+ this.state.watching = false;
391
+ this.logger.info("Watch mode stopped");
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Analyze bundle size
397
+ */
398
+ async analyze(): Promise<BundleAnalysis> {
399
+ if (!this.state.lastResult || !this.state.lastResult.success) {
400
+ throw new Error("No successful build to analyze");
401
+ }
402
+
403
+ const result = this.state.lastResult;
404
+ const modules: BundleAnalysis["modules"] = [];
405
+ const largeModules: BundleAnalysis["largeModules"] = [];
406
+ const dependencyTree: BundleAnalysis["dependencyTree"] = {};
407
+
408
+ // Process outputs for analysis
409
+ for (const output of result.outputs) {
410
+ if (output.type === "js") {
411
+ const percentage = (output.size / result.totalSize) * 100;
412
+ modules.push({
413
+ path: output.path,
414
+ size: output.size,
415
+ percentage,
416
+ });
417
+
418
+ // Track large modules (>50KB)
419
+ if (output.size > 50 * 1024) {
420
+ largeModules.push({
421
+ path: output.path,
422
+ size: output.size,
423
+ });
424
+ }
425
+
426
+ // Build dependency tree
427
+ if (output.imports) {
428
+ dependencyTree[output.path] = output.imports;
429
+ }
430
+ }
431
+ }
432
+
433
+ // Sort modules by size
434
+ modules.sort((a, b) => b.size - a.size);
435
+
436
+ // Detect duplicates (simplified)
437
+ const duplicates: BundleAnalysis["duplicates"] = [];
438
+ const moduleOccurrences = new Map<string, number>();
439
+
440
+ for (const output of result.outputs) {
441
+ if (output.imports) {
442
+ for (const imp of output.imports) {
443
+ moduleOccurrences.set(imp, (moduleOccurrences.get(imp) || 0) + 1);
444
+ }
445
+ }
446
+ }
447
+
448
+ for (const [module, occurrences] of moduleOccurrences) {
449
+ if (occurrences > 1) {
450
+ duplicates.push({
451
+ module,
452
+ occurrences,
453
+ wastedBytes: 0, // Would need metafile analysis for exact size
454
+ });
455
+ }
456
+ }
457
+
458
+ return {
459
+ totalSize: result.totalSize,
460
+ modules,
461
+ duplicates,
462
+ largeModules,
463
+ dependencyTree,
464
+ };
465
+ }
466
+
467
+ // ============= Private Methods =============
468
+
469
+ /**
470
+ * Collect environment variables with the configured prefix
471
+ */
472
+ private collectEnvVars(): Record<string, string> {
473
+ const envVars: Record<string, string> = {};
474
+ const prefix = this.config.envPrefix;
475
+
476
+ for (const [key, value] of Object.entries(process.env)) {
477
+ if (key.startsWith(prefix) && value !== undefined) {
478
+ envVars[`process.env.${key}`] = JSON.stringify(value);
479
+ }
480
+ }
481
+
482
+ return envVars;
483
+ }
484
+
485
+ /**
486
+ * Parse build error from Bun build log
487
+ */
488
+ private parseBuildError(log: { message: string; position?: { line: number; column: number } | null; file?: string }): BuildError {
489
+ return {
490
+ message: log.message,
491
+ file: log.file,
492
+ line: log.position?.line ?? undefined,
493
+ column: log.position?.column ?? undefined,
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Parse build warning from Bun build log
499
+ */
500
+ private parseBuildWarning(log: { message: string; position?: { line: number; column: number } | null; file?: string }): BuildWarning {
501
+ return {
502
+ message: log.message,
503
+ file: log.file,
504
+ line: log.position?.line ?? undefined,
505
+ column: log.position?.column ?? undefined,
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Process build outputs from Bun.build result
511
+ */
512
+ private processBuildOutputs(outputs: Awaited<ReturnType<typeof Bun.build>>["outputs"]): BuildOutput[] {
513
+ return outputs.map((output) => {
514
+ const path = output.path.replace(`${this.config.outDir}/`, "");
515
+ const type = this.getOutputType(output.path);
516
+ const hash = this.extractHash(output.path);
517
+
518
+ const buildOutput: BuildOutput = {
519
+ path,
520
+ type,
521
+ size: output.size,
522
+ hash,
523
+ };
524
+
525
+ return buildOutput;
526
+ });
527
+ }
528
+
529
+ /**
530
+ * Get output file type
531
+ */
532
+ private getOutputType(path: string): "js" | "css" | "asset" {
533
+ if (path.endsWith(".js") || path.endsWith(".mjs")) {
534
+ return "js";
535
+ }
536
+ if (path.endsWith(".css")) {
537
+ return "css";
538
+ }
539
+ return "asset";
540
+ }
541
+
542
+ /**
543
+ * Extract content hash from filename
544
+ */
545
+ private extractHash(path: string): string | undefined {
546
+ const match = path.match(/\.([a-f0-9]{8,})\.(js|css)$/);
547
+ return match ? match[1] : undefined;
548
+ }
549
+
550
+ /**
551
+ * Generate build manifest for SSR integration
552
+ */
553
+ private async generateManifest(
554
+ buildResult: Awaited<ReturnType<typeof Bun.build>>,
555
+ outputs: BuildOutput[],
556
+ duration: number
557
+ ): Promise<BuildManifest> {
558
+ const entryPoints: Record<string, string[]> = {};
559
+ const files: BuildManifest["files"] = {};
560
+ const css: Record<string, string[]> = {};
561
+
562
+ // Process entry points
563
+ const entryNames = Array.isArray(this.config.entryPoints)
564
+ ? this.config.entryPoints.map((e) => e.split("/").pop()?.replace(/\.[^.]+$/, "") || "main")
565
+ : [this.config.entryPoints.split("/").pop()?.replace(/\.[^.]+$/, "") || "main"];
566
+
567
+ for (const name of entryNames) {
568
+ entryPoints[name] = outputs
569
+ .filter((o) => o.type === "js" && (o.entryPoint === name || !o.entryPoint))
570
+ .map((o) => o.path);
571
+
572
+ css[name] = outputs
573
+ .filter((o) => o.type === "css")
574
+ .map((o) => o.path);
575
+ }
576
+
577
+ // Process all files
578
+ for (const output of outputs) {
579
+ files[output.path] = {
580
+ type: output.type,
581
+ size: output.size,
582
+ hash: output.hash,
583
+ imports: output.imports,
584
+ dynamicImports: output.dynamicImports,
585
+ };
586
+ }
587
+
588
+ const manifest: BuildManifest = {
589
+ entryPoints,
590
+ files,
591
+ css,
592
+ timestamp: Date.now(),
593
+ duration,
594
+ };
595
+
596
+ // Write manifest to disk
597
+ const manifestPath = `${this.config.outDir}/manifest.json`;
598
+ await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
599
+ this.logger.debug(`Build manifest written to ${manifestPath}`);
600
+
601
+ return manifest;
602
+ }
603
+
604
+ /**
605
+ * Check if a file change is relevant to the build
606
+ */
607
+ private isRelevantFile(filePath: string, entryPoints: string[]): boolean {
608
+ // Check if file is in source directory
609
+ const srcDir = `${this.config.rootDir}/src`;
610
+ if (!filePath.startsWith(srcDir)) {
611
+ return false;
612
+ }
613
+
614
+ // Check file extension
615
+ const supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".css", ".vue", ".svelte"];
616
+ return supportedExtensions.some((ext) => filePath.endsWith(ext));
617
+ }
618
+ }
619
+
620
+ // ============= Factory Function =============
621
+
622
+ /**
623
+ * Create a bundler instance
624
+ */
625
+ export function createBundler(config: PartialBundlerConfig): Bundler {
626
+ return new Bundler(config);
627
+ }
628
+
629
+ // ============= Utility Functions =============
630
+
631
+ /**
632
+ * Quick build function for simple use cases
633
+ */
634
+ export async function build(
635
+ entryPoints: string | string[],
636
+ options?: Partial<Omit<PartialBundlerConfig, "entryPoints">>
637
+ ): Promise<BuildResult> {
638
+ const bundler = createBundler({
639
+ entryPoints,
640
+ ...options,
641
+ });
642
+ return bundler.build();
643
+ }