@grupodiariodaregiao/bunstone 0.5.1 → 0.5.3
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/bin/cli.ts +110 -7
- package/dist/index.js +153 -106
- package/dist/lib/app-startup.d.ts +0 -6
- package/dist/lib/utils/bundler.d.ts +8 -0
- package/lib/app-startup.ts +5 -134
- package/lib/utils/bundler.ts +184 -0
- package/package.json +1 -1
- package/starter/package.json +3 -2
package/bin/cli.ts
CHANGED
|
@@ -504,32 +504,135 @@ async function scaffold(projectName_?: string) {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
//
|
|
508
|
-
// Help
|
|
509
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
507
|
+
// ── Help ───────────────────────────────────────────────────────────────────
|
|
510
508
|
|
|
511
509
|
function printHelp() {
|
|
512
510
|
console.log(`
|
|
513
511
|
${bold("bunstone")} — CLI for the Bunstone framework
|
|
514
512
|
|
|
515
513
|
${cyan("Usage:")}
|
|
516
|
-
bunstone new <project-name>
|
|
517
|
-
bunstone run [bun-flags] <entry>
|
|
518
|
-
bunstone
|
|
514
|
+
bunstone new <project-name> Scaffold a new project
|
|
515
|
+
bunstone run [bun-flags] <entry> Run your app with enhanced error messages
|
|
516
|
+
bunstone build [entry] [options] Build your app for production
|
|
517
|
+
bunstone exports List all public exports
|
|
518
|
+
|
|
519
|
+
${cyan("Build Options:")}
|
|
520
|
+
--views <dir> Directory containing React views (default: src/views)
|
|
521
|
+
--out <dir> Output directory (default: dist)
|
|
522
|
+
--compile Compile to a standalone binary
|
|
523
|
+
--no-bundle Skip application bundling (only bundle views)
|
|
519
524
|
|
|
520
525
|
${cyan("Examples:")}
|
|
521
526
|
bunstone new my-api
|
|
522
527
|
bunstone run src/main.ts
|
|
523
|
-
bunstone
|
|
528
|
+
bunstone build src/main.ts
|
|
529
|
+
bunstone build --compile
|
|
524
530
|
`);
|
|
525
531
|
}
|
|
526
532
|
|
|
533
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
534
|
+
// bunstone build [entry] [options]
|
|
535
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
async function buildCommand(buildArgs: string[]) {
|
|
538
|
+
console.log(`${cyan(BORDER)}`);
|
|
539
|
+
console.log(`${cyan(bold(" 📦 Bunstone — Production Build"))}`);
|
|
540
|
+
console.log(`${cyan(BORDER)}\n`);
|
|
541
|
+
|
|
542
|
+
let entry = "";
|
|
543
|
+
let viewsDir = "src/views";
|
|
544
|
+
let outDir = "dist";
|
|
545
|
+
let compile = false;
|
|
546
|
+
let skipAppBundle = false;
|
|
547
|
+
|
|
548
|
+
for (let i = 0; i < buildArgs.length; i++) {
|
|
549
|
+
const arg = buildArgs[i];
|
|
550
|
+
if (arg === "--views") {
|
|
551
|
+
viewsDir = buildArgs[++i];
|
|
552
|
+
} else if (arg === "--out") {
|
|
553
|
+
outDir = buildArgs[++i];
|
|
554
|
+
} else if (arg === "--compile") {
|
|
555
|
+
compile = true;
|
|
556
|
+
} else if (arg === "--no-bundle") {
|
|
557
|
+
skipAppBundle = true;
|
|
558
|
+
} else if (!arg.startsWith("-")) {
|
|
559
|
+
entry = arg;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Try to detect entry if not provided
|
|
564
|
+
if (!entry && !skipAppBundle) {
|
|
565
|
+
const candidates = ["src/index.ts", "index.ts", "src/main.ts", "main.ts"];
|
|
566
|
+
for (const c of candidates) {
|
|
567
|
+
if (await Bun.file(join(process.cwd(), c)).exists()) {
|
|
568
|
+
entry = c;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (!entry && !skipAppBundle) {
|
|
575
|
+
console.error(red(" ✖ No entrypoint found or specified."));
|
|
576
|
+
console.error(
|
|
577
|
+
gray(" Please specify an entrypoint: bunstone build src/index.ts"),
|
|
578
|
+
);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
// We use dynamic import for Bundler to keep CLI light if not building
|
|
584
|
+
// Since we are in the same repo, we can import from the lib
|
|
585
|
+
const { Bundler } = await import("../lib/utils/bundler");
|
|
586
|
+
|
|
587
|
+
// 1. Build views
|
|
588
|
+
const viewsDirAbs = join(process.cwd(), viewsDir);
|
|
589
|
+
const viewsStat = await Bun.file(viewsDirAbs)
|
|
590
|
+
.stat()
|
|
591
|
+
.catch(() => null);
|
|
592
|
+
|
|
593
|
+
if (viewsStat && viewsStat.isDirectory()) {
|
|
594
|
+
console.log(
|
|
595
|
+
` ${yellow("→")} Bundling React views from ${bold(viewsDir)}...`,
|
|
596
|
+
);
|
|
597
|
+
await Bundler.buildViews(viewsDir);
|
|
598
|
+
} else {
|
|
599
|
+
console.log(
|
|
600
|
+
` ${gray("○")} No views directory found at ${viewsDir}, skipping view bundling.`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 2. Build app
|
|
605
|
+
if (!skipAppBundle) {
|
|
606
|
+
console.log(
|
|
607
|
+
` ${yellow("→")} Bundling application ${bold(entry)} to ${bold(outDir)}...`,
|
|
608
|
+
);
|
|
609
|
+
await Bundler.buildApp(entry, outDir, compile);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
console.log(`\n${green(" ✅ Build completed successfully!")}`);
|
|
613
|
+
if (!skipAppBundle) {
|
|
614
|
+
console.log(
|
|
615
|
+
` Output: ${bold(outDir + (compile ? "/app" : "/index.js"))}`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
} catch (error: any) {
|
|
619
|
+
console.error(`\n${red(" ✖ Build failed:")}`);
|
|
620
|
+
console.error(red(` ${error.message}`));
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
527
625
|
async function main() {
|
|
528
626
|
if (command === "run") {
|
|
529
627
|
await runCommand(args.slice(1));
|
|
530
628
|
return;
|
|
531
629
|
}
|
|
532
630
|
|
|
631
|
+
if (command === "build") {
|
|
632
|
+
await buildCommand(args.slice(1));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
533
636
|
if (command === "exports") {
|
|
534
637
|
await exportsCommand();
|
|
535
638
|
return;
|
package/dist/index.js
CHANGED
|
@@ -100673,9 +100673,7 @@ function toPublicBucketPath(key) {
|
|
|
100673
100673
|
return `/${normalizeS3Key(key)}`;
|
|
100674
100674
|
}
|
|
100675
100675
|
// lib/app-startup.ts
|
|
100676
|
-
import {
|
|
100677
|
-
import { mkdir, readdir } from "fs/promises";
|
|
100678
|
-
import { basename, extname as extname2, join as join3, resolve } from "path";
|
|
100676
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
100679
100677
|
import { cors } from "@elysiajs/cors";
|
|
100680
100678
|
import { html } from "@elysiajs/html";
|
|
100681
100679
|
import jwt2 from "@elysiajs/jwt";
|
|
@@ -117539,11 +117537,158 @@ function Render(component) {
|
|
|
117539
117537
|
};
|
|
117540
117538
|
}
|
|
117541
117539
|
|
|
117540
|
+
// lib/utils/bundler.ts
|
|
117541
|
+
import { statSync as statSync2 } from "fs";
|
|
117542
|
+
import { mkdir, readdir } from "fs/promises";
|
|
117543
|
+
import { basename, extname as extname2, join as join3, resolve } from "path";
|
|
117544
|
+
|
|
117542
117545
|
// lib/utils/cwd.ts
|
|
117543
117546
|
function cwd() {
|
|
117544
117547
|
return process.cwd();
|
|
117545
117548
|
}
|
|
117546
117549
|
|
|
117550
|
+
// lib/utils/bundler.ts
|
|
117551
|
+
class Bundler {
|
|
117552
|
+
static logger = new Logger("Bundler");
|
|
117553
|
+
static async buildViews(viewsDir, outDir = "./public") {
|
|
117554
|
+
const viewsDirAbs = resolve(viewsDir);
|
|
117555
|
+
const viewsDirStat = statSync2(viewsDirAbs, { throwIfNoEntry: false });
|
|
117556
|
+
if (!viewsDirStat || !viewsDirStat.isDirectory())
|
|
117557
|
+
return;
|
|
117558
|
+
const bunstoneDir = join3(cwd(), ".bunstone");
|
|
117559
|
+
const bunstoneDirExists = await Bun.file(bunstoneDir).exists();
|
|
117560
|
+
if (!bunstoneDirExists) {
|
|
117561
|
+
await mkdir(bunstoneDir, { recursive: true });
|
|
117562
|
+
}
|
|
117563
|
+
const files = await Bundler.getFilesRecursively(viewsDirAbs);
|
|
117564
|
+
Bundler.logger.log(`Auto-bundling views from ${viewsDirAbs} (${files.length} views found)`);
|
|
117565
|
+
for (const absolutePath of files) {
|
|
117566
|
+
if (absolutePath.endsWith(".tsx") || absolutePath.endsWith(".jsx")) {
|
|
117567
|
+
const componentName = basename(absolutePath, extname2(absolutePath));
|
|
117568
|
+
const entryPath = join3(bunstoneDir, `${componentName}.client.tsx`);
|
|
117569
|
+
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
117570
|
+
const entryContent = Bundler.generateHydrationEntry(absolutePath, componentName);
|
|
117571
|
+
await Bun.write(entryPath, entryContent);
|
|
117572
|
+
await Bundler.bundleView(entryPath, outDir, bundleName);
|
|
117573
|
+
}
|
|
117574
|
+
}
|
|
117575
|
+
}
|
|
117576
|
+
static async bundleView(entryPath, outdir, outputName) {
|
|
117577
|
+
try {
|
|
117578
|
+
const result = await Bun.build({
|
|
117579
|
+
entrypoints: [entryPath],
|
|
117580
|
+
outdir,
|
|
117581
|
+
naming: outputName,
|
|
117582
|
+
minify: true,
|
|
117583
|
+
external: [
|
|
117584
|
+
"react",
|
|
117585
|
+
"react-dom",
|
|
117586
|
+
"react-dom/client",
|
|
117587
|
+
"react/jsx-runtime",
|
|
117588
|
+
"react/jsx-dev-runtime"
|
|
117589
|
+
]
|
|
117590
|
+
});
|
|
117591
|
+
if (!result.success) {
|
|
117592
|
+
Bundler.logger.error(`Bundle failed for ${outputName}: ${result.logs.map((l5) => l5.message).join(`
|
|
117593
|
+
`)}`);
|
|
117594
|
+
} else {
|
|
117595
|
+
Bundler.logger.log(`Bundle created successfully: ${outdir}/${outputName}`);
|
|
117596
|
+
}
|
|
117597
|
+
} catch (error48) {
|
|
117598
|
+
Bundler.logger.error(`Error during bundling ${outputName}: ${error48.message}`);
|
|
117599
|
+
}
|
|
117600
|
+
}
|
|
117601
|
+
static async getFilesRecursively(dir) {
|
|
117602
|
+
let results = [];
|
|
117603
|
+
const list = await readdir(dir);
|
|
117604
|
+
for (const file2 of list) {
|
|
117605
|
+
const fullPath = join3(dir, file2);
|
|
117606
|
+
const stat = statSync2(fullPath);
|
|
117607
|
+
if (stat?.isDirectory()) {
|
|
117608
|
+
results = results.concat(await Bundler.getFilesRecursively(fullPath));
|
|
117609
|
+
} else {
|
|
117610
|
+
results.push(resolve(fullPath));
|
|
117611
|
+
}
|
|
117612
|
+
}
|
|
117613
|
+
return results;
|
|
117614
|
+
}
|
|
117615
|
+
static generateHydrationEntry(path3, name3) {
|
|
117616
|
+
return `
|
|
117617
|
+
import React from 'react';
|
|
117618
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
117619
|
+
import * as Mod from '${path3}';
|
|
117620
|
+
|
|
117621
|
+
const Component = Mod['${name3}'] || Mod.default;
|
|
117622
|
+
|
|
117623
|
+
function hydrate() {
|
|
117624
|
+
const dataElement = document.getElementById("__BUNSTONE_DATA__");
|
|
117625
|
+
const data = dataElement ? JSON.parse(dataElement.textContent || "{}") : {};
|
|
117626
|
+
|
|
117627
|
+
if (typeof document !== 'undefined' && Component) {
|
|
117628
|
+
const root = document.getElementById("root");
|
|
117629
|
+
if (root) {
|
|
117630
|
+
try {
|
|
117631
|
+
hydrateRoot(root, React.createElement(Component, data));
|
|
117632
|
+
console.log('[Bunstone] Hydration successful for component: ${name3}');
|
|
117633
|
+
} catch (e) {
|
|
117634
|
+
console.error('[Bunstone] Hydration failed for component: ${name3}', e);
|
|
117635
|
+
}
|
|
117636
|
+
} else {
|
|
117637
|
+
console.error('[Bunstone] Root element "root" not found for hydration.');
|
|
117638
|
+
}
|
|
117639
|
+
} else {
|
|
117640
|
+
console.error('[Bunstone] Component ${name3} not found in bundle.');
|
|
117641
|
+
}
|
|
117642
|
+
}
|
|
117643
|
+
|
|
117644
|
+
if (document.readyState === 'loading') {
|
|
117645
|
+
document.addEventListener('DOMContentLoaded', hydrate);
|
|
117646
|
+
} else {
|
|
117647
|
+
hydrate();
|
|
117648
|
+
}
|
|
117649
|
+
`;
|
|
117650
|
+
}
|
|
117651
|
+
static async buildApp(entrypoint, outdir = "./dist", compile2 = false) {
|
|
117652
|
+
const entryAbs = resolve(entrypoint);
|
|
117653
|
+
if (!await Bun.file(entryAbs).exists()) {
|
|
117654
|
+
throw new Error(`Entrypoint not found: ${entrypoint}`);
|
|
117655
|
+
}
|
|
117656
|
+
Bundler.logger.log(`Building application from ${entrypoint}...`);
|
|
117657
|
+
if (compile2) {
|
|
117658
|
+
const result = await Bun.spawn([
|
|
117659
|
+
"bun",
|
|
117660
|
+
"build",
|
|
117661
|
+
entrypoint,
|
|
117662
|
+
"--compile",
|
|
117663
|
+
"--outfile",
|
|
117664
|
+
join3(outdir, "app")
|
|
117665
|
+
], {
|
|
117666
|
+
stdout: "inherit",
|
|
117667
|
+
stderr: "inherit"
|
|
117668
|
+
});
|
|
117669
|
+
const exitCode = await result.exited;
|
|
117670
|
+
if (exitCode !== 0) {
|
|
117671
|
+
throw new Error("Compilation failed");
|
|
117672
|
+
}
|
|
117673
|
+
} else {
|
|
117674
|
+
const result = await Bun.build({
|
|
117675
|
+
entrypoints: [entrypoint],
|
|
117676
|
+
outdir,
|
|
117677
|
+
target: "bun",
|
|
117678
|
+
minify: true,
|
|
117679
|
+
sourcemap: "external",
|
|
117680
|
+
external: ["react", "react-dom", "elysia", "@elysiajs/*"]
|
|
117681
|
+
});
|
|
117682
|
+
if (!result.success) {
|
|
117683
|
+
Bundler.logger.error("Build failed:", result.logs.map((l5) => l5.message).join(`
|
|
117684
|
+
`));
|
|
117685
|
+
throw new Error("Application build failed");
|
|
117686
|
+
}
|
|
117687
|
+
Bundler.logger.log(`Application built successfully to ${outdir}`);
|
|
117688
|
+
}
|
|
117689
|
+
}
|
|
117690
|
+
}
|
|
117691
|
+
|
|
117547
117692
|
// lib/app-startup.ts
|
|
117548
117693
|
class AppStartup {
|
|
117549
117694
|
static elysia = new Elysia;
|
|
@@ -117554,7 +117699,6 @@ class AppStartup {
|
|
|
117554
117699
|
static destroyPromise = null;
|
|
117555
117700
|
static hasBeenDestroyed = false;
|
|
117556
117701
|
static rootModule;
|
|
117557
|
-
static viewBundles = new Map;
|
|
117558
117702
|
static globalRateLimitConfig;
|
|
117559
117703
|
static rateLimitService = new RateLimitService;
|
|
117560
117704
|
static async create(module, options) {
|
|
@@ -117567,14 +117711,14 @@ class AppStartup {
|
|
|
117567
117711
|
AppStartup.hasBeenDestroyed = false;
|
|
117568
117712
|
const publicExists = await Bun.file("public").exists();
|
|
117569
117713
|
if (!publicExists)
|
|
117570
|
-
await
|
|
117714
|
+
await mkdir2("./public", { recursive: true });
|
|
117571
117715
|
AppStartup.elysia.use(html());
|
|
117572
117716
|
AppStartup.elysia.use(staticPlugin({
|
|
117573
117717
|
assets: "public",
|
|
117574
117718
|
prefix: "/public"
|
|
117575
117719
|
}));
|
|
117576
117720
|
if (options?.viewsDir) {
|
|
117577
|
-
|
|
117721
|
+
Bundler.buildViews(options.viewsDir).catch((err) => {
|
|
117578
117722
|
AppStartup.logger.error(`Failed to auto-bundle views: ${err.message}`);
|
|
117579
117723
|
});
|
|
117580
117724
|
}
|
|
@@ -117667,104 +117811,6 @@ class AppStartup {
|
|
|
117667
117811
|
process.exit(1);
|
|
117668
117812
|
}
|
|
117669
117813
|
}
|
|
117670
|
-
static async bundle(entryPath, outputName) {
|
|
117671
|
-
try {
|
|
117672
|
-
const result = await Bun.build({
|
|
117673
|
-
entrypoints: [entryPath],
|
|
117674
|
-
outdir: "./public",
|
|
117675
|
-
naming: outputName,
|
|
117676
|
-
minify: true,
|
|
117677
|
-
external: [
|
|
117678
|
-
"react",
|
|
117679
|
-
"react-dom",
|
|
117680
|
-
"react-dom/client",
|
|
117681
|
-
"react/jsx-runtime",
|
|
117682
|
-
"react/jsx-dev-runtime"
|
|
117683
|
-
]
|
|
117684
|
-
});
|
|
117685
|
-
if (!result.success) {
|
|
117686
|
-
AppStartup.logger.error(`Bundle failed for ${outputName}: ${result.logs.map((l5) => l5.message).join(`
|
|
117687
|
-
`)}`);
|
|
117688
|
-
} else {
|
|
117689
|
-
AppStartup.logger.log(`Bundle created successfully: public/${outputName}`);
|
|
117690
|
-
}
|
|
117691
|
-
} catch (error48) {
|
|
117692
|
-
AppStartup.logger.error(`Error during bundling ${outputName}: ${error48.message}`);
|
|
117693
|
-
}
|
|
117694
|
-
}
|
|
117695
|
-
static async autoBundle(viewsDir) {
|
|
117696
|
-
const viewDirExists = await Bun.file(viewsDir).exists();
|
|
117697
|
-
if (!viewDirExists)
|
|
117698
|
-
return;
|
|
117699
|
-
const bunstoneDirExists = await Bun.file("./.bunstone").exists();
|
|
117700
|
-
if (!bunstoneDirExists) {
|
|
117701
|
-
await mkdir("./.bunstone", { recursive: true });
|
|
117702
|
-
}
|
|
117703
|
-
const getFilesRecursively = async (dir) => {
|
|
117704
|
-
let results = [];
|
|
117705
|
-
const list = await readdir(dir);
|
|
117706
|
-
for (const file2 of list) {
|
|
117707
|
-
const fullPath = join3(dir, file2);
|
|
117708
|
-
const stat = statSync2(fullPath);
|
|
117709
|
-
if (stat?.isDirectory()) {
|
|
117710
|
-
results = results.concat(await getFilesRecursively(fullPath));
|
|
117711
|
-
} else {
|
|
117712
|
-
results.push(resolve(fullPath));
|
|
117713
|
-
}
|
|
117714
|
-
}
|
|
117715
|
-
return results;
|
|
117716
|
-
};
|
|
117717
|
-
const viewsDirAbs = resolve(viewsDir);
|
|
117718
|
-
const files = await getFilesRecursively(viewsDirAbs);
|
|
117719
|
-
AppStartup.logger.log(`Auto-bundling views from ${viewsDirAbs} (${files.length} views found)`);
|
|
117720
|
-
for (const absolutePath of files) {
|
|
117721
|
-
const file2 = basename(absolutePath);
|
|
117722
|
-
if (file2.endsWith(".tsx") || file2.endsWith(".jsx")) {
|
|
117723
|
-
const componentName = basename(file2, extname2(file2));
|
|
117724
|
-
const entryPath = join3(cwd(), ".bunstone", `${componentName}.client.tsx`);
|
|
117725
|
-
const entryContent = `
|
|
117726
|
-
import React from 'react';
|
|
117727
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
117728
|
-
import * as Mod from '${absolutePath}';
|
|
117729
|
-
|
|
117730
|
-
const Component = Mod['${componentName}'] || Mod.default;
|
|
117731
|
-
|
|
117732
|
-
function hydrate() {
|
|
117733
|
-
const dataElement = document.getElementById("__BUNSTONE_DATA__");
|
|
117734
|
-
const data = dataElement ? JSON.parse(dataElement.textContent || "{}") : {};
|
|
117735
|
-
|
|
117736
|
-
if (typeof document !== 'undefined' && Component) {
|
|
117737
|
-
const root = document.getElementById("root");
|
|
117738
|
-
if (root) {
|
|
117739
|
-
try {
|
|
117740
|
-
hydrateRoot(root, React.createElement(Component, data));
|
|
117741
|
-
console.log('[Bunstone] Hydration successful for component: ${componentName}');
|
|
117742
|
-
} catch (e) {
|
|
117743
|
-
console.error('[Bunstone] Hydration failed for component: ${componentName}', e);
|
|
117744
|
-
}
|
|
117745
|
-
} else {
|
|
117746
|
-
console.error('[Bunstone] Root element "root" not found for hydration.');
|
|
117747
|
-
}
|
|
117748
|
-
} else {
|
|
117749
|
-
console.error('[Bunstone] Component ${componentName} not found in bundle.');
|
|
117750
|
-
}
|
|
117751
|
-
}
|
|
117752
|
-
|
|
117753
|
-
// Ensure DOM is fully loaded before hydrating
|
|
117754
|
-
if (document.readyState === 'loading') {
|
|
117755
|
-
document.addEventListener('DOMContentLoaded', hydrate);
|
|
117756
|
-
} else {
|
|
117757
|
-
hydrate();
|
|
117758
|
-
}
|
|
117759
|
-
`;
|
|
117760
|
-
await Bun.write(entryPath, entryContent);
|
|
117761
|
-
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
117762
|
-
await AppStartup.bundle(entryPath, bundleName);
|
|
117763
|
-
AppStartup.viewBundles.set(componentName, bundleName);
|
|
117764
|
-
AppStartup.viewBundles.set(componentName.toLowerCase(), bundleName);
|
|
117765
|
-
}
|
|
117766
|
-
}
|
|
117767
|
-
}
|
|
117768
117814
|
static listen(port) {
|
|
117769
117815
|
AppStartup.logger.log(`App is running at http://localhost:${port}`);
|
|
117770
117816
|
AppStartup.elysia.listen(port);
|
|
@@ -117806,8 +117852,9 @@ if (document.readyState === 'loading') {
|
|
|
117806
117852
|
context.set.headers["Content-Type"] = "text/html; charset=utf8";
|
|
117807
117853
|
}
|
|
117808
117854
|
const componentName = component.name || component.displayName;
|
|
117809
|
-
const
|
|
117810
|
-
|
|
117855
|
+
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
117856
|
+
const bundle = `/public/${bundleName}`;
|
|
117857
|
+
AppStartup.logger.log(`Rendering component: ${componentName}`);
|
|
117811
117858
|
if (!bundle) {
|
|
117812
117859
|
AppStartup.logger.warn(`No client bundle found for component: ${componentName}. useEffect and other hooks will not work on the client.`);
|
|
117813
117860
|
}
|
|
@@ -14,7 +14,6 @@ export declare class AppStartup {
|
|
|
14
14
|
private static destroyPromise;
|
|
15
15
|
private static hasBeenDestroyed;
|
|
16
16
|
private static rootModule;
|
|
17
|
-
private static readonly viewBundles;
|
|
18
17
|
private static globalRateLimitConfig;
|
|
19
18
|
private static rateLimitService;
|
|
20
19
|
/**
|
|
@@ -62,11 +61,6 @@ export declare class AppStartup {
|
|
|
62
61
|
response: {};
|
|
63
62
|
}>;
|
|
64
63
|
}>;
|
|
65
|
-
/**
|
|
66
|
-
* Bundles a client-side component for hydration (internal).
|
|
67
|
-
*/
|
|
68
|
-
private static bundle;
|
|
69
|
-
private static autoBundle;
|
|
70
64
|
/**
|
|
71
65
|
* Starts the server on the specified port.
|
|
72
66
|
* @param port The port number to listen on.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class Bundler {
|
|
2
|
+
private static logger;
|
|
3
|
+
static buildViews(viewsDir: string, outDir?: string): Promise<void>;
|
|
4
|
+
private static bundleView;
|
|
5
|
+
private static getFilesRecursively;
|
|
6
|
+
private static generateHydrationEntry;
|
|
7
|
+
static buildApp(entrypoint: string, outdir?: string, compile?: boolean): Promise<void>;
|
|
8
|
+
}
|
package/lib/app-startup.ts
CHANGED
|
@@ -43,6 +43,7 @@ import type { RateLimitMetadata } from "./ratelimit/ratelimit.decorator";
|
|
|
43
43
|
import { RateLimitService } from "./ratelimit/ratelimit.service";
|
|
44
44
|
import { RENDER_METADATA } from "./render";
|
|
45
45
|
import type { Options, RateLimitGlobalConfig } from "./types/options";
|
|
46
|
+
import { Bundler } from "./utils/bundler";
|
|
46
47
|
import { cwd } from "./utils/cwd";
|
|
47
48
|
import {
|
|
48
49
|
GlobalRegistry,
|
|
@@ -64,7 +65,6 @@ export class AppStartup {
|
|
|
64
65
|
private static destroyPromise: Promise<void> | null = null;
|
|
65
66
|
private static hasBeenDestroyed = false;
|
|
66
67
|
private static rootModule: any;
|
|
67
|
-
private static readonly viewBundles = new Map<string, string>();
|
|
68
68
|
private static globalRateLimitConfig: RateLimitGlobalConfig | undefined;
|
|
69
69
|
private static rateLimitService: RateLimitService = new RateLimitService();
|
|
70
70
|
|
|
@@ -97,7 +97,7 @@ export class AppStartup {
|
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
if (options?.viewsDir) {
|
|
100
|
-
|
|
100
|
+
Bundler.buildViews(options.viewsDir).catch((err) => {
|
|
101
101
|
AppStartup.logger.error(
|
|
102
102
|
`Failed to auto-bundle views: ${err.message}`,
|
|
103
103
|
);
|
|
@@ -236,129 +236,6 @@ export class AppStartup {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
/**
|
|
240
|
-
* Bundles a client-side component for hydration (internal).
|
|
241
|
-
*/
|
|
242
|
-
private static async bundle(entryPath: string, outputName: string) {
|
|
243
|
-
try {
|
|
244
|
-
const result = await Bun.build({
|
|
245
|
-
entrypoints: [entryPath],
|
|
246
|
-
outdir: "./public",
|
|
247
|
-
naming: outputName,
|
|
248
|
-
minify: true,
|
|
249
|
-
external: [
|
|
250
|
-
"react",
|
|
251
|
-
"react-dom",
|
|
252
|
-
"react-dom/client",
|
|
253
|
-
"react/jsx-runtime",
|
|
254
|
-
"react/jsx-dev-runtime",
|
|
255
|
-
],
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
if (!result.success) {
|
|
259
|
-
AppStartup.logger.error(
|
|
260
|
-
`Bundle failed for ${outputName}: ${result.logs
|
|
261
|
-
.map((l) => l.message)
|
|
262
|
-
.join("\n")}`,
|
|
263
|
-
);
|
|
264
|
-
} else {
|
|
265
|
-
AppStartup.logger.log(
|
|
266
|
-
`Bundle created successfully: public/${outputName}`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
} catch (error: any) {
|
|
270
|
-
AppStartup.logger.error(
|
|
271
|
-
`Error during bundling ${outputName}: ${error.message}`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private static async autoBundle(viewsDir: string) {
|
|
277
|
-
const viewDirExists = await Bun.file(viewsDir).exists();
|
|
278
|
-
if (!viewDirExists) return;
|
|
279
|
-
|
|
280
|
-
const bunstoneDirExists = await Bun.file("./.bunstone").exists();
|
|
281
|
-
if (!bunstoneDirExists) {
|
|
282
|
-
await mkdir("./.bunstone", { recursive: true });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const getFilesRecursively = async (dir: string): Promise<string[]> => {
|
|
286
|
-
let results: string[] = [];
|
|
287
|
-
const list = await readdir(dir);
|
|
288
|
-
for (const file of list) {
|
|
289
|
-
const fullPath = join(dir, file);
|
|
290
|
-
const stat = statSync(fullPath);
|
|
291
|
-
if (stat?.isDirectory()) {
|
|
292
|
-
results = results.concat(await getFilesRecursively(fullPath));
|
|
293
|
-
} else {
|
|
294
|
-
results.push(resolve(fullPath));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return results;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const viewsDirAbs = resolve(viewsDir);
|
|
301
|
-
const files = await getFilesRecursively(viewsDirAbs);
|
|
302
|
-
AppStartup.logger.log(
|
|
303
|
-
`Auto-bundling views from ${viewsDirAbs} (${files.length} views found)`,
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
for (const absolutePath of files) {
|
|
307
|
-
const file = basename(absolutePath);
|
|
308
|
-
if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
309
|
-
const componentName = basename(file, extname(file));
|
|
310
|
-
const entryPath = join(
|
|
311
|
-
cwd(),
|
|
312
|
-
".bunstone",
|
|
313
|
-
`${componentName}.client.tsx`,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
const entryContent = `
|
|
317
|
-
import React from 'react';
|
|
318
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
319
|
-
import * as Mod from '${absolutePath}';
|
|
320
|
-
|
|
321
|
-
const Component = Mod['${componentName}'] || Mod.default;
|
|
322
|
-
|
|
323
|
-
function hydrate() {
|
|
324
|
-
const dataElement = document.getElementById("__BUNSTONE_DATA__");
|
|
325
|
-
const data = dataElement ? JSON.parse(dataElement.textContent || "{}") : {};
|
|
326
|
-
|
|
327
|
-
if (typeof document !== 'undefined' && Component) {
|
|
328
|
-
const root = document.getElementById("root");
|
|
329
|
-
if (root) {
|
|
330
|
-
try {
|
|
331
|
-
hydrateRoot(root, React.createElement(Component, data));
|
|
332
|
-
console.log('[Bunstone] Hydration successful for component: ${componentName}');
|
|
333
|
-
} catch (e) {
|
|
334
|
-
console.error('[Bunstone] Hydration failed for component: ${componentName}', e);
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
console.error('[Bunstone] Root element "root" not found for hydration.');
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
console.error('[Bunstone] Component ${componentName} not found in bundle.');
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Ensure DOM is fully loaded before hydrating
|
|
345
|
-
if (document.readyState === 'loading') {
|
|
346
|
-
document.addEventListener('DOMContentLoaded', hydrate);
|
|
347
|
-
} else {
|
|
348
|
-
hydrate();
|
|
349
|
-
}
|
|
350
|
-
`;
|
|
351
|
-
|
|
352
|
-
await Bun.write(entryPath, entryContent);
|
|
353
|
-
|
|
354
|
-
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
355
|
-
await AppStartup.bundle(entryPath, bundleName);
|
|
356
|
-
AppStartup.viewBundles.set(componentName, bundleName);
|
|
357
|
-
AppStartup.viewBundles.set(componentName.toLowerCase(), bundleName);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
239
|
/**
|
|
363
240
|
* Starts the server on the specified port.
|
|
364
241
|
* @param port The port number to listen on.
|
|
@@ -426,16 +303,10 @@ if (document.readyState === 'loading') {
|
|
|
426
303
|
}
|
|
427
304
|
|
|
428
305
|
const componentName = component.name || component.displayName;
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
AppStartup.viewBundles.get(componentName) ||
|
|
432
|
-
AppStartup.viewBundles.get(componentName.toLowerCase());
|
|
306
|
+
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
307
|
+
const bundle = `/public/${bundleName}`;
|
|
433
308
|
|
|
434
|
-
AppStartup.logger.log(
|
|
435
|
-
`Rendering component: ${componentName}, bundle found: ${
|
|
436
|
-
bundle || "none"
|
|
437
|
-
}`,
|
|
438
|
-
);
|
|
309
|
+
AppStartup.logger.log(`Rendering component: ${componentName}`);
|
|
439
310
|
|
|
440
311
|
if (!bundle) {
|
|
441
312
|
AppStartup.logger.warn(
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
4
|
+
import { cwd } from "./cwd";
|
|
5
|
+
import { Logger } from "./logger";
|
|
6
|
+
|
|
7
|
+
export class Bundler {
|
|
8
|
+
private static logger = new Logger("Bundler");
|
|
9
|
+
|
|
10
|
+
static async buildViews(viewsDir: string, outDir: string = "./public") {
|
|
11
|
+
const viewsDirAbs = resolve(viewsDir);
|
|
12
|
+
const viewsDirStat = statSync(viewsDirAbs, { throwIfNoEntry: false });
|
|
13
|
+
if (!viewsDirStat || !viewsDirStat.isDirectory()) return;
|
|
14
|
+
|
|
15
|
+
const bunstoneDir = join(cwd(), ".bunstone");
|
|
16
|
+
const bunstoneDirExists = await Bun.file(bunstoneDir).exists();
|
|
17
|
+
if (!bunstoneDirExists) {
|
|
18
|
+
await mkdir(bunstoneDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files = await Bundler.getFilesRecursively(viewsDirAbs);
|
|
22
|
+
Bundler.logger.log(
|
|
23
|
+
`Auto-bundling views from ${viewsDirAbs} (${files.length} views found)`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (const absolutePath of files) {
|
|
27
|
+
if (absolutePath.endsWith(".tsx") || absolutePath.endsWith(".jsx")) {
|
|
28
|
+
const componentName = basename(absolutePath, extname(absolutePath));
|
|
29
|
+
const entryPath = join(bunstoneDir, `${componentName}.client.tsx`);
|
|
30
|
+
const bundleName = `${componentName.toLowerCase()}.bundle.js`;
|
|
31
|
+
|
|
32
|
+
const entryContent = Bundler.generateHydrationEntry(
|
|
33
|
+
absolutePath,
|
|
34
|
+
componentName,
|
|
35
|
+
);
|
|
36
|
+
await Bun.write(entryPath, entryContent);
|
|
37
|
+
|
|
38
|
+
await Bundler.bundleView(entryPath, outDir, bundleName);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static async bundleView(
|
|
44
|
+
entryPath: string,
|
|
45
|
+
outdir: string,
|
|
46
|
+
outputName: string,
|
|
47
|
+
) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await Bun.build({
|
|
50
|
+
entrypoints: [entryPath],
|
|
51
|
+
outdir: outdir,
|
|
52
|
+
naming: outputName,
|
|
53
|
+
minify: true,
|
|
54
|
+
external: [
|
|
55
|
+
"react",
|
|
56
|
+
"react-dom",
|
|
57
|
+
"react-dom/client",
|
|
58
|
+
"react/jsx-runtime",
|
|
59
|
+
"react/jsx-dev-runtime",
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
Bundler.logger.error(
|
|
65
|
+
`Bundle failed for ${outputName}: ${result.logs
|
|
66
|
+
.map((l) => l.message)
|
|
67
|
+
.join("\n")}`,
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
Bundler.logger.log(
|
|
71
|
+
`Bundle created successfully: ${outdir}/${outputName}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
Bundler.logger.error(
|
|
76
|
+
`Error during bundling ${outputName}: ${error.message}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static async getFilesRecursively(dir: string): Promise<string[]> {
|
|
82
|
+
let results: string[] = [];
|
|
83
|
+
const list = await readdir(dir);
|
|
84
|
+
for (const file of list) {
|
|
85
|
+
const fullPath = join(dir, file);
|
|
86
|
+
const stat = statSync(fullPath);
|
|
87
|
+
if (stat?.isDirectory()) {
|
|
88
|
+
results = results.concat(await Bundler.getFilesRecursively(fullPath));
|
|
89
|
+
} else {
|
|
90
|
+
results.push(resolve(fullPath));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static generateHydrationEntry(path: string, name: string) {
|
|
97
|
+
return `
|
|
98
|
+
import React from 'react';
|
|
99
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
100
|
+
import * as Mod from '${path}';
|
|
101
|
+
|
|
102
|
+
const Component = Mod['${name}'] || Mod.default;
|
|
103
|
+
|
|
104
|
+
function hydrate() {
|
|
105
|
+
const dataElement = document.getElementById("__BUNSTONE_DATA__");
|
|
106
|
+
const data = dataElement ? JSON.parse(dataElement.textContent || "{}") : {};
|
|
107
|
+
|
|
108
|
+
if (typeof document !== 'undefined' && Component) {
|
|
109
|
+
const root = document.getElementById("root");
|
|
110
|
+
if (root) {
|
|
111
|
+
try {
|
|
112
|
+
hydrateRoot(root, React.createElement(Component, data));
|
|
113
|
+
console.log('[Bunstone] Hydration successful for component: ${name}');
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('[Bunstone] Hydration failed for component: ${name}', e);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
console.error('[Bunstone] Root element "root" not found for hydration.');
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.error('[Bunstone] Component ${name} not found in bundle.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (document.readyState === 'loading') {
|
|
126
|
+
document.addEventListener('DOMContentLoaded', hydrate);
|
|
127
|
+
} else {
|
|
128
|
+
hydrate();
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static async buildApp(
|
|
134
|
+
entrypoint: string,
|
|
135
|
+
outdir: string = "./dist",
|
|
136
|
+
compile: boolean = false,
|
|
137
|
+
) {
|
|
138
|
+
const entryAbs = resolve(entrypoint);
|
|
139
|
+
if (!(await Bun.file(entryAbs).exists())) {
|
|
140
|
+
throw new Error(`Entrypoint not found: ${entrypoint}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Bundler.logger.log(`Building application from ${entrypoint}...`);
|
|
144
|
+
|
|
145
|
+
if (compile) {
|
|
146
|
+
const result = await Bun.spawn(
|
|
147
|
+
[
|
|
148
|
+
"bun",
|
|
149
|
+
"build",
|
|
150
|
+
entrypoint,
|
|
151
|
+
"--compile",
|
|
152
|
+
"--outfile",
|
|
153
|
+
join(outdir, "app"),
|
|
154
|
+
],
|
|
155
|
+
{
|
|
156
|
+
stdout: "inherit",
|
|
157
|
+
stderr: "inherit",
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
const exitCode = await result.exited;
|
|
161
|
+
if (exitCode !== 0) {
|
|
162
|
+
throw new Error("Compilation failed");
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const result = await Bun.build({
|
|
166
|
+
entrypoints: [entrypoint],
|
|
167
|
+
outdir: outdir,
|
|
168
|
+
target: "bun",
|
|
169
|
+
minify: true,
|
|
170
|
+
sourcemap: "external",
|
|
171
|
+
external: ["react", "react-dom", "elysia", "@elysiajs/*"],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
Bundler.logger.error(
|
|
176
|
+
"Build failed:",
|
|
177
|
+
result.logs.map((l) => l.message).join("\n"),
|
|
178
|
+
);
|
|
179
|
+
throw new Error("Application build failed");
|
|
180
|
+
}
|
|
181
|
+
Bundler.logger.log(`Application built successfully to ${outdir}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
package/package.json
CHANGED
package/starter/package.json
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
"main": "src/main.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"start": "bun run
|
|
8
|
-
"dev": "
|
|
7
|
+
"start": "bun run dist/index.js",
|
|
8
|
+
"dev": "bunstone run --watch src/main.ts",
|
|
9
|
+
"build": "bunstone build src/main.ts",
|
|
9
10
|
"test": "bun test",
|
|
10
11
|
"lint": "biome check .",
|
|
11
12
|
"lint:fix": "biome check --write .",
|