feat: support for extracting css variables

This commit is contained in:
ayang
2025-12-15 10:57:31 +08:00
parent 3255b4d87d
commit cc23f9d180
3 changed files with 137 additions and 89 deletions

39
scripts/buildWebAfter.ts Normal file
View File

@@ -0,0 +1,39 @@
import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const extractCssVars = () => {
const filePath = join(__dirname, "../out/search-chat/index.css");
const cssContent = readFileSync(filePath, "utf-8");
const vars: Record<string, string> = {};
const propertyBlockRegex = /@property\s+(--[\w-]+)\s*\{([\s\S]*?)\}/g;
let match: RegExpExecArray | null;
while ((match = propertyBlockRegex.exec(cssContent))) {
const [, varName, body] = match;
const initialValueMatch = /initial-value\s*:\s*([^;]+);/.exec(body);
if (initialValueMatch) {
vars[varName] = initialValueMatch[1].trim();
}
}
const cssVarsBlock =
`.coco-container {\n` +
Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`)
.join("\n") +
`\n}\n`;
writeFileSync(filePath, `${cssContent}\n${cssVarsBlock}`, "utf-8");
};
extractCssVars();

View File

@@ -73,8 +73,6 @@
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--tw-border-style: solid;
} }
@theme { @theme {
@@ -105,7 +103,7 @@
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
} }
#searchChat-container{ #searchChat-container {
/* Map tokens directly; they are oklch(...) or other full values */ /* Map tokens directly; they are oklch(...) or other full values */
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);

View File

@@ -1,12 +1,18 @@
import { defineConfig } from 'tsup'; import { defineConfig } from "tsup";
import { writeFileSync, readFileSync, readdirSync, statSync, existsSync } from 'fs'; import {
import { join, resolve } from 'path'; writeFileSync,
import postcss from 'postcss'; readFileSync,
import tailwindcssPostcss from '@tailwindcss/postcss'; readdirSync,
import autoprefixer from 'autoprefixer'; statSync,
existsSync,
} from "fs";
import { join, resolve } from "path";
import postcss from "postcss";
import tailwindcssPostcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";
const projectPackageJson = JSON.parse( const projectPackageJson = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8') readFileSync(join(__dirname, "package.json"), "utf-8")
); );
function walk(dir: string): string[] { function walk(dir: string): string[] {
@@ -29,8 +35,8 @@ function hasTauriRefs(content: string): boolean {
} }
export default defineConfig({ export default defineConfig({
entry: ['src/pages/web/index.tsx'], entry: ["src/pages/web/index.tsx"],
format: ['esm'], format: ["esm"],
dts: true, dts: true,
splitting: true, splitting: true,
sourcemap: false, sourcemap: false,
@@ -38,74 +44,73 @@ export default defineConfig({
treeshake: true, treeshake: true,
minify: true, minify: true,
env: { env: {
BUILD_TARGET: 'web', BUILD_TARGET: "web",
NODE_ENV: 'production', NODE_ENV: "production",
}, },
external: [ external: ["react", "react-dom"],
'react',
'react-dom',
],
esbuildOptions(options) { esbuildOptions(options) {
options.bundle = true; options.bundle = true;
options.platform = 'browser'; options.platform = "browser";
// Enable Tailwind v4 CSS import resolution using the "style" condition // Enable Tailwind v4 CSS import resolution using the "style" condition
// so that `@import "tailwindcss";` in CSS can be resolved by esbuild. // so that `@import "tailwindcss";` in CSS can be resolved by esbuild.
// See: https://tailwindcss.com/docs/installation#bundlers // See: https://tailwindcss.com/docs/installation#bundlers
(options as any).conditions = ["style", "browser", "module", "default"]; (options as any).conditions = ["style", "browser", "module", "default"];
options.loader = { (options.loader = {
'.css': 'css', ".css": "css",
'.scss': 'css', ".scss": "css",
'.svg': 'dataurl', ".svg": "dataurl",
'.png': 'dataurl', ".png": "dataurl",
'.jpg': 'dataurl', ".jpg": "dataurl",
}, }),
options.alias = { (options.alias = {
'@': resolve(__dirname, './src') "@": resolve(__dirname, "./src"),
} });
options.external = [ options.external = [
'@tauri-apps/api', "@tauri-apps/api",
'@tauri-apps/plugin-*', "@tauri-apps/plugin-*",
'tauri-plugin-*', "tauri-plugin-*",
]; ];
options.treeShaking = true; options.treeShaking = true;
options.define = { options.define = {
'process.env.BUILD_TARGET': '"web"', "process.env.BUILD_TARGET": '"web"',
'process.env.NODE_ENV': '"production"', "process.env.NODE_ENV": '"production"',
'process.env.DEBUG': 'false', "process.env.DEBUG": "false",
'process.env.IS_DEV': 'false', "process.env.IS_DEV": "false",
'process.env.VERSION': `"${projectPackageJson.version}"`, "process.env.VERSION": `"${projectPackageJson.version}"`,
}; };
options.pure = ['console.log']; options.pure = ["console.log"];
options.target = 'es2020'; options.target = "es2020";
options.legalComments = 'none'; options.legalComments = "none";
options.ignoreAnnotations = false; options.ignoreAnnotations = false;
}, },
esbuildPlugins: [ esbuildPlugins: [
{ {
name: 'jsx-import-source', name: "jsx-import-source",
setup(build) { setup(build) {
build.initialOptions.jsx = 'automatic'; build.initialOptions.jsx = "automatic";
build.initialOptions.jsxImportSource = 'react'; build.initialOptions.jsxImportSource = "react";
}, },
}, },
], ],
outDir: 'out/search-chat', outDir: "out/search-chat",
async onSuccess() { async onSuccess() {
const outDir = join(__dirname, 'out/search-chat'); const outDir = join(__dirname, "out/search-chat");
const files = walk(outDir).filter(f => /\.(m?js|cjs)$/i.test(f)); const files = walk(outDir).filter((f) => /\.(m?js|cjs)$/i.test(f));
const tauriFiles = files.filter(f => { const tauriFiles = files.filter((f) => {
const content = readFileSync(f, 'utf-8'); const content = readFileSync(f, "utf-8");
return hasTauriRefs(content); return hasTauriRefs(content);
}); });
if (tauriFiles.length) { if (tauriFiles.length) {
throw new Error( throw new Error(
`Build output contains Tauri references:\n${tauriFiles.map(f => ` - ${f}`).join('\n')}` `Build output contains Tauri references:\n${tauriFiles
.map((f) => ` - ${f}`)
.join("\n")}`
); );
} }
const projectPackageJson = JSON.parse( const projectPackageJson = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8') readFileSync(join(__dirname, "package.json"), "utf-8")
); );
const packageJson = { const packageJson = {
@@ -117,57 +122,51 @@ export default defineConfig({
types: "index.d.ts", types: "index.d.ts",
dependencies: projectPackageJson.dependencies as Record<string, string>, dependencies: projectPackageJson.dependencies as Record<string, string>,
peerDependencies: { peerDependencies: {
"react": "^18.0.0", react: "^18.0.0",
"react-dom": "^18.0.0" "react-dom": "^18.0.0",
},
sideEffects: ["*.css", "*.scss"],
publishConfig: {
access: "public",
registry: "https://registry.npmjs.org/",
}, },
"sideEffects": [
"*.css",
"*.scss"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}; };
const noNeedDeps = [
"dotenv",
"uuid",
"wavesurfer.js",
]
const tauriDeps = Object.keys(packageJson.dependencies).filter(dep => const noNeedDeps = ["dotenv", "uuid", "wavesurfer.js"];
dep.includes('@tauri-apps') ||
dep.includes('tauri-plugin') || const tauriDeps = Object.keys(packageJson.dependencies).filter(
noNeedDeps.includes(dep) (dep) =>
dep.includes("@tauri-apps") ||
dep.includes("tauri-plugin") ||
noNeedDeps.includes(dep)
); );
tauriDeps.forEach(dep => { tauriDeps.forEach((dep) => {
delete packageJson.dependencies[dep]; delete packageJson.dependencies[dep];
}); });
writeFileSync( writeFileSync(
join(__dirname, 'out/search-chat/package.json'), join(__dirname, "out/search-chat/package.json"),
JSON.stringify(packageJson, null, 2) JSON.stringify(packageJson, null, 2)
); );
try { try {
const readmePath = join(__dirname, 'src/pages/web/README.md'); const readmePath = join(__dirname, "src/pages/web/README.md");
const readmeContent = readFileSync(readmePath, 'utf-8'); const readmeContent = readFileSync(readmePath, "utf-8");
writeFileSync( writeFileSync(
join(__dirname, 'out/search-chat/README.md'), join(__dirname, "out/search-chat/README.md"),
readmeContent readmeContent
); );
} catch (error) { } catch (error) {
console.error('Failed to copy README.md:', error); console.error("Failed to copy README.md:", error);
} }
// Ensure Tailwind v4 directives (@import, @source, @apply, @theme) are compiled // Ensure Tailwind v4 directives (@import, @source, @apply, @theme) are compiled
// into a final CSS for the library consumer. Esbuild doesn't process Tailwind, // into a final CSS for the library consumer. Esbuild doesn't process Tailwind,
// so we run PostCSS with Tailwind + Autoprefixer here to produce index.css. // so we run PostCSS with Tailwind + Autoprefixer here to produce index.css.
try { try {
const cssInPath = join(__dirname, 'src/main.css'); const cssInPath = join(__dirname, "src/main.css");
const cssOutPath = join(__dirname, 'out/search-chat/index.css'); const cssOutPath = join(__dirname, "out/search-chat/index.css");
const cssIn = readFileSync(cssInPath, 'utf-8'); const cssIn = readFileSync(cssInPath, "utf-8");
const result = await postcss([ const result = await postcss([
// Use the Tailwind v4 PostCSS plugin from @tailwindcss/postcss // Use the Tailwind v4 PostCSS plugin from @tailwindcss/postcss
@@ -183,20 +182,21 @@ export default defineConfig({
// This fixes consumer bundlers failing to resolve "/assets/*.png" from node_modules // This fixes consumer bundlers failing to resolve "/assets/*.png" from node_modules
const assetRegex = /url\((['"])\/assets\/([^'"\)]+)\1\)/g; const assetRegex = /url\((['"])\/assets\/([^'"\)]+)\1\)/g;
const rewrittenCss = result.css.replace(assetRegex, (_m, quote, file) => { const rewrittenCss = result.css.replace(assetRegex, (_m, quote, file) => {
const srcAssetPath = join(__dirname, 'src/assets', file); const srcAssetPath = join(__dirname, "src/assets", file);
if (!existsSync(srcAssetPath)) { if (!existsSync(srcAssetPath)) {
console.warn(`[build:web] Asset not found: ${srcAssetPath}`); console.warn(`[build:web] Asset not found: ${srcAssetPath}`);
return `url(${quote}/assets/${file}${quote})`; return `url(${quote}/assets/${file}${quote})`;
} }
try { try {
const buffer = readFileSync(srcAssetPath); const buffer = readFileSync(srcAssetPath);
const ext = file.split('.').pop()?.toLowerCase() ?? 'png'; const ext = file.split(".").pop()?.toLowerCase() ?? "png";
const mime = ext === 'svg' const mime =
? 'image/svg+xml' ext === "svg"
: ext === 'jpg' || ext === 'jpeg' ? "image/svg+xml"
? 'image/jpeg' : ext === "jpg" || ext === "jpeg"
: 'image/png'; ? "image/jpeg"
const base64 = buffer.toString('base64'); : "image/png";
const base64 = buffer.toString("base64");
const dataUrl = `data:${mime};base64,${base64}`; const dataUrl = `data:${mime};base64,${base64}`;
return `url(${quote}${dataUrl}${quote})`; return `url(${quote}${dataUrl}${quote})`;
} catch (err) { } catch (err) {
@@ -207,7 +207,18 @@ export default defineConfig({
writeFileSync(cssOutPath, rewrittenCss); writeFileSync(cssOutPath, rewrittenCss);
} catch (error) { } catch (error) {
console.error('Failed to compile Tailwind CSS with PostCSS:', error); console.error("Failed to compile Tailwind CSS with PostCSS:", error);
} }
}
try {
console.log("[build:web] Executing buildWebAfter.ts script...");
await import("./scripts/buildWebAfter.ts");
console.log("[build:web] buildWebAfter.ts script executed successfully");
} catch (error) {
console.error(
"[build:web] Failed to execute buildWebAfter.ts script:",
error
);
}
},
}); });