@grupodiariodaregiao/bunstone 0.5.1 → 0.5.2

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 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> Scaffold a new project
517
- bunstone run [bun-flags] <entry> Run your app with enhanced error messages
518
- bunstone exports List all public exports
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 run --watch src/main.ts
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 { statSync as statSync2 } from "fs";
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 mkdir("./public", { recursive: true });
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
- AppStartup.autoBundle(options.viewsDir).catch((err) => {
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 bundle = result?.bundle || AppStartup.viewBundles.get(componentName) || AppStartup.viewBundles.get(componentName.toLowerCase());
117810
- AppStartup.logger.log(`Rendering component: ${componentName}, bundle found: ${bundle || "none"}`);
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
+ }
@@ -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
- AppStartup.autoBundle(options.viewsDir).catch((err) => {
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 bundle =
430
- result?.bundle ||
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
@@ -13,7 +13,7 @@
13
13
  "types": "./dist/*.d.ts"
14
14
  }
15
15
  },
16
- "version": "0.5.1",
16
+ "version": "0.5.2",
17
17
  "homepage": "https://bunstone.diario.one/",
18
18
  "repository": {
19
19
  "url": "https://github.com/diariodaregiao/bunstone.git",
@@ -4,8 +4,9 @@
4
4
  "main": "src/main.ts",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "start": "bun run src/main.ts",
8
- "dev": "bun --watch src/main.ts",
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 .",