/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scripts/embed-presets.js
248 строк
9 KB
Starolat Sergei
feat: удаление встроенных пресетов (--prune/--remove в embed-presets, purge stale built-ins в LayoutPresetService v1.6.1)
14 июл 2026, 10:29
14 июл 2026, 10:29
a7de332
Код
Авторство
О чём код?
#!/usr/bin/env node /** * @fileoverview Embed presets into assets/presets/ as built-in (dev tool). * @version 1.1.0 * * Takes a dump produced by scripts/dump-presets.browser.js (all presets from * the running app, built-in and user-created) and writes each preset into * assets/presets/ with isBuiltIn = true, then regenerates index.json. * * Usage: * node scripts/embed-presets.js <dump.json> [--prune] [--remove <presetId|name>]... * * --prune Also DELETE built-in preset files that are not present in the * dump (the dump becomes the single source of truth). Use with * care — deletion is immediate. * --remove Exclude a preset from the dump before embedding, by presetId or * exact name (repeatable). Combined with --prune this deletes the * corresponding built-in file — a one-command preset removal * without editing the dump JSON by hand. * * Input formats accepted: * - dump: { format: 'deepdive-presets-dump', presets: [...] } * - array: [ preset, preset, ... ] * - single: { format: 'deepdive-level-preset', preset: {...} } or raw preset * * Notes: * - Files are matched by presetId: an existing file with the same presetId * is updated in place (keeps its filename). * - If content changed but preset.version is unchanged, patch version is * bumped — otherwise the app keeps the stale localStorage copy on init * (LayoutPresetService merges defaults only when version differs). * - index.json is regenerated via scripts/generate-preset-index.js. */ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const PRESETS_DIR = path.join(__dirname, '..', 'assets', 'presets'); /** Cyrillic → latin transliteration for file slugs. */ const TRANSLIT = { а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'c', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya', }; /** * @param {string} name * @returns {string} */ function slugify(name) { const lower = String(name || '').toLowerCase(); let out = ''; for (const ch of lower) { if (TRANSLIT[ch] !== undefined) out += TRANSLIT[ch]; else if (/[a-z0-9]/.test(ch)) out += ch; else if (ch === ' ' || ch === '-' || ch === '_') out += '-'; } return out.replace(/-+/g, '-').replace(/^-|-$/g, ''); } /** * @param {string} version * @returns {string} */ function bumpPatch(version) { const m = String(version || '').match(/^(\d+)\.(\d+)\.(\d+)$/); if (m) return `${m[1]}.${m[2]}.${Number(m[3]) + 1}`; return `${version || '1.0.0'}.1`; } /** * Read a preset file and return its inner preset object. * @param {string} filePath * @returns {any|null} */ function readPreset(filePath) { try { const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); return data.preset || data; } catch { return null; } } function main() { // ---- Parse args: <dump.json> [--prune] [--remove <id|name>]... ---- const args = process.argv.slice(2); /** @type {string[]} */ const removeTargets = []; let dumpPath = null; let prune = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--prune') { prune = true; } else if (arg === '--remove') { const value = args[++i]; if (!value) { console.error('[embed-presets] --remove requires a value (presetId or name)'); process.exit(1); } removeTargets.push(value); } else if (arg.startsWith('--remove=')) { removeTargets.push(arg.slice('--remove='.length)); } else if (!dumpPath) { dumpPath = arg; } else { console.error(`[embed-presets] Unknown argument: ${arg}`); process.exit(1); } } if (!dumpPath) { console.error('Usage: node scripts/embed-presets.js <dump.json> [--prune] [--remove <presetId|name>]...'); process.exit(1); } if (!fs.existsSync(dumpPath)) { console.error(`[embed-presets] File not found: ${dumpPath}`); process.exit(1); } // ---- Parse input (dump / array / single envelope / raw preset) ---- const input = JSON.parse(fs.readFileSync(dumpPath, 'utf-8')); /** @type {any[]} */ let presets; if (Array.isArray(input)) presets = input; else if (Array.isArray(input.presets)) presets = input.presets; else if (input.preset) presets = [input.preset]; else presets = [input]; // ---- Apply --remove exclusions (by presetId or exact name) ---- if (removeTargets.length > 0) { const remaining = new Set(removeTargets); presets = presets.filter((p) => { const hit = p && (remaining.has(p.presetId) || remaining.has(p.name)); if (hit) { remaining.delete(p.presetId); remaining.delete(p.name); console.log(` removed from dump: ${p.name} (${p.presetId})`); } return !hit; }); for (const target of remaining) { console.warn(`[embed-presets] --remove target not found in dump: ${target}`); } } // ---- Index existing files by presetId ---- /** @type {Map<string, {file: string, preset: any}>} */ const existing = new Map(); for (const entry of fs.readdirSync(PRESETS_DIR, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith('.json') || entry.name === 'index.json') continue; const p = readPreset(path.join(PRESETS_DIR, entry.name)); if (p && p.presetId) existing.set(p.presetId, { file: entry.name, preset: p }); } /** @type {Set<string>} filenames that will exist after this run */ const usedNames = new Set([...existing.values()].map(v => v.file)); /** @type {Set<string>} filenames written/updated in this run */ const writtenNames = new Set(); let written = 0; let skipped = 0; for (const preset of presets) { if (!preset || typeof preset !== 'object') { console.warn('[embed-presets] Skipping invalid entry (not an object)'); skipped++; continue; } if (!preset.presetId || !preset.name || !Array.isArray(preset.levels) || preset.levels.length === 0) { console.warn(`[embed-presets] Skipping invalid preset: ${preset.name || preset.presetId || '(no name)'}`); skipped++; continue; } preset.isBuiltIn = true; // Target filename: reuse existing file for this presetId, else slug const prev = existing.get(preset.presetId); let filename; if (prev) { filename = prev.file; // Bump version when content changed but version did not — the app // merges built-ins over localStorage copies only on version mismatch. // isBuiltIn is excluded from comparison: it is a runtime flag and // is not persisted in built-in files, so it must not alone cause // a version bump. const strip = (/** @type {any} */ p) => { const c = JSON.parse(JSON.stringify(p)); delete c.isBuiltIn; return JSON.stringify(c); }; const changed = strip(prev.preset) !== strip(preset); if (changed && prev.preset.version === preset.version) { preset.version = bumpPatch(preset.version); console.log(` version bumped: ${preset.name} -> ${preset.version}`); } } else { let slug = slugify(preset.name) || `preset-${String(preset.presetId).slice(0, 8)}`; let candidate = `${slug}.json`; let n = 2; while (usedNames.has(candidate)) { candidate = `${slug}-${n++}.json`; } filename = candidate; } usedNames.add(filename); const envelope = { format: 'deepdive-level-preset', version: '2.0.0', exportedAt: new Date().toISOString(), preset, }; fs.writeFileSync( path.join(PRESETS_DIR, filename), JSON.stringify(envelope, null, 2) + '\n', 'utf-8', ); console.log(` ${prev ? 'updated' : 'added'}: ${filename} (${preset.name})`); writtenNames.add(filename); written++; } // ---- Prune: delete built-in files whose presets are absent from the dump ---- let pruned = 0; if (prune) { for (const { file, preset } of existing.values()) { if (writtenNames.has(file)) continue; fs.unlinkSync(path.join(PRESETS_DIR, file)); console.log(` pruned: ${file} (${preset.name || '?'})`); pruned++; } } console.log(`[embed-presets] Written: ${written}, skipped: ${skipped}, pruned: ${pruned}`); if (written === 0 && pruned === 0) { console.warn('[embed-presets] Nothing changed, index.json left unchanged'); return; } // ---- Regenerate index.json ---- execFileSync(process.execPath, [path.join(__dirname, 'generate-preset-index.js')], { stdio: 'inherit' }); } main();