From b7d33eee042496cb1947de2861be404e7516fdc5 Mon Sep 17 00:00:00 2001 From: Grug Date: Fri, 23 Oct 2026 09:23:51 +0800 Subject: [PATCH] Add single-file .bundle.css outputs for Tailwind CLI without npm (#206) Emit dist/basecoat-{style}.bundle.css for every style pack, plus dist/basecoat.bundle.css (Vega alias) and dist/basecoat-base.bundle.css. Each is the uncompiled source with relative @imports inlined, so it can be downloaded as one file (e.g. from jsDelivr) and imported after tailwindcss. Compiling a bundle produces byte-identical output to the split entrypoint. Document the bundles on the installation page and fix the description of basecoat-css/all.css, which is an alias for the default stylesheet. Refs #150 --- scripts/build.js | 45 ++++++++++++++++++++++++++++++++++ site/src/docs/installation.mdx | 8 +++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/scripts/build.js b/scripts/build.js index 2d8b3a0..32219f6 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -64,6 +64,37 @@ async function copyDirRecursive(src, dest) { } } +// Recursively inlines relative `@import "./...";` lines into a single CSS string. +// Each file is inlined at most once per bundle (tracked in `seen`). +async function inlineCssImports(filePath, seen) { + const resolved = path.resolve(filePath); + if (seen.has(resolved)) return ''; + seen.add(resolved); + + const source = await fs.readFile(resolved, 'utf-8'); + const lines = source.split('\n'); + const out = []; + for (const line of lines) { + const match = line.match(/^\s*@import\s+["'](\.{1,2}\/[^"']+)["']\s*;\s*$/); + if (match) { + const inlined = await inlineCssImports(path.join(path.dirname(resolved), match[1]), seen); + if (inlined) out.push(inlined.replace(/\n+$/, '')); + } else if (/^\s*@import\s+["']\.{1,2}\//.test(line)) { + throw new Error(`Unsupported relative @import (with modifiers) in ${resolved}: ${line.trim()}`); + } else { + out.push(line); + } + } + return out.join('\n'); +} + +// Writes an uncompiled single-file bundle for a dist CSS entrypoint. +async function writeCssBundle(entryPath, outPath, label, version) { + const body = await inlineCssImports(entryPath, new Set()); + const header = `/* Basecoat v${version}, ${label}, single-file source. Import after tailwindcss. */\n`; + await fs.writeFile(outPath, `${header}${body.replace(/\n+$/, '')}\n`); +} + // Main build function to prepare the package for publishing. async function build() { console.log('Starting build process...'); @@ -155,6 +186,20 @@ async function build() { await copyDirRecursive(srcCssCompatDir, cssCompatDistDir); console.log(`Copied split CSS folders to ${cssDistDir}`); + // Create uncompiled single-file bundles for Tailwind users without a package manager. + const { version } = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8')); + for (const style of styles) { + await writeCssBundle( + path.join(cssDistDir, `basecoat-${style}.css`), + path.join(cssDistDir, `basecoat-${style}.bundle.css`), + style, + version, + ); + } + await writeCssBundle(path.join(cssDistDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.bundle.css'), 'vega', version); + await writeCssBundle(path.join(cssDistDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.bundle.css'), 'base (no style pack)', version); + console.log(`Generated single-file CSS bundles in ${cssDistDir}`); + await copyDirRecursive(srcNunjucksDir, path.join(cssTemplatesDir, 'nunjucks')); await copyDirRecursive(srcJinjaDir, path.join(cssTemplatesDir, 'jinja')); console.log(`Copied template assets to ${cssTemplatesDir}`); diff --git a/site/src/docs/installation.mdx b/site/src/docs/installation.mdx index 1c4dd50..862a972 100644 --- a/site/src/docs/installation.mdx +++ b/site/src/docs/installation.mdx @@ -64,6 +64,8 @@ Or load the Basecoat runtime once, then only the component scripts you use. | `dist/basecoat-{style}.cdn.min.css` | Standalone CDN stylesheet for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. | | `dist/basecoat-base.cdn.min.css` | Base tokens, utilities, and component structure without a style pack. | | `dist/basecoat-compat.cdn.min.css` | Optional pre-1.0 compatibility aliases. Load after Basecoat. | +| `dist/basecoat.bundle.css` | Uncompiled single-file source for use with your own Tailwind build. Alias for the Vega style. | +| `dist/basecoat-{style}.bundle.css` | Uncompiled single-file source for use with your own Tailwind build, for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. | | `dist/js/all.min.js` | All auto-initialized JavaScript components except Chart. | | `dist/js/basecoat.min.js` | Runtime only. Load once before individual component scripts. | | `dist/js/{component}.min.js` | Individual JavaScript component file. | @@ -129,6 +131,10 @@ Or import a named style bundle. @import "basecoat-css/maia"; ``` + + Download a single-file bundle, for example {`basecoat-maia.bundle.css`}. Save it next to your CSS and import it after Tailwind: `@import "tailwindcss"; @import "./basecoat-maia.bundle.css";`. Use `basecoat.bundle.css` for the default Vega style. + + Load Basecoat after any stylesheet that includes Tailwind base/preflight. Loading another full Tailwind build after Basecoat can reset borders, inputs, and other component defaults. @@ -195,7 +201,7 @@ See [Templates](/templates) for the available macros and arguments. | `basecoat-css/components` | All component structure without a style pack. | | `basecoat-css/components/button` | Individual component CSS. Replace `button` with any component file. | | `basecoat-css/styles/maia` | Individual style-pack CSS. Use with `basecoat-css/base` or `basecoat-css/components`. | -| `basecoat-css/all.css` | Full default stylesheet plus all style-pack CSS. Use only when you need every style pack available at runtime. | +| `basecoat-css/all.css` | Alias for `basecoat-css`, the default Vega stylesheet. It does not include the other style packs. | | `basecoat-css/compat` | Optional pre-1.0 compatibility aliases. Load after Basecoat. | ### Template files