#!/usr/bin/env node /** * sync-design-system.mjs * * Vendors shared/playground-design-system/ into a plugin's * playground/vendor/playground-design-system/ tree. * * Usage: * node scripts/sync-design-system.mjs [--force] * * Each plugin keeps its own pinned copy so it stays standalone. * MANIFEST.json records SHA-256 per file + source commit + sync date. * Drift detection refuses overwrite if a vendored file was modified * locally after sync; pass --force to overwrite anyway. * * No npm dependencies. Node 16.7+ for fs.cp(). */ import { createHash } from 'node:crypto'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const MARKETPLACE_ROOT = path.resolve(__dirname, '..'); const SOURCE_DIR = path.join(MARKETPLACE_ROOT, 'shared', 'playground-design-system'); const GENERATED_HEADER = '/* Code generated by sync-design-system.mjs; DO NOT EDIT. */\n'; function parseArgs(argv) { const args = { plugin: null, force: false }; for (const a of argv.slice(2)) { if (a === '--force') args.force = true; else if (a.startsWith('--')) { throw new Error(`Unknown flag: ${a}`); } else if (!args.plugin) { args.plugin = a; } else { throw new Error(`Unexpected positional arg: ${a}`); } } if (!args.plugin) { throw new Error('Missing plugin name. Usage: node scripts/sync-design-system.mjs [--force]'); } return args; } async function sha256(filePath) { const buf = await fs.readFile(filePath); return createHash('sha256').update(buf).digest('hex'); } async function walk(dir, base = dir) { const entries = await fs.readdir(dir, { withFileTypes: true }); const out = []; for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) { out.push(...(await walk(full, base))); } else if (e.isFile()) { out.push(path.relative(base, full)); } } return out; } async function readJsonIfExists(p) { try { return JSON.parse(await fs.readFile(p, 'utf8')); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } } async function detectDrift(targetDir, prevManifest) { if (!prevManifest || !prevManifest.files) return []; const drifted = []; for (const [rel, prevHash] of Object.entries(prevManifest.files)) { const tgt = path.join(targetDir, rel); try { const cur = await sha256(tgt); if (cur !== prevHash) drifted.push(rel); } catch (e) { if (e.code === 'ENOENT') drifted.push(`${rel} (missing)`); else throw e; } } return drifted; } async function injectGeneratedHeader(targetDir, files) { for (const rel of files) { if (!rel.endsWith('.css')) continue; const p = path.join(targetDir, rel); const content = await fs.readFile(p, 'utf8'); if (content.startsWith(GENERATED_HEADER)) continue; await fs.writeFile(p, GENERATED_HEADER + content, 'utf8'); } } async function buildManifest(targetDir, files, sourceCommit) { const fileHashes = {}; for (const rel of files.sort()) { fileHashes[rel] = await sha256(path.join(targetDir, rel)); } return { generated_by: 'scripts/sync-design-system.mjs', do_not_edit: true, source: 'shared/playground-design-system/', source_commit: sourceCommit, sync_date: new Date().toISOString(), file_count: files.length, files: fileHashes, }; } function getCurrentCommit() { try { return execSync('git rev-parse HEAD', { cwd: MARKETPLACE_ROOT, encoding: 'utf8', }).trim(); } catch { return 'unknown'; } } async function main() { const args = parseArgs(process.argv); const pluginDir = path.join(MARKETPLACE_ROOT, 'plugins', args.plugin); try { const stat = await fs.stat(pluginDir); if (!stat.isDirectory()) throw new Error('not a directory'); } catch { throw new Error(`Plugin directory not found: ${pluginDir}`); } try { await fs.access(SOURCE_DIR); } catch { throw new Error(`Source directory missing: ${SOURCE_DIR}`); } const targetDir = path.join(pluginDir, 'playground', 'vendor', 'playground-design-system'); const manifestPath = path.join(targetDir, 'MANIFEST.json'); const prevManifest = await readJsonIfExists(manifestPath); const drifted = await detectDrift(targetDir, prevManifest); if (drifted.length && !args.force) { console.error(`Refusing sync: ${drifted.length} vendored file(s) drifted from previous MANIFEST:`); for (const f of drifted) console.error(` - ${f}`); console.error('Pass --force to overwrite local changes.'); process.exit(2); } if (drifted.length && args.force) { console.warn(`--force: overwriting ${drifted.length} drifted file(s).`); } await fs.mkdir(path.dirname(targetDir), { recursive: true }); await fs.rm(targetDir, { recursive: true, force: true }); await fs.cp(SOURCE_DIR, targetDir, { recursive: true, force: true }); const files = await walk(targetDir); await injectGeneratedHeader(targetDir, files); const sourceCommit = getCurrentCommit(); const finalFiles = await walk(targetDir); const manifest = await buildManifest(targetDir, finalFiles, sourceCommit); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8'); console.log(`Synced shared/playground-design-system/ → plugins/${args.plugin}/playground/vendor/playground-design-system/`); console.log(` Files: ${manifest.file_count + 1} (incl. MANIFEST.json)`); console.log(` Source commit: ${sourceCommit}`); console.log(` Sync date: ${manifest.sync_date}`); } main().catch(err => { console.error(`Error: ${err.message}`); process.exit(1); });