seogeneratorserver/tools/pack-lib.cjs
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

156 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
// ---- CLI options -----------------------------------------------------------
function getArg(name, def) {
const i = args.findIndex(a => a === `--${name}` || a.startsWith(`--${name}=`));
if (i === -1) return def;
const v = args[i].includes('=') ? args[i].split('=').slice(1).join('=') : args[i + 1];
return v ?? true;
}
const ROOT = process.cwd();
const DIR = path.resolve(ROOT, getArg('dir', 'lib'));
const OUT = path.resolve(ROOT, getArg('out', 'code.js'));
const ENTRY= getArg('entry', 'lib/test-manual.js');
const ORDER= getArg('order', 'topo'); // 'topo' | 'alpha' | 'entry-first'
// ---- Helpers ---------------------------------------------------------------
function readFileSafe(p) {
try { return fs.readFileSync(p, 'utf8'); }
catch (e) { return null; }
}
function* walk(dir) {
const list = fs.readdirSync(dir, { withFileTypes: true });
for (const d of list) {
const p = path.join(dir, d.name);
if (d.isDirectory()) yield* walk(p);
else if (d.isFile() && d.name.endsWith('.js')) yield p;
}
}
// Normalise un chemin importé (./x, ../x, sans extension)
function resolveImport(fromFile, spec) {
if (!spec) return null;
if (spec.startsWith('node:')) return null; // builtin
if (!spec.startsWith('./') && !spec.startsWith('../')) return null; // externe: ignorer
let target = path.resolve(path.dirname(fromFile), spec);
if (!/\.js$/i.test(target)) {
const withJs = `${target}.js`;
if (fs.existsSync(withJs)) target = withJs;
else if (fs.existsSync(path.join(target, 'index.js'))) target = path.join(target, 'index.js');
}
return target;
}
// Très simple parseur dimports/require (suffisant pour 95 % des cas)
const RE_IMPORT_1 = /\bimport\s+[^'"]*\s+from\s+['"]([^'"]+)['"]/g; // import X from '...'
const RE_IMPORT_2 = /\bimport\s+['"]([^'"]+)['"]/g; // import '...'
const RE_REQUIRE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g; // require('...')
function extractDeps(absPath, code) {
const specs = new Set();
let m;
for (RE_IMPORT_1.lastIndex = 0; (m = RE_IMPORT_1.exec(code)); ) specs.add(m[1]);
for (RE_IMPORT_2.lastIndex = 0; (m = RE_IMPORT_2.exec(code)); ) specs.add(m[1]);
for (RE_REQUIRE.lastIndex = 0; (m = RE_REQUIRE.exec(code)); ) specs.add(m[1]);
const deps = [];
for (const s of specs) {
const r = resolveImport(absPath, s);
if (r) deps.push(r);
}
return deps;
}
function asciiBox(title, width = 70) {
const clean = title.length > width - 6 ? title.slice(0, width - 9) + '…' : title;
const line = '─'.repeat(width - 2);
const pad = width - 6 - clean.length;
return [
`/*`,
`${line}`,
`${clean}${' '.repeat(Math.max(0, pad))}`,
`${line}`,
`*/`
].join('\n');
}
// ---- Collecte des fichiers -------------------------------------------------
if (!fs.existsSync(DIR) || !fs.statSync(DIR).isDirectory()) {
console.error(`[pack-lib] Dossier introuvable: ${DIR}`);
process.exit(1);
}
const allFiles = Array.from(walk(DIR)).map(p => path.resolve(p));
// ---- Graphe de dépendances -------------------------------------------------
/**
* graph: Map<file, { deps: string[], code: string }>
*/
const graph = new Map();
for (const f of allFiles) {
const code = readFileSafe(f) ?? '';
const deps = extractDeps(f, code).filter(d => allFiles.includes(d));
graph.set(f, { deps, code });
}
// ---- Ordonnancement --------------------------------------------------------
function topoSort(graph, entryAbs) {
const visited = new Set();
const temp = new Set();
const order = [];
const visit = (n) => {
if (visited.has(n)) return;
if (temp.has(n)) return; // cycle: on ignore pour éviter boucle
temp.add(n);
const { deps = [] } = graph.get(n) || {};
for (const d of deps) if (graph.has(d)) visit(d);
temp.delete(n);
visited.add(n);
order.push(n);
};
if (entryAbs && graph.has(entryAbs)) visit(entryAbs);
for (const n of graph.keys()) visit(n);
return order;
}
let order;
const entryAbs = path.resolve(ROOT, ENTRY);
if (ORDER === 'alpha') {
order = [...graph.keys()].sort((a, b) => a.localeCompare(b));
} else if (ORDER === 'entry-first') {
order = [entryAbs, ...[...graph.keys()].filter(f => f !== entryAbs).sort((a,b)=>a.localeCompare(b))]
.filter(Boolean);
} else {
order = topoSort(graph, entryAbs);
}
// ---- Écriture du bundle ----------------------------------------------------
const header = `/*
code.js — bundle concaténé
Généré: ${new Date().toISOString()}
Source: ${path.relative(ROOT, DIR)}
Fichiers: ${order.length}
Ordre: ${ORDER}
*/\n\n`;
let out = header;
for (const file of order) {
const rel = path.relative(ROOT, file).replace(/\\/g, '/');
const box = asciiBox(`File: ${rel}`);
const code = (graph.get(file)?.code) ?? '';
out += `${box}\n\n${code}\n\n`;
}
// Sassure que le dossier cible existe
fs.writeFileSync(OUT, out, 'utf8');
const relOut = path.relative(ROOT, OUT).replace(/\\/g, '/');
console.log(`[pack-lib] OK -> ${relOut} (${order.length} fichiers)`);