@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.
- package/docs/deployment.md +20 -0
- package/docs/github-actions.md +2 -0
- package/docs/server.md +1 -1
- package/package.json +2 -1
- package/src/build/build-binary.ts +13 -1
- package/src/server/create-web-app-server.ts +18 -15
package/docs/deployment.md
CHANGED
|
@@ -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
|
package/docs/github-actions.md
CHANGED
|
@@ -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` |
|
|
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
|
+
"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
|
|
418
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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");
|