upd
This commit is contained in:
83
src/dir-listing.js
Normal file
83
src/dir-listing.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'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)
|
||||
* All paths are relative to ROOT_DIR and use forward slashes.
|
||||
*/
|
||||
export function listDir(dir, { seek = 0, limit = 50, recursive = 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) {
|
||||
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) };
|
||||
}
|
||||
Reference in New Issue
Block a user