134 lines
4.8 KiB
JavaScript
134 lines
4.8 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parse } from 'acorn';
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const rootDir = path.resolve(scriptDir, '..');
|
|
const outputDir = path.join(rootDir, 'output');
|
|
const excludedDirs = new Set(['.git', 'data', 'node_modules', 'output']);
|
|
const functions = [];
|
|
|
|
function collectJavaScriptFiles(dir) {
|
|
const files = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.isDirectory() && excludedDirs.has(entry.name)) continue;
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...collectJavaScriptFiles(fullPath));
|
|
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function inferredName(node, parent) {
|
|
if (node.id?.name) return node.id.name;
|
|
if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
return parent.id.name;
|
|
}
|
|
if (parent?.type === 'AssignmentExpression') {
|
|
return parent.left.name ?? parent.left.property?.name ?? 'assigned';
|
|
}
|
|
if (parent?.type === 'Property' || parent?.type === 'MethodDefinition') {
|
|
return parent.key.name ?? parent.key.value ?? 'method';
|
|
}
|
|
if (parent?.type === 'ExportDefaultDeclaration') return 'default';
|
|
return 'anonymous';
|
|
}
|
|
|
|
function visit(node, parent, source, relativePath) {
|
|
if (!node || typeof node !== 'object') return;
|
|
|
|
if (node.type === 'FunctionDeclaration'
|
|
|| node.type === 'FunctionExpression'
|
|
|| node.type === 'ArrowFunctionExpression') {
|
|
const method = (parent?.type === 'MethodDefinition'
|
|
|| (parent?.type === 'Property' && parent.method))
|
|
? parent
|
|
: node;
|
|
const start = method.start;
|
|
const end = method.end;
|
|
const startLine = source.slice(0, start).split('\n').length;
|
|
const endLine = source.slice(0, end).split('\n').length;
|
|
const name = inferredName(node, parent);
|
|
let signature = source.slice(start, node.body.start).replace(/\s+/g, ' ').trim();
|
|
if (method === node
|
|
&& name !== 'anonymous'
|
|
&& node.type !== 'FunctionDeclaration'
|
|
&& parent?.type !== 'ExportDefaultDeclaration') {
|
|
signature = `${name} = ${signature}`;
|
|
}
|
|
functions.push({
|
|
source: relativePath,
|
|
start,
|
|
end,
|
|
startLine,
|
|
endLine,
|
|
name,
|
|
signature,
|
|
code: source.slice(start, end),
|
|
});
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(node)) {
|
|
if (key === 'start' || key === 'end' || key === 'loc') continue;
|
|
if (Array.isArray(value)) {
|
|
for (const child of value) visit(child, node, source, relativePath);
|
|
} else if (value && typeof value === 'object') {
|
|
visit(value, node, source, relativePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
const sourceFiles = collectJavaScriptFiles(rootDir).sort();
|
|
for (const filePath of sourceFiles) {
|
|
const source = fs.readFileSync(filePath, 'utf8');
|
|
const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
let ast;
|
|
try {
|
|
ast = parse(source, {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
allowHashBang: true,
|
|
});
|
|
} catch (error) {
|
|
throw new Error(`Could not parse ${relativePath}: ${error.message}`, { cause: error });
|
|
}
|
|
visit(ast, null, source, relativePath);
|
|
}
|
|
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
for (const entry of fs.readdirSync(outputDir, { withFileTypes: true })) {
|
|
if (entry.isFile() && /^function-.*\.js$/.test(entry.name)) {
|
|
fs.unlinkSync(path.join(outputDir, entry.name));
|
|
}
|
|
}
|
|
|
|
functions.sort((a, b) => a.source.localeCompare(b.source) || a.start - b.start);
|
|
const nameCounts = new Map();
|
|
for (const fn of functions) {
|
|
const sourceSlug = fn.source.replace(/\.js$/, '').replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
const nameSlug = fn.name.replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
const baseName = `function-${sourceSlug}-L${fn.startLine}-${nameSlug}`;
|
|
const sequence = (nameCounts.get(baseName) ?? 0) + 1;
|
|
nameCounts.set(baseName, sequence);
|
|
|
|
const outputName = `${baseName}-${sequence}.js`;
|
|
const contents = [
|
|
`// Source: ${fn.source} (lines ${fn.startLine}-${fn.endLine})`,
|
|
fn.code,
|
|
'',
|
|
].join('\n');
|
|
fs.writeFileSync(path.join(outputDir, outputName), contents);
|
|
}
|
|
|
|
const index = functions
|
|
.map(fn => `${fn.source}:${fn.startLine}\t${fn.signature}`)
|
|
.join('\n');
|
|
fs.writeFileSync(path.join(outputDir, 'functions.txt'), `${index}\n`);
|
|
|
|
console.log(`Extracted ${functions.length} functions from ${sourceFiles.length} JavaScript files into ${path.relative(rootDir, outputDir)}/`);
|