2025-05-08 09:06:32 +08:00
|
|
|
import fs from 'fs/promises';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
2025-06-15 08:11:00 +08:00
|
|
|
import { exec } from 'child_process';
|
|
|
|
|
import { promisify } from 'util';
|
2026-06-03 23:08:41 +08:00
|
|
|
import { generateCssEntrypoints } from './generate-css-entrypoints.js';
|
2025-06-15 08:11:00 +08:00
|
|
|
|
|
|
|
|
const execPromise = promisify(exec);
|
2025-05-08 09:06:32 +08:00
|
|
|
|
|
|
|
|
// Resolve project root, assuming this script is in <project_root>/scripts/
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
|
const projectRoot = path.resolve(__dirname, '..');
|
|
|
|
|
|
|
|
|
|
// Ensures a directory exists. If it doesn't, it's created recursively.
|
|
|
|
|
async function ensureDir(dirPath) {
|
|
|
|
|
try {
|
|
|
|
|
await fs.access(dirPath);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.code === 'ENOENT') {
|
|
|
|
|
await fs.mkdir(dirPath, { recursive: true });
|
|
|
|
|
} else {
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cleans a directory by removing all its contents (files and subdirectories).
|
|
|
|
|
async function cleanDir(directory) {
|
|
|
|
|
try {
|
|
|
|
|
await fs.access(directory);
|
|
|
|
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const entryPath = path.join(directory, entry.name);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
await fs.rm(entryPath, { recursive: true, force: true });
|
|
|
|
|
} else {
|
|
|
|
|
await fs.unlink(entryPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log(`Cleaned directory: ${directory}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.code === 'ENOENT') {
|
|
|
|
|
console.log(`Directory not found, no need to clean: ${directory}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`Error cleaning directory ${directory}:`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recursively copies a directory.
|
|
|
|
|
async function copyDirRecursive(src, dest) {
|
|
|
|
|
await ensureDir(dest);
|
|
|
|
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
|
|
|
for (let entry of entries) {
|
|
|
|
|
const srcPath = path.join(src, entry.name);
|
|
|
|
|
const destPath = path.join(dest, entry.name);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
await copyDirRecursive(srcPath, destPath);
|
|
|
|
|
} else {
|
|
|
|
|
await fs.copyFile(srcPath, destPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 09:23:51 +08:00
|
|
|
// 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`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 18:07:58 +08:00
|
|
|
// Main build function to prepare the package for publishing.
|
2025-05-08 09:06:32 +08:00
|
|
|
async function build() {
|
|
|
|
|
console.log('Starting build process...');
|
2026-06-03 23:08:41 +08:00
|
|
|
await generateCssEntrypoints();
|
2025-05-08 09:06:32 +08:00
|
|
|
|
|
|
|
|
// Define all necessary paths
|
2026-06-27 18:07:58 +08:00
|
|
|
const cssDistDir = path.join(projectRoot, 'dist');
|
|
|
|
|
const cssTemplatesDir = path.join(projectRoot, 'templates');
|
2025-05-08 09:06:32 +08:00
|
|
|
|
|
|
|
|
const srcDir = path.join(projectRoot, 'src');
|
|
|
|
|
const srcCssDir = path.join(srcDir, 'css');
|
2026-06-01 15:10:35 +08:00
|
|
|
const srcCssStylesDir = path.join(srcCssDir, 'styles');
|
2026-06-20 13:58:42 +08:00
|
|
|
const srcCssCompatDir = path.join(srcCssDir, 'compat');
|
2025-05-08 09:06:32 +08:00
|
|
|
const srcJsDir = path.join(srcDir, 'js');
|
2026-06-27 18:07:58 +08:00
|
|
|
const srcNunjucksDir = path.join(srcDir, 'templates', 'nunjucks');
|
|
|
|
|
const srcJinjaDir = path.join(srcDir, 'templates', 'jinja');
|
2025-05-08 09:06:32 +08:00
|
|
|
|
|
|
|
|
// Clean previous build artifacts
|
|
|
|
|
console.log('Cleaning distribution directories...');
|
|
|
|
|
await cleanDir(cssDistDir);
|
2026-06-27 08:55:48 +08:00
|
|
|
await cleanDir(cssTemplatesDir);
|
2025-05-08 09:06:32 +08:00
|
|
|
|
2025-06-15 08:11:00 +08:00
|
|
|
// JS files minification and copy
|
|
|
|
|
const jsFiles = await fs.readdir(srcJsDir);
|
|
|
|
|
const cssJsDistDir = path.join(cssDistDir, 'js');
|
|
|
|
|
await ensureDir(cssJsDistDir);
|
|
|
|
|
console.log('Copying and minifying JS files...');
|
|
|
|
|
|
|
|
|
|
for (const jsFile of jsFiles) {
|
|
|
|
|
if (path.extname(jsFile) === '.js') {
|
|
|
|
|
const srcFile = path.join(srcJsDir, jsFile);
|
|
|
|
|
const baseName = path.basename(jsFile, '.js');
|
|
|
|
|
const minifiedFileName = `${baseName}.min.js`;
|
|
|
|
|
|
2026-06-27 08:55:48 +08:00
|
|
|
// Copy original file to destination
|
2025-06-15 08:11:00 +08:00
|
|
|
await fs.copyFile(srcFile, path.join(cssJsDistDir, jsFile));
|
|
|
|
|
|
2026-06-27 08:55:48 +08:00
|
|
|
// Create minified file
|
|
|
|
|
await execPromise(`npx terser ${srcFile} -o ${path.join(cssJsDistDir, minifiedFileName)} --compress --mangle`);
|
2025-06-15 08:11:00 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create combined component files
|
|
|
|
|
console.log('Creating combined component files...');
|
2026-06-25 12:59:15 +08:00
|
|
|
const componentsToCombine = ['basecoat.js', 'accordion.js', 'command.js', 'combobox.js', 'drawer.js', 'dropdown-menu.js', 'popover.js', 'range.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
|
2025-06-15 08:11:00 +08:00
|
|
|
const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f));
|
|
|
|
|
|
|
|
|
|
// Create non-minified bundle
|
|
|
|
|
let combinedContent = '';
|
|
|
|
|
for (const p of componentPaths) {
|
|
|
|
|
combinedContent += await fs.readFile(p, 'utf-8') + '\n';
|
|
|
|
|
}
|
2026-06-27 08:55:48 +08:00
|
|
|
const allJsPath = path.join(cssJsDistDir, 'all.js');
|
2025-06-15 08:11:00 +08:00
|
|
|
await fs.writeFile(allJsPath, combinedContent);
|
|
|
|
|
|
|
|
|
|
// Create minified bundle
|
2026-06-27 08:55:48 +08:00
|
|
|
const allMinJsPath = path.join(cssJsDistDir, 'all.min.js');
|
2025-06-15 08:11:00 +08:00
|
|
|
await execPromise(`npx terser ${componentPaths.join(' ')} -o ${allMinJsPath} --compress --mangle`);
|
|
|
|
|
|
2026-06-27 08:55:48 +08:00
|
|
|
console.log(`Copied and minified JS to ${cssJsDistDir}`);
|
2025-05-08 09:06:32 +08:00
|
|
|
|
|
|
|
|
// Build CSS package
|
|
|
|
|
console.log('Building CSS package...');
|
2026-06-27 18:07:58 +08:00
|
|
|
await ensureDir(cssDistDir);
|
2026-06-03 18:59:21 +08:00
|
|
|
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
|
2025-05-08 09:06:32 +08:00
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.css'));
|
2026-06-01 15:10:35 +08:00
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat.all.css'), path.join(cssDistDir, 'basecoat.all.css'));
|
2026-06-03 23:08:41 +08:00
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.css'));
|
|
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.cdn.css'), path.join(cssDistDir, 'basecoat-base.cdn.css'));
|
2026-06-20 13:58:42 +08:00
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.css'), path.join(cssDistDir, 'basecoat-compat.css'));
|
|
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.cdn.css'), path.join(cssDistDir, 'basecoat-compat.cdn.css'));
|
2026-06-03 23:08:41 +08:00
|
|
|
await fs.copyFile(path.join(srcCssDir, 'basecoat-components.css'), path.join(cssDistDir, 'basecoat-components.css'));
|
2026-06-03 18:59:21 +08:00
|
|
|
for (const style of styles) {
|
|
|
|
|
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.css`), path.join(cssDistDir, `basecoat-${style}.css`));
|
|
|
|
|
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.cdn.css`), path.join(cssDistDir, `basecoat-${style}.cdn.css`));
|
|
|
|
|
}
|
|
|
|
|
console.log(`Copied basecoat CSS entrypoints to ${cssDistDir}`);
|
2025-05-08 09:06:32 +08:00
|
|
|
|
2026-06-01 15:10:35 +08:00
|
|
|
// Copy split CSS folders used by basecoat.css imports.
|
|
|
|
|
const cssBaseSrcDir = path.join(srcCssDir, 'base');
|
|
|
|
|
const cssComponentsSrcDir = path.join(srcCssDir, 'components');
|
|
|
|
|
const cssBaseDistDir = path.join(cssDistDir, 'base');
|
|
|
|
|
const cssComponentsDistDir = path.join(cssDistDir, 'components');
|
|
|
|
|
const cssStylesDistDir = path.join(cssDistDir, 'styles');
|
2026-06-20 13:58:42 +08:00
|
|
|
const cssCompatDistDir = path.join(cssDistDir, 'compat');
|
2026-06-01 15:10:35 +08:00
|
|
|
await copyDirRecursive(cssBaseSrcDir, cssBaseDistDir);
|
|
|
|
|
await copyDirRecursive(cssComponentsSrcDir, cssComponentsDistDir);
|
|
|
|
|
await copyDirRecursive(srcCssStylesDir, cssStylesDistDir);
|
2026-06-20 13:58:42 +08:00
|
|
|
await copyDirRecursive(srcCssCompatDir, cssCompatDistDir);
|
2026-06-01 15:10:35 +08:00
|
|
|
console.log(`Copied split CSS folders to ${cssDistDir}`);
|
|
|
|
|
|
2026-10-23 09:23:51 +08:00
|
|
|
// 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}`);
|
|
|
|
|
|
2026-06-27 08:55:48 +08:00
|
|
|
await copyDirRecursive(srcNunjucksDir, path.join(cssTemplatesDir, 'nunjucks'));
|
|
|
|
|
await copyDirRecursive(srcJinjaDir, path.join(cssTemplatesDir, 'jinja'));
|
|
|
|
|
console.log(`Copied template assets to ${cssTemplatesDir}`);
|
|
|
|
|
|
2026-06-03 18:59:21 +08:00
|
|
|
// Create Tailwind CSS builds for the CSS package.
|
2026-06-20 13:58:42 +08:00
|
|
|
const cdnEntries = ['basecoat.cdn.css', 'basecoat-base.cdn.css', 'basecoat-compat.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
|
2026-06-03 18:59:21 +08:00
|
|
|
for (const entry of cdnEntries) {
|
|
|
|
|
const cdnCssSrc = path.join(srcCssDir, entry);
|
|
|
|
|
const baseName = path.basename(entry, '.css');
|
|
|
|
|
const cssDistCdnPath = path.join(cssDistDir, `${baseName}.css`);
|
|
|
|
|
const cssDistCdnMinPath = path.join(cssDistDir, `${baseName}.min.css`);
|
|
|
|
|
|
|
|
|
|
await execPromise(`npx tailwindcss -i "${cdnCssSrc}" -o "${cssDistCdnPath}"`);
|
|
|
|
|
console.log(`Generated non-minified CSS: ${cssDistCdnPath}`);
|
|
|
|
|
await execPromise(`npx tailwindcss -i "${cdnCssSrc}" -o "${cssDistCdnMinPath}" --minify`);
|
|
|
|
|
console.log(`Generated minified CSS: ${cssDistCdnMinPath}`);
|
|
|
|
|
}
|
2025-06-15 08:11:00 +08:00
|
|
|
|
2025-05-08 09:06:32 +08:00
|
|
|
console.log('Build process finished successfully!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
build().catch(error => {
|
|
|
|
|
console.error('Build failed:', error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|