@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- 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
|
+
}
|