@pablozaiden/webapp 0.5.2 → 0.5.4

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.
@@ -17,6 +17,26 @@ await buildWebAppBinary({
17
17
  });
18
18
  ```
19
19
 
20
+ The binary builder compiles the browser bundle with the framework defaults needed for `webapp` apps, including Tailwind CSS v4 processing. If an app needs extra browser build behavior, add Bun plugins or browser-only defines under `web.build`:
21
+
22
+ ```ts
23
+ await buildWebAppBinary({
24
+ entrypoint: "src/index.ts",
25
+ outfile: "./dist/my-app",
26
+ web: {
27
+ entry: "./frontend.tsx",
28
+ build: {
29
+ plugins: [myBrowserPlugin],
30
+ define: {
31
+ "process.env.MY_BROWSER_FLAG": JSON.stringify("enabled"),
32
+ },
33
+ },
34
+ },
35
+ });
36
+ ```
37
+
38
+ App-provided plugins run before the framework defaults. Use `web.build.disableDefaultPlugins` only for specialized builds that intentionally replace the framework browser pipeline.
39
+
20
40
  Run the binary with the same CLI contract:
21
41
 
22
42
  ```bash
@@ -415,6 +415,8 @@ await buildWebAppBinary({
415
415
  });
416
416
  ```
417
417
 
418
+ `buildWebAppBinary` includes the framework browser build defaults, including Tailwind CSS v4 processing. Apps that need additional browser-only transforms can pass Bun plugins or defines through `web.build.plugins` and `web.build.define`; app plugins run before the framework defaults.
419
+
418
420
  ## Adaptation checklist
419
421
 
420
422
  1. Replace `my-app` everywhere with the binary name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/webapp",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Opinionated Bun + React webapp framework for single-server apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "dependencies": {
44
44
  "@simplewebauthn/browser": "13.3.0",
45
45
  "@simplewebauthn/server": "13.3.1",
46
+ "bun-plugin-tailwind": "0.1.2",
46
47
  "jose": "6.2.3",
47
48
  "tailwindcss": "4.3.1",
48
49
  "zod": "4.4.3"
@@ -1,3 +1,5 @@
1
+ import type { BunPlugin } from "bun";
2
+ import tailwindPlugin from "bun-plugin-tailwind";
1
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
4
  import { basename, dirname, extname, resolve } from "node:path";
3
5
 
@@ -15,6 +17,11 @@ export interface BuildWebAppBinaryOptions {
15
17
  define?: Record<string, string>;
16
18
  web?: {
17
19
  entry?: string;
20
+ build?: {
21
+ plugins?: BunPlugin[];
22
+ define?: Record<string, string>;
23
+ disableDefaultPlugins?: boolean;
24
+ };
18
25
  };
19
26
  }
20
27
 
@@ -31,21 +38,31 @@ export async function buildWebAppBinary(options: BuildWebAppBinaryOptions): Prom
31
38
  try {
32
39
  mkdirSync(cacheDir, { recursive: true });
33
40
  const webEntry = resolve(dirname(resolve(options.entrypoint)), options.web?.entry ?? "./web/main.tsx");
34
- const browserEntry = resolve(cacheDir, "webapp-browser-entry.ts");
41
+ const rendererEntry = resolve(cacheDir, "webapp-renderer-prelude.ts");
42
+ const clientEntry = resolve(cacheDir, "webapp-client-entry.ts");
35
43
  const browserOutDir = resolve(cacheDir, "browser");
36
- writeFileSync(browserEntry, `import { createRoot } from "react-dom/client";
44
+ writeFileSync(rendererEntry, `import { createRoot } from "react-dom/client";
37
45
  import { configureWebAppRenderer } from "@pablozaiden/webapp/web";
38
46
 
39
47
  configureWebAppRenderer(createRoot);
40
- import ${JSON.stringify(webEntry)};
41
48
  `);
49
+ writeFileSync(clientEntry, `import ${JSON.stringify(webEntry)};
50
+ `);
51
+ const browserPlugins = [
52
+ ...(options.web?.build?.plugins ?? []),
53
+ ...(options.web?.build?.disableDefaultPlugins ? [] : [tailwindPlugin]),
54
+ ];
42
55
  const browserBuild = await Bun.build({
43
- entrypoints: [browserEntry],
56
+ entrypoints: [rendererEntry, clientEntry],
44
57
  outdir: browserOutDir,
45
58
  target: "browser",
59
+ format: "esm",
60
+ splitting: true,
61
+ publicPath: "/webapp-compiled/",
46
62
  minify: true,
47
63
  sourcemap: "external",
48
- define: options.define,
64
+ define: { ...options.define, ...options.web?.build?.define },
65
+ plugins: browserPlugins,
49
66
  });
50
67
  if (!browserBuild.success) {
51
68
  for (const log of browserBuild.logs) {
@@ -54,13 +71,17 @@ import ${JSON.stringify(webEntry)};
54
71
  throw new Error("Browser build failed");
55
72
  }
56
73
  const assets = browserBuild.outputs
74
+ .filter((output) => extname(output.path).toLowerCase() !== ".map")
57
75
  .map((output) => {
58
76
  const ext = extname(output.path).toLowerCase();
77
+ const fileName = basename(output.path);
59
78
  const publicPath = `/webapp-compiled/${basename(output.path)}`;
79
+ const scriptKind = ext === ".js" ? compiledScriptKind(fileName) : undefined;
60
80
  return {
61
81
  path: publicPath,
62
82
  contentType: contentTypeForOutput(ext),
63
- role: ext === ".css" ? "style" : ext === ".js" ? "script" : "asset",
83
+ role: ext === ".css" ? "style" : scriptKind ? "script" : "asset",
84
+ ...(scriptKind ? { scriptOrder: scriptKind === "renderer" ? 0 : 1 } : {}),
64
85
  body: readFileSync(output.path).toString("base64"),
65
86
  };
66
87
  });
@@ -115,3 +136,9 @@ function contentTypeForOutput(ext: string): string {
115
136
  if (ext === ".map") return "application/json; charset=utf-8";
116
137
  return "application/octet-stream";
117
138
  }
139
+
140
+ function compiledScriptKind(fileName: string): "renderer" | "client" | undefined {
141
+ if (/^webapp-renderer-prelude(?:[-.][\w-]+)?\.(?:mjs|js)$/.test(fileName)) return "renderer";
142
+ if (/^webapp-client-entry(?:[-.][\w-]+)?\.(?:mjs|js)$/.test(fileName)) return "client";
143
+ return undefined;
144
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Server, ServerWebSocket, WebSocketHandler } from "bun";
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -147,6 +147,7 @@ type CompiledClientAsset = {
147
147
  path: string;
148
148
  contentType: string;
149
149
  role: "script" | "style" | "asset";
150
+ scriptOrder?: number;
150
151
  body: string;
151
152
  };
152
153
 
@@ -180,7 +181,7 @@ function safeCachePathSegment(value: string): string {
180
181
  function createDocumentCacheDir(envPrefix: string): string {
181
182
  const root = join(tmpdir(), "webapp", safeCachePathSegment(envPrefix));
182
183
  mkdirSync(root, { recursive: true });
183
- const cacheDir = mkdtempSync(join(root, WEBAPP_DOCUMENT_CACHE_PREFIX));
184
+ const cacheDir = realpathSync(mkdtempSync(join(root, WEBAPP_DOCUMENT_CACHE_PREFIX)));
184
185
  documentCacheDirs.add(cacheDir);
185
186
  if (!documentCacheCleanupRegistered) {
186
187
  documentCacheCleanupRegistered = true;
@@ -481,7 +482,11 @@ function generatedHtml(
481
482
  const htmlAppleTouchPath = appleTouchPath.replace(/^\//, "./");
482
483
  const styleTags = compiledAssets?.filter((asset) => asset.role === "style").map((asset) => ` <link rel="stylesheet" href="${escapeAttribute(asset.path)}" />`).join("\n") ?? "";
483
484
  const scriptTags = compiledAssets
484
- ? compiledAssets.filter((asset) => asset.role === "script").map((asset) => ` <script type="module" src="${escapeAttribute(asset.path)}"></script>`).join("\n")
485
+ ? compiledAssets
486
+ .filter((asset) => asset.role === "script")
487
+ .sort((left, right) => (left.scriptOrder ?? 0) - (right.scriptOrder ?? 0))
488
+ .map((asset) => ` <script type="module" src="${escapeAttribute(asset.path)}"></script>`)
489
+ .join("\n")
485
490
  : ` <script type="module" src="${escapeAttribute(relativePrelude ?? "")}"></script>
486
491
  <script type="module" src="${escapeAttribute(relativeEntry ?? "")}"></script>`;
487
492
  const manifestTags = pwaEnabled(web)