feat(marketplace): add sync-design-system.mjs script
Vendors shared/playground-design-system/ into a plugin's playground/vendor/playground-design-system/ tree so each plugin stays standalone (no marketplace-rot dependency at runtime). Features: - Generates MANIFEST.json with SHA-256 per file, source commit hash, sync date - Drift detection: refuses overwrite if vendored file changed since last sync - --force flag to override drift - Injects "DO NOT EDIT" header into copied CSS files - Pure Node.js, zero npm deps (uses fs.cp from Node 16.7+) Usage: node scripts/sync-design-system.mjs <plugin-name> [--force]
This commit is contained in:
parent
f1fecf39b8
commit
f4aa1ed58f
1 changed files with 182 additions and 0 deletions
182
scripts/sync-design-system.mjs
Normal file
182
scripts/sync-design-system.mjs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 <plugin-name> [--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 <plugin-name> [--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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue