Skip to content

Commit 4f6cf9b

Browse files
sayefdeenclaude
andcommitted
fix: embed UI assets into JS bundle at build time
Replaces runtime fs.readFileSync with build-time embedding via scripts/embed-ui.js. The UI CSS and JS are now string constants in the bundle, making it work in serverless environments (Vercel, Lambda, Cloudflare Workers) where filesystem access is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3a9e74d commit 4f6cf9b

5 files changed

Lines changed: 41 additions & 62 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ coverage
1111
*.log
1212
.next
1313
.trpc-studio.json
14+
packages/server/src/ui-assets.ts
1415
plan.md

examples/nextjs/next.config.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {
3-
experimental: {
4-
serverComponentsExternalPackages: ["@srawad/trpc-studio"],
5-
},
6-
};
2+
const nextConfig = {};
73

84
module.exports = nextConfig;

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"LICENSE"
2929
],
3030
"scripts": {
31-
"build": "tsup",
31+
"build": "node scripts/embed-ui.js && tsup",
3232
"dev": "tsup --watch",
3333
"lint": "eslint src --ext .ts",
3434
"lint:fix": "eslint src --ext .ts --fix",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// This script runs after the UI build and before the server build.
2+
// It reads the built UI assets and writes them as a TypeScript module
3+
// so they get bundled directly into the server JS — no fs.readFileSync needed.
4+
5+
import fs from "node:fs";
6+
import path from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10+
const uiDir = path.join(__dirname, "..", "dist", "ui", "assets");
11+
const outFile = path.join(__dirname, "..", "src", "ui-assets.ts");
12+
13+
let jsContent = "";
14+
let cssContent = "";
15+
16+
try {
17+
jsContent = fs.readFileSync(path.join(uiDir, "index.js"), "utf-8");
18+
cssContent = fs.readFileSync(path.join(uiDir, "index.css"), "utf-8");
19+
} catch {
20+
console.error("Warning: UI assets not found at", uiDir);
21+
console.error("Make sure @trpc-studio/ui is built first.");
22+
}
23+
24+
// Escape backticks and ${} in the content for template literals
25+
const escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
26+
27+
const output = `// AUTO-GENERATED — do not edit. Run "node scripts/embed-ui.js" to regenerate.
28+
export const UI_JS = \`${escapeTemplate(jsContent)}\`;
29+
export const UI_CSS = \`${escapeTemplate(cssContent)}\`;
30+
`;
31+
32+
fs.writeFileSync(outFile, output, "utf-8");
33+
console.log(`Embedded UI assets into ${outFile} (JS: ${jsContent.length} bytes, CSS: ${cssContent.length} bytes)`);

packages/server/src/render.ts

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import fs from "node:fs";
2-
import path from "node:path";
3-
import { fileURLToPath } from "node:url";
4-
51
import { introspectRouter } from "./introspect";
2+
import { UI_CSS, UI_JS } from "./ui-assets";
63

74
import type { JsonSchema } from "@trpc-studio/core";
85

@@ -30,51 +27,6 @@ function safeInlineJson(json: string): string {
3027
return json.replace(/<\//g, "<\\/");
3128
}
3229

33-
// Load UI assets — try multiple resolution strategies
34-
let cachedJs: string | null = null;
35-
let cachedCss: string | null = null;
36-
let assetsLoaded = false;
37-
38-
function loadAssets(): void {
39-
if (assetsLoaded) return;
40-
assetsLoaded = true;
41-
42-
const tryPaths: string[] = [];
43-
44-
// Strategy 1: __dirname (CJS, standard Node.js)
45-
if (typeof __dirname !== "undefined") {
46-
tryPaths.push(path.join(__dirname, "ui", "assets"));
47-
}
48-
49-
// Strategy 2: import.meta.url (ESM)
50-
try {
51-
const esmDir = path.dirname(fileURLToPath(import.meta.url));
52-
tryPaths.push(path.join(esmDir, "ui", "assets"));
53-
} catch {
54-
// import.meta.url not available
55-
}
56-
57-
// Strategy 3: resolve from the package itself
58-
try {
59-
const pkgDir = path.dirname(require.resolve("@srawad/trpc-studio/package.json"));
60-
tryPaths.push(path.join(pkgDir, "dist", "ui", "assets"));
61-
} catch {
62-
// package not resolvable this way
63-
}
64-
65-
for (const dir of tryPaths) {
66-
try {
67-
const js = fs.readFileSync(path.join(dir, "index.js"), "utf-8");
68-
const css = fs.readFileSync(path.join(dir, "index.css"), "utf-8");
69-
cachedJs = js;
70-
cachedCss = css;
71-
return;
72-
} catch {
73-
// try next path
74-
}
75-
}
76-
}
77-
7830
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7931
export function renderTrpcStudio(router: any, options: RenderOptions): string {
8032
const manifest = introspectRouter(router);
@@ -108,27 +60,24 @@ export function renderTrpcStudio(router: any, options: RenderOptions): string {
10860
const safeOptions = safeInlineJson(optionsJson);
10961
const scriptTag = `<script>window.__TRPC_STUDIO_MANIFEST__=${safeManifest};window.__TRPC_STUDIO_OPTIONS__=${safeOptions};</script>`;
11062

111-
// Load and cache UI assets
112-
loadAssets();
113-
114-
if (cachedJs) {
63+
if (UI_JS) {
11564
return `<!DOCTYPE html>
11665
<html lang="en">
11766
<head>
11867
<meta charset="UTF-8" />
11968
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
12069
<title>${title}</title>
121-
${cachedCss ? `<style>${cachedCss}</style>` : ""}
70+
${UI_CSS ? `<style>${UI_CSS}</style>` : ""}
12271
${scriptTag}
12372
</head>
12473
<body>
12574
<div id="root"></div>
126-
<script type="module">${cachedJs}</script>
75+
<script type="module">${UI_JS}</script>
12776
</body>
12877
</html>`;
12978
}
13079

131-
// Fallback if UI assets not found
80+
// Fallback if UI assets were not embedded
13281
return `<!DOCTYPE html>
13382
<html lang="en">
13483
<head>

0 commit comments

Comments
 (0)