#!/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 */ 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)`);