From 02d36bd090336c382eabc6a19b4f2431c661962b Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 6 Mar 2026 11:04:55 -0500 Subject: [PATCH 1/6] feat: implement CSS import resolution for workspace UI in post-build process --- packages/stack/scripts/postbuild.cjs | 117 +++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/packages/stack/scripts/postbuild.cjs b/packages/stack/scripts/postbuild.cjs index 636961ce..769af3cb 100644 --- a/packages/stack/scripts/postbuild.cjs +++ b/packages/stack/scripts/postbuild.cjs @@ -2,6 +2,9 @@ /* Post-build step for BTST package. - Copies all .css files from src/plugins/** to dist/plugins/** preserving structure + - Resolves @workspace/ui/... CSS imports in dist files by inlining the referenced + content into dist/plugins/shared/ and rewriting the import path — so consumers + outside the monorepo (e.g. npm installs, StackBlitz) never see workspace imports - Executes optional per-plugin postbuild scripts if present at: src/plugins//postbuild.(js|cjs|mjs) */ @@ -54,6 +57,119 @@ function copyAllPluginCss() { } } +/** + * Recursively inline relative @import statements in a CSS file, producing a + * single blob of CSS with no relative imports left. Non-relative imports + * (e.g. "tailwindcss", bare package names) are kept as-is. + */ +function inlineCssImports(cssContent, baseDir, seen = new Set()) { + return cssContent.replace( + /^@import\s+"([^"]+)";?[ \t]*$/gm, + (match, importPath) => { + if (!importPath.startsWith("./") && !importPath.startsWith("../")) { + return match; + } + const resolved = path.resolve(baseDir, importPath); + if (seen.has(resolved)) return ""; + if (!fs.existsSync(resolved)) return match; + seen.add(resolved); + const content = fs.readFileSync(resolved, "utf8"); + return inlineCssImports(content, path.dirname(resolved), seen); + }, + ); +} + +/** + * After all plugin CSS files are copied to dist, scan them for + * @import "@workspace/ui/..." declarations. For each unique specifier found: + * 1. Resolve the actual file via packages/ui/package.json exports map + * 2. Inline all relative sub-imports recursively into one blob + * 3. Write the blob to dist/plugins/shared/.css + * 4. Rewrite the @workspace/ui/... import in the dist CSS to the relative path + * + * This keeps source files using proper workspace imports while ensuring the + * published package is self-contained. + */ +function resolveWorkspaceCssImports() { + const UI_PKG_DIR = path.resolve(ROOT, "..", "ui"); + const UI_PKG_JSON = path.join(UI_PKG_DIR, "package.json"); + if (!fs.existsSync(UI_PKG_JSON)) { + console.warn( + "@btst/stack: packages/ui not found — skipping workspace CSS resolution", + ); + return; + } + + const uiExports = + JSON.parse(fs.readFileSync(UI_PKG_JSON, "utf8")).exports || {}; + + function resolveUiSpecifier(specifier) { + // "@workspace/ui/components/foo/bar.css" → "./components/foo/bar.css" + const subpath = specifier.replace("@workspace/ui", "."); + const exportEntry = uiExports[subpath]; + if (!exportEntry) return null; + return path.resolve(UI_PKG_DIR, exportEntry); + } + + const WORKSPACE_IMPORT_RE = /@import\s+"(@workspace\/ui[^"]+)";?[ \t]*/g; + const sharedDir = path.join(DIST_PLUGINS_DIR, "shared"); + + // specifier → filename written in dist/plugins/shared/ + const generated = new Map(); + + if (!fs.existsSync(DIST_PLUGINS_DIR)) return; + + for (const filePath of walk(DIST_PLUGINS_DIR)) { + if (!filePath.endsWith(".css")) continue; + + let content = fs.readFileSync(filePath, "utf8"); + if (!WORKSPACE_IMPORT_RE.test(content)) continue; + WORKSPACE_IMPORT_RE.lastIndex = 0; + + let modified = false; + + content = content.replace(WORKSPACE_IMPORT_RE, (match, specifier) => { + if (!generated.has(specifier)) { + const resolvedPath = resolveUiSpecifier(specifier); + if (!resolvedPath || !fs.existsSync(resolvedPath)) { + console.warn( + `@btst/stack: could not resolve workspace import: ${specifier}`, + ); + return match; + } + const raw = fs.readFileSync(resolvedPath, "utf8"); + const inlined = inlineCssImports(raw, path.dirname(resolvedPath)); + // Derive a stable filename from the specifier + const slug = specifier + .replace("@workspace/ui/", "") + .replace(/\//g, "-"); + ensureDir(sharedDir); + const destPath = path.join(sharedDir, slug); + fs.writeFileSync(destPath, inlined); + generated.set(specifier, slug); + console.log( + `@btst/stack: resolved workspace import "${specifier}" → shared/${slug}`, + ); + } + const slug = generated.get(specifier); + const rel = path.relative( + path.dirname(filePath), + path.join(sharedDir, slug), + ); + const relNormalized = rel.startsWith(".") ? rel : `./${rel}`; + modified = true; + return `@import "${relNormalized}";`; + }); + + if (modified) { + fs.writeFileSync(filePath, content); + console.log( + `@btst/stack: rewrote workspace imports in ${path.relative(DIST_PLUGINS_DIR, filePath)}`, + ); + } + } +} + function runPerPluginPostbuilds() { if (!fs.existsSync(SRC_PLUGINS_DIR)) return; const candidates = ["postbuild.js", "postbuild.cjs", "postbuild.mjs"]; @@ -93,6 +209,7 @@ function runPerPluginPostbuilds() { function main() { ensureDir(DIST_PLUGINS_DIR); copyAllPluginCss(); + resolveWorkspaceCssImports(); runPerPluginPostbuilds(); } From 13a366c4791b428c29b9213e66ffbbfd2fc55a6d Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 6 Mar 2026 11:15:59 -0500 Subject: [PATCH 2/6] chore: adjust CSS imports for demo projects --- demos/ai-chat/app/globals.css | 4 +- demos/ai-chat/package.json | 2 +- demos/blog/app/globals.css | 4 +- demos/blog/package.json | 2 +- demos/cms/app/globals.css | 4 +- demos/cms/package.json | 2 +- demos/form-builder/app/globals.css | 4 +- demos/form-builder/package.json | 2 +- demos/kanban/app/globals.css | 4 +- demos/kanban/package.json | 2 +- demos/ui-builder/app/globals.css | 6 +-- demos/ui-builder/package.json | 2 +- docs/content/docs/demos/ai-chat.mdx | 9 ++++- docs/content/docs/demos/blog.mdx | 9 ++++- docs/content/docs/demos/cms.mdx | 9 ++++- docs/content/docs/demos/form-builder.mdx | 9 ++++- docs/content/docs/demos/kanban.mdx | 9 ++++- docs/content/docs/demos/ui-builder.mdx | 9 ++++- packages/stack/package.json | 9 ++++- .../stack/src/plugins/ai-chat/style-wc.css | 37 +++++++++++++++++++ packages/stack/src/plugins/blog/style-wc.css | 37 +++++++++++++++++++ packages/stack/src/plugins/cms/style-wc.css | 37 +++++++++++++++++++ .../src/plugins/form-builder/style-wc.css | 37 +++++++++++++++++++ .../stack/src/plugins/kanban/style-wc.css | 37 +++++++++++++++++++ .../stack/src/plugins/route-docs/style-wc.css | 37 +++++++++++++++++++ .../stack/src/plugins/ui-builder/style-wc.css | 37 +++++++++++++++++++ 26 files changed, 334 insertions(+), 26 deletions(-) create mode 100644 packages/stack/src/plugins/ai-chat/style-wc.css create mode 100644 packages/stack/src/plugins/blog/style-wc.css create mode 100644 packages/stack/src/plugins/cms/style-wc.css create mode 100644 packages/stack/src/plugins/form-builder/style-wc.css create mode 100644 packages/stack/src/plugins/kanban/style-wc.css create mode 100644 packages/stack/src/plugins/route-docs/style-wc.css create mode 100644 packages/stack/src/plugins/ui-builder/style-wc.css diff --git a/demos/ai-chat/app/globals.css b/demos/ai-chat/app/globals.css index 23035e76..93408a5c 100644 --- a/demos/ai-chat/app/globals.css +++ b/demos/ai-chat/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/ai-chat/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/ai-chat/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/ai-chat/package.json b/demos/ai-chat/package.json index 116c12b9..2e2371e3 100644 --- a/demos/ai-chat/package.json +++ b/demos/ai-chat/package.json @@ -11,7 +11,7 @@ "@ai-sdk/openai": "^3.0.41", "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-dialog": "^1.1.15", diff --git a/demos/blog/app/globals.css b/demos/blog/app/globals.css index 0640acda..f60cfcb2 100644 --- a/demos/blog/app/globals.css +++ b/demos/blog/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/blog/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/blog/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/blog/package.json b/demos/blog/package.json index 798f9f3b..342d98a1 100644 --- a/demos/blog/package.json +++ b/demos/blog/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-dialog": "^1.1.15", diff --git a/demos/cms/app/globals.css b/demos/cms/app/globals.css index 62dce9a0..8215362f 100644 --- a/demos/cms/app/globals.css +++ b/demos/cms/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/cms/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/cms/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/cms/package.json b/demos/cms/package.json index e0da6aff..2f37e358 100644 --- a/demos/cms/package.json +++ b/demos/cms/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/demos/form-builder/app/globals.css b/demos/form-builder/app/globals.css index df1dbb9c..05434653 100644 --- a/demos/form-builder/app/globals.css +++ b/demos/form-builder/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/form-builder/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/form-builder/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/form-builder/package.json b/demos/form-builder/package.json index c9bcfe7d..6bfcd283 100644 --- a/demos/form-builder/package.json +++ b/demos/form-builder/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", diff --git a/demos/kanban/app/globals.css b/demos/kanban/app/globals.css index 05473a81..da46c43b 100644 --- a/demos/kanban/app/globals.css +++ b/demos/kanban/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/kanban/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/kanban/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/kanban/package.json b/demos/kanban/package.json index 26c02bb0..b7078127 100644 --- a/demos/kanban/package.json +++ b/demos/kanban/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/demos/ui-builder/app/globals.css b/demos/ui-builder/app/globals.css index a9d201e4..2c82c159 100644 --- a/demos/ui-builder/app/globals.css +++ b/demos/ui-builder/app/globals.css @@ -1,8 +1,8 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "@btst/stack/plugins/ui-builder/css"; -@import "@btst/stack/plugins/cms/css"; -@import "@btst/stack/plugins/route-docs/css"; +@import "@btst/stack/plugins/ui-builder/css-wc"; +@import "@btst/stack/plugins/cms/css-wc"; +@import "@btst/stack/plugins/route-docs/css-wc"; @custom-variant dark (&:is(.dark *)); diff --git a/demos/ui-builder/package.json b/demos/ui-builder/package.json index 23162df6..f67c5295 100644 --- a/demos/ui-builder/package.json +++ b/demos/ui-builder/package.json @@ -10,7 +10,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.118", "@btst/adapter-memory": "^2.0.3", - "@btst/stack": "^2.5.1", + "@btst/stack": "^2.5.2", "@btst/yar": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/docs/content/docs/demos/ai-chat.mdx b/docs/content/docs/demos/ai-chat.mdx index a22efc91..66aea9b9 100644 --- a/docs/content/docs/demos/ai-chat.mdx +++ b/docs/content/docs/demos/ai-chat.mdx @@ -15,4 +15,11 @@ A standalone Next.js demo showcasing the `ai-chat` plugin in **public mode** (no To enable AI responses in StackBlitz, add your `OPENAI_API_KEY` to the `.env.local` file (copy from `.env.local.example`). -[Open in CodeSandbox →](https://codesandbox.io/p/devbox/github/better-stack-ai/better-stack/tree/main/demos/ai-chat) +[Open in CodeSandbox →](https://codesandbox.io/p/devbox/github/better-stack-ai/better-stack/tree/main/demos/ai-chat?file=%2Flib%2Fstack.ts) +[Open in StackBlitz →](https://stackblitz.com/github/better-stack-ai/better-stack/tree/main/demos/ai-chat?file=lib%2Fstack.ts) + +