feat: init
This commit is contained in:
commit
610fa90bec
147 files changed
+4979
No files matched your search
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env node
|
||||
// claddcn — puts the cladd.io look onto an existing shadcn-svelte project.
|
||||
//
|
||||
// npx claddcn init [--cwd <dir>] [--css <file>] [--only button,card] [--strip-vars] [--no-install] [--dry]
|
||||
//
|
||||
// What `init` does, in order:
|
||||
// 1. copies claddcn.css next to the Tailwind entry named in components.json
|
||||
// and imports it right after the other top-level @imports;
|
||||
// 2. with --strip-vars, removes the old :root / .dark / @theme inline token
|
||||
// blocks from that file (claddcn ships its own, and later ones would win);
|
||||
// 3. overwrites ui components with the cladd versions (same names, same API),
|
||||
// backing up whatever it replaces into .claddcn-backup/<timestamp>/;
|
||||
// 4. teaches `cn()` the cladd scale so tailwind-merge doesn't drop classes;
|
||||
// 5. adds the Geist / Geist Mono fonts with the project's package manager.
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const REGISTRY_UI = path.join(ROOT, 'registry', 'ui');
|
||||
const THEME_CSS = path.join(ROOT, 'src', 'claddcn.css');
|
||||
const UTILS_TS = path.join(ROOT, 'registry', 'lib', 'utils.ts');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
const flag = (name) => args.includes(`--${name}`);
|
||||
const option = (name) => {
|
||||
const i = args.indexOf(`--${name}`);
|
||||
return i >= 0 ? args[i + 1] : undefined;
|
||||
};
|
||||
|
||||
const cwd = path.resolve(option('cwd') ?? process.cwd());
|
||||
const dry = flag('dry');
|
||||
const log = (msg) => console.log(`claddcn ${msg}`);
|
||||
|
||||
function write(file, content) {
|
||||
if (dry) return log(`[dry] write ${path.relative(cwd, file)}`);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, content);
|
||||
}
|
||||
|
||||
function copyDir(src, dest) {
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const from = path.join(src, entry.name);
|
||||
const to = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) copyDir(from, to);
|
||||
else write(to, fs.readFileSync(from));
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAlias(alias, config) {
|
||||
const lib = config.aliases?.lib ?? '$lib';
|
||||
if (alias.startsWith('$lib')) return path.join(cwd, 'src', 'lib', alias.slice('$lib'.length));
|
||||
if (alias.startsWith(lib)) return path.join(cwd, 'src', 'lib', alias.slice(lib.length));
|
||||
return path.join(cwd, alias);
|
||||
}
|
||||
|
||||
// Removes a top-level block that starts with `selector {` by brace matching.
|
||||
function stripBlock(css, selector) {
|
||||
const re = new RegExp(`(^|\\n)${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{`);
|
||||
const m = re.exec(css);
|
||||
if (!m) return css;
|
||||
let depth = 0;
|
||||
let i = m.index + m[0].length - 1;
|
||||
for (; i < css.length; i++) {
|
||||
if (css[i] === '{') depth++;
|
||||
else if (css[i] === '}' && --depth === 0) break;
|
||||
}
|
||||
return css.slice(0, m.index) + css.slice(i + 1);
|
||||
}
|
||||
|
||||
// Tailwind entries under src/, i.e. CSS files that `@import 'tailwindcss'`.
|
||||
function findTailwindEntries(dir = path.join(cwd, 'src'), found = []) {
|
||||
if (!fs.existsSync(dir)) return found;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const file = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) findTailwindEntries(file, found);
|
||||
else if (
|
||||
entry.name.endsWith('.css') &&
|
||||
entry.name !== 'claddcn.css' &&
|
||||
/^@import ['"]tailwindcss['"]/m.test(fs.readFileSync(file, 'utf8'))
|
||||
)
|
||||
found.push(file);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// --css wins; then components.json; if that path is stale, the single Tailwind
|
||||
// entry under src/ (or the one that already imports claddcn).
|
||||
function resolveCss(config) {
|
||||
if (option('css')) {
|
||||
const file = path.join(cwd, option('css'));
|
||||
if (fs.existsSync(file)) return file;
|
||||
console.error(`claddcn ${option('css')} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
const configured = path.join(cwd, config.tailwind.css);
|
||||
if (fs.existsSync(configured)) return configured;
|
||||
const entries = findTailwindEntries();
|
||||
const ours = entries.filter((f) => fs.readFileSync(f, 'utf8').includes('claddcn.css'));
|
||||
const pick = entries.length === 1 ? entries[0] : ours.length === 1 ? ours[0] : null;
|
||||
if (pick) {
|
||||
log(`components.json points at ${config.tailwind.css}, using ${path.relative(cwd, pick)}`);
|
||||
return pick;
|
||||
}
|
||||
console.error(`claddcn ${config.tailwind.css} not found. Pick the Tailwind entry with --css:`);
|
||||
for (const f of entries) console.error(` --css "${path.relative(cwd, f)}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function patchCss(cssFile) {
|
||||
let css = fs.readFileSync(cssFile, 'utf8');
|
||||
if (!css.includes('claddcn.css')) {
|
||||
const imports = [...css.matchAll(/^@import[^;]+;\s*$/gm)];
|
||||
const at = imports.length ? imports.at(-1).index + imports.at(-1)[0].length : 0;
|
||||
css = `${css.slice(0, at)}\n@import './claddcn.css';${css.slice(at)}`;
|
||||
log(`import added to ${path.relative(cwd, cssFile)}`);
|
||||
}
|
||||
if (flag('strip-vars')) {
|
||||
for (const selector of [':root', '.dark', '@theme inline']) css = stripBlock(css, selector);
|
||||
log('old :root / .dark / @theme inline blocks removed');
|
||||
} else if (/(^|\n):root\s*\{[^}]*--background/.test(css)) {
|
||||
log('warning: this file still defines shadcn tokens after the import and will override');
|
||||
log(' claddcn colors. Re-run with --strip-vars or delete those blocks by hand.');
|
||||
}
|
||||
write(cssFile, css.replace(/\n{3,}/g, '\n\n'));
|
||||
}
|
||||
|
||||
function patchUtils(utilsFile) {
|
||||
if (!fs.existsSync(utilsFile)) return write(utilsFile, fs.readFileSync(UTILS_TS));
|
||||
const src = fs.readFileSync(utilsFile, 'utf8');
|
||||
if (src.includes('extendTailwindMerge')) return log('cn() already extended, left as is');
|
||||
const ours = fs.readFileSync(UTILS_TS, 'utf8');
|
||||
const block = ours.slice(0, ours.indexOf('export function cn'));
|
||||
const next = src.replace(
|
||||
/import \{ clsx, type ClassValue \} from 'clsx';\s*\nimport \{ twMerge \} from 'tailwind-merge';\s*\n/,
|
||||
block
|
||||
);
|
||||
if (next === src) {
|
||||
log(
|
||||
`warning: couldn't patch ${path.relative(cwd, utilsFile)} — merge registry/lib/utils.ts by hand`
|
||||
);
|
||||
return;
|
||||
}
|
||||
write(utilsFile, next);
|
||||
log('cn() now knows the cladd scale');
|
||||
}
|
||||
|
||||
function install() {
|
||||
const has = (f) => fs.existsSync(path.join(cwd, f));
|
||||
const pkg = '@fontsource-variable/geist @fontsource-variable/geist-mono';
|
||||
const cmd =
|
||||
has('bun.lock') || has('bun.lockb')
|
||||
? `bun add -d ${pkg}`
|
||||
: has('pnpm-lock.yaml')
|
||||
? `pnpm add -D ${pkg}`
|
||||
: has('yarn.lock')
|
||||
? `yarn add -D ${pkg}`
|
||||
: `npm i -D ${pkg}`;
|
||||
if (flag('no-install') || dry) return log(`skip install, run: ${cmd}`);
|
||||
log(cmd);
|
||||
execSync(cmd, { cwd, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function init() {
|
||||
const configFile = path.join(cwd, 'components.json');
|
||||
if (!fs.existsSync(configFile)) {
|
||||
console.error(`claddcn no components.json in ${cwd} — is this a shadcn-svelte project?`);
|
||||
process.exit(1);
|
||||
}
|
||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
const cssFile = resolveCss(config);
|
||||
const uiDir = resolveAlias(config.aliases?.ui ?? '$lib/components/ui', config);
|
||||
const utilsFile = `${resolveAlias(config.aliases?.utils ?? '$lib/utils', config)}.ts`;
|
||||
|
||||
const only = option('only')?.split(',');
|
||||
const available = fs.readdirSync(REGISTRY_UI);
|
||||
const components = only ?? available;
|
||||
const missing = components.filter((c) => !available.includes(c));
|
||||
if (missing.length) log(`unknown components skipped: ${missing.join(', ')}`);
|
||||
|
||||
write(path.join(path.dirname(cssFile), 'claddcn.css'), fs.readFileSync(THEME_CSS));
|
||||
patchCss(cssFile);
|
||||
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backup = path.join(cwd, '.claddcn-backup', stamp);
|
||||
for (const name of components.filter((c) => available.includes(c))) {
|
||||
const target = path.join(uiDir, name);
|
||||
if (fs.existsSync(target) && !dry) {
|
||||
fs.mkdirSync(backup, { recursive: true });
|
||||
fs.cpSync(target, path.join(backup, name), { recursive: true });
|
||||
}
|
||||
copyDir(path.join(REGISTRY_UI, name), target);
|
||||
}
|
||||
log(`${components.length} components written to ${path.relative(cwd, uiDir)}`);
|
||||
if (fs.existsSync(backup)) log(`previous versions saved in ${path.relative(cwd, backup)}`);
|
||||
|
||||
patchUtils(utilsFile);
|
||||
install();
|
||||
log('done — the cladd look is in. Dark mode follows the `.dark` class (mode-watcher).');
|
||||
}
|
||||
|
||||
if (command === 'init') init();
|
||||
else {
|
||||
console.log(
|
||||
`usage: claddcn init [--cwd <dir>] [--css <file>] [--only a,b] [--strip-vars] [--no-install] [--dry]`
|
||||
);
|
||||
process.exit(command ? 1 : 0);
|
||||
}
|
||||
Reference in new issue
Block a user