@pablozaiden/webapp 0.5.3 → 0.5.5

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/docs/server.md CHANGED
@@ -129,7 +129,7 @@ For a functional PWA:
129
129
  | --- | --- | --- |
130
130
  | `web.entry` | `./web/main.tsx` | The app frontend entrypoint lives somewhere else, such as `./frontend.tsx` |
131
131
  | `web.shortName` | `appName` | The installed app label should be shorter than the full name |
132
- | `web.themeColor` | `#111827` | Browser chrome/install metadata should match product branding |
132
+ | `web.themeColor` | Not emitted unless set | Browser chrome/install metadata should match product branding; the generated default icon still uses `#111827` when unset |
133
133
  | `web.backgroundColor` | `#ffffff` | The manifest background should match the app splash/background |
134
134
  | `web.icons.favicon` | Generated initials SVG | Browser tabs should use product artwork instead of initials |
135
135
  | `web.icons.appleTouch` | `favicon` or generated initials SVG | iOS home-screen/Dock should use product artwork |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/webapp",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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
 
@@ -41,6 +48,10 @@ configureWebAppRenderer(createRoot);
41
48
  `);
42
49
  writeFileSync(clientEntry, `import ${JSON.stringify(webEntry)};
43
50
  `);
51
+ const browserPlugins = [
52
+ ...(options.web?.build?.plugins ?? []),
53
+ ...(options.web?.build?.disableDefaultPlugins ? [] : [tailwindPlugin]),
54
+ ];
44
55
  const browserBuild = await Bun.build({
45
56
  entrypoints: [rendererEntry, clientEntry],
46
57
  outdir: browserOutDir,
@@ -50,7 +61,8 @@ configureWebAppRenderer(createRoot);
50
61
  publicPath: "/webapp-compiled/",
51
62
  minify: true,
52
63
  sourcemap: "external",
53
- define: options.define,
64
+ define: { ...options.define, ...options.web?.build?.define },
65
+ plugins: browserPlugins,
54
66
  });
55
67
  if (!browserBuild.success) {
56
68
  for (const log of browserBuild.logs) {
@@ -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";
@@ -181,7 +181,7 @@ function safeCachePathSegment(value: string): string {
181
181
  function createDocumentCacheDir(envPrefix: string): string {
182
182
  const root = join(tmpdir(), "webapp", safeCachePathSegment(envPrefix));
183
183
  mkdirSync(root, { recursive: true });
184
- const cacheDir = mkdtempSync(join(root, WEBAPP_DOCUMENT_CACHE_PREFIX));
184
+ const cacheDir = realpathSync(mkdtempSync(join(root, WEBAPP_DOCUMENT_CACHE_PREFIX)));
185
185
  documentCacheDirs.add(cacheDir);
186
186
  if (!documentCacheCleanupRegistered) {
187
187
  documentCacheCleanupRegistered = true;
@@ -413,13 +413,15 @@ function contentTypeForIcon(path: string, explicit?: string): string {
413
413
  return "application/octet-stream";
414
414
  }
415
415
 
416
- function themeBootScript(themeColor: string): string {
417
- const darkThemeColor = JSON.stringify(themeColor);
418
- const lightThemeColor = JSON.stringify(DEFAULT_BACKGROUND_COLOR);
416
+ function themeBootScript(themeColor: string | undefined): string {
417
+ const themeColorUpdate = themeColor
418
+ ? `
419
+ const metaThemeColor = document.querySelector('meta[name="theme-color"]');
420
+ if (metaThemeColor instanceof HTMLMetaElement) metaThemeColor.content = resolved === "dark" ? ${JSON.stringify(themeColor)} : ${JSON.stringify(DEFAULT_BACKGROUND_COLOR)};`
421
+ : "";
419
422
  return `(() => {
420
423
  const key = "webapp.theme";
421
424
  const root = document.documentElement;
422
- const metaThemeColor = document.querySelector('meta[name="theme-color"]');
423
425
  const stored = window.localStorage.getItem(key);
424
426
  const preference = stored === "light" || stored === "dark" || stored === "system" ? stored : "system";
425
427
  const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
@@ -428,7 +430,7 @@ function themeBootScript(themeColor: string): string {
428
430
  root.style.colorScheme = resolved;
429
431
  root.dataset.theme = preference;
430
432
  root.dataset.resolvedTheme = resolved;
431
- if (metaThemeColor instanceof HTMLMetaElement) metaThemeColor.content = resolved === "dark" ? ${darkThemeColor} : ${lightThemeColor};
433
+ ${themeColorUpdate}
432
434
  })();`;
433
435
  }
434
436
 
@@ -440,7 +442,7 @@ function pwaConfig(web: WebAppDocumentConfig): WebAppPwaConfig {
440
442
  return typeof web.pwa === "object" ? web.pwa : {};
441
443
  }
442
444
 
443
- function generatedManifest(config: RuntimeConfig, web: WebAppDocumentConfig, themeColor: string, backgroundColor: string, icons: WebAppIconConfig[]): string {
445
+ function generatedManifest(config: RuntimeConfig, web: WebAppDocumentConfig, backgroundColor: string, icons: WebAppIconConfig[]): string {
444
446
  const pwa = pwaConfig(web);
445
447
  return JSON.stringify({
446
448
  name: config.appName,
@@ -449,7 +451,7 @@ function generatedManifest(config: RuntimeConfig, web: WebAppDocumentConfig, the
449
451
  scope: pwa.scope ?? "./",
450
452
  display: pwa.display ?? "standalone",
451
453
  background_color: backgroundColor,
452
- theme_color: themeColor,
454
+ ...(web.themeColor ? { theme_color: web.themeColor } : {}),
453
455
  icons,
454
456
  }, null, 2);
455
457
  }
@@ -471,7 +473,7 @@ function generatedHtml(
471
473
  web: WebAppDocumentConfig,
472
474
  relativeEntry: string | undefined,
473
475
  relativePrelude: string | undefined,
474
- themeColor: string,
476
+ themeColor: string | undefined,
475
477
  faviconPath: string,
476
478
  appleTouchPath: string,
477
479
  compiledAssets?: CompiledClientAsset[],
@@ -480,6 +482,7 @@ function generatedHtml(
480
482
  const shortName = escapeAttribute(web.shortName ?? config.appName);
481
483
  const htmlFaviconPath = faviconPath.replace(/^\//, "./");
482
484
  const htmlAppleTouchPath = appleTouchPath.replace(/^\//, "./");
485
+ const themeMetaTag = themeColor ? ` <meta name="theme-color" content="${escapeAttribute(themeColor)}" />\n` : "";
483
486
  const styleTags = compiledAssets?.filter((asset) => asset.role === "style").map((asset) => ` <link rel="stylesheet" href="${escapeAttribute(asset.path)}" />`).join("\n") ?? "";
484
487
  const scriptTags = compiledAssets
485
488
  ? compiledAssets
@@ -508,7 +511,7 @@ function generatedHtml(
508
511
  <head>
509
512
  <meta charset="utf-8" />
510
513
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
511
- <meta name="theme-color" content="${escapeAttribute(themeColor)}" />
514
+ ${themeMetaTag}\
512
515
  ${manifestTags} <title>${title}</title>
513
516
  <script>${themeBootScript(themeColor)}</script>
514
517
  ${styleTags}
@@ -525,13 +528,13 @@ async function createWebDocument(config: RuntimeConfig, webInput: WebAppDocument
525
528
  const web = webInput ?? {};
526
529
  const compiled = compiledClient();
527
530
  const entryFile = compiled ? undefined : resolveWebEntry(web.entry);
528
- const themeColor = web.themeColor ?? DEFAULT_THEME_COLOR;
531
+ const iconThemeColor = web.themeColor ?? DEFAULT_THEME_COLOR;
529
532
  const backgroundColor = web.backgroundColor ?? DEFAULT_BACKGROUND_COLOR;
530
533
  const packageRoot = compiled?.packageRoot ?? findPackageRoot(dirname(resolve(Bun.main || process.argv[1] || (entryFile ?? "."))));
531
534
  const publicEntry = entryFile ? webEntryPublicPath(entryFile, packageRoot) : "";
532
535
  const cacheDir = createDocumentCacheDir(config.envPrefix);
533
536
  const htmlPath = resolve(cacheDir, `${config.envPrefix.toLowerCase()}-index.html`);
534
- const icon = generatedIcon(config.appName, themeColor, backgroundColor);
537
+ const icon = generatedIcon(config.appName, iconThemeColor, backgroundColor);
535
538
  const favicon = iconConfig(web.icons?.favicon);
536
539
  const appleTouch = iconConfig(web.icons?.appleTouch) ?? favicon;
537
540
  const manifestIconConfigs = web.icons?.manifest?.length ? web.icons.manifest : undefined;
@@ -547,7 +550,7 @@ async function createWebDocument(config: RuntimeConfig, webInput: WebAppDocument
547
550
  };
548
551
  })
549
552
  : [{ src: "./webapp-icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any maskable" }];
550
- const manifest = pwaEnabled(web) ? generatedManifest(config, web, themeColor, backgroundColor, manifestIcons) : "";
553
+ const manifest = pwaEnabled(web) ? generatedManifest(config, web, backgroundColor, manifestIcons) : "";
551
554
  writeFileSync(resolve(cacheDir, "webapp-icon.svg"), icon);
552
555
  if (manifest) {
553
556
  writeFileSync(resolve(cacheDir, "site.webmanifest"), manifest);
@@ -578,7 +581,7 @@ configureWebAppRenderer(createRoot);
578
581
  }
579
582
  }
580
583
  const compiledAssets = compiled?.assets;
581
- writeFileSync(htmlPath, generatedHtml(config, web, relativeEntry, relativePrelude, themeColor, faviconPath, appleTouchPath, compiledAssets));
584
+ writeFileSync(htmlPath, generatedHtml(config, web, relativeEntry, relativePrelude, web.themeColor, faviconPath, appleTouchPath, compiledAssets));
582
585
  const bundle = compiledAssets ? undefined : (await import(`${pathToFileURL(htmlPath).href}?v=${Date.now()}-${Math.random()}`)).default;
583
586
  if (!compiledAssets && !isHtmlBundleIndex(bundle)) {
584
587
  throw new Error("Generated web document did not produce a Bun HTMLBundle");