120 lines
4.4 KiB
JavaScript
120 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Directory listing helpers — .gitignore-aware listing with pagination.
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import ignore from 'ignore';
|
|
import { ROOT_DIR } from './config.js';
|
|
|
|
/**
|
|
* Build an `ignore` filter that respects all .gitignore files found between
|
|
* ROOT_DIR and `dir` (inclusive), plus a hardcoded node_modules rule.
|
|
* Paths fed to ig.ignores() must be relative to ROOT_DIR and use forward slashes.
|
|
*/
|
|
export function buildIgnoreFilter(dir) {
|
|
const ig = ignore().add('node_modules/');
|
|
// Collect .gitignore files from ROOT_DIR down to dir
|
|
const rel = path.relative(ROOT_DIR, dir);
|
|
const parts = rel === '' ? [] : rel.split(path.sep);
|
|
const ancestors = [ROOT_DIR];
|
|
for (let i = 0; i < parts.length; i++) {
|
|
ancestors.push(path.join(ROOT_DIR, ...parts.slice(0, i + 1)));
|
|
}
|
|
for (const ancestor of ancestors) {
|
|
const gi = path.join(ancestor, '.gitignore');
|
|
if (fs.existsSync(gi)) {
|
|
try {
|
|
const rules = fs.readFileSync(gi, 'utf8');
|
|
// Rules in a nested .gitignore are relative to that directory;
|
|
// prefix them so they work relative to ROOT_DIR.
|
|
const prefix = path.relative(ROOT_DIR, ancestor).replace(/\\/g, '/');
|
|
const prefixed = rules
|
|
.split('\n')
|
|
.filter(l => l.trim() && !l.startsWith('#'))
|
|
.map(l => (prefix ? `${prefix}/${l.trim()}` : l.trim()));
|
|
ig.add(prefixed);
|
|
} catch { /* ignore read errors */ }
|
|
}
|
|
}
|
|
return ig;
|
|
}
|
|
|
|
/**
|
|
* List entries of a directory, optionally recursive, respecting .gitignore.
|
|
* Returns { entries: Array<{path,type,size}>, total, truncated }
|
|
* - type: 'd' for directory, 'f' for file
|
|
* - size: file size in bytes (null for directories)
|
|
* Dot-directories (names starting with '.', e.g. .git) are skipped unless
|
|
* `includeDotDirs` is true.
|
|
* All paths are relative to ROOT_DIR and use forward slashes.
|
|
*/
|
|
export function listDir(dir, { seek = 0, limit = 50, recursive = false, includeDotDirs = false } = {}) {
|
|
const ig = buildIgnoreFilter(dir);
|
|
const allEntries = [];
|
|
|
|
const collect = (current) => {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const e of entries) {
|
|
// Skip dot-directories (e.g. .git) by default.
|
|
if (e.isDirectory() && e.name.startsWith('.') && !includeDotDirs) continue;
|
|
const full = path.join(current, e.name);
|
|
const rel = path.relative(ROOT_DIR, full).replace(/\\/g, '/');
|
|
if (ig.ignores(e.isDirectory() ? rel + '/' : rel)) continue;
|
|
if (e.isDirectory()) {
|
|
allEntries.push({ path: rel + '/', type: 'd', size: null });
|
|
if (recursive) collect(full);
|
|
} else {
|
|
let size = null;
|
|
try { size = fs.statSync(full).size; } catch { /* skip */ }
|
|
allEntries.push({ path: rel, type: 'f', size });
|
|
}
|
|
}
|
|
};
|
|
collect(dir);
|
|
|
|
const total = allEntries.length;
|
|
const sliced = allEntries.slice(seek, seek + limit);
|
|
const truncated = total - (seek + sliced.length);
|
|
return { entries: sliced, total, truncated: Math.max(0, truncated) };
|
|
}
|
|
|
|
/**
|
|
* Recursively walk the *files* under `dir`, honoring .gitignore rules
|
|
* (including nested .gitignore files) plus the hardcoded node_modules rule.
|
|
* Returns an array of { abs, rel } where `rel` is relative to ROOT_DIR using
|
|
* forward slashes. Symlinks are skipped to avoid loops.
|
|
*/
|
|
export function walkFiles(dir) {
|
|
const results = [];
|
|
const walk = (current) => {
|
|
// Rebuild per directory so nested .gitignore files apply to their subtree.
|
|
const ig = buildIgnoreFilter(current);
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const e of entries) {
|
|
const full = path.join(current, e.name);
|
|
const rel = path.relative(ROOT_DIR, full).replace(/\\/g, '/');
|
|
if (ig.ignores(e.isDirectory() ? rel + '/' : rel)) continue;
|
|
if (e.isDirectory()) {
|
|
walk(full);
|
|
} else if (e.isFile()) {
|
|
results.push({ abs: full, rel });
|
|
}
|
|
}
|
|
};
|
|
walk(dir);
|
|
return results;
|
|
}
|