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>
156 lines
5.3 KiB
JavaScript
156 lines
5.3 KiB
JavaScript
#!/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 d’imports/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`;
|
||
}
|
||
|
||
// S’assure 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)`);
|
||
|