import { walkSync } from "https://deno.land/std@0.133.0/fs/walk.ts";
import { join, relative } from "https://deno.land/std@0.224.0/path/mod.ts";
import type { I_File } from "$modules/zip.ts";
import { globToRegExp } from "https://deno.land/std@0.133.0/path/glob.ts";
import { existsSync } from "jsr:@std/fs/exists";
interface ScanOptions {
maxDepth?: number
includeFolders?: boolean
}
interface FileCacheEntry {
timestamp: number
files: I_File[]
}
const file_cache: Map<string, FileCacheEntry> = new Map()
const FIVE_MINUTES = 1000 * 60 * 5;
export const ScanFiles = (path: string, { maxDepth, includeFolders }: ScanOptions = {}) => {
const cache_entry_name = `${path}:${includeFolders ? 'all' : 'files'}:${maxDepth || '-1'}`;
cache_lookup: if(file_cache.has(cache_entry_name)) {
const entry = file_cache.get(cache_entry_name)!;
if(Date.now() - entry.timestamp > FIVE_MINUTES) break cache_lookup;
return entry.files;
}
let gitignore_entries: RegExp[] = [];
try {
gitignore_entries = Deno.readTextFileSync(join(path, '.gitignore'))?.trim()?.split(/[\r\n]+/)?.map(line => [globToRegExp(line), globToRegExp('/' + line)])?.flat() ?? [];
} catch(_) { /**/ }
const files: I_File[] = [];
if(!existsSync(path)) return files;
for(const walkEntry of walkSync(path, { maxDepth, includeDirs: includeFolders === true })) {
const { path: full_path, name } = walkEntry;
const relative_path = relative(path, full_path);
if(relative_path.startsWith('.git/')) continue;
if(relative_path.match(/\bnode_modules\b/)) continue;
if(relative_path == 'deno.lock') continue;
if(gitignore_entries.some(pattern => pattern.test(relative_path))) continue;
files.push({
name,
full_path,
relative_path,
is_directory: walkEntry.isDirectory
});
}
file_cache.set(cache_entry_name, {
timestamp: Date.now(),
files
})
return files;
}
// region Gitignore Applicator
import ignore from "npm:ignore";
import { posix } from "node:path";
type IgnoreSource = {
dirFromRoot: string;
patterns: string[];
};
function loadGitIgnoresSimple(root: string): IgnoreSource[] {
const sources: IgnoreSource[] = [];
for(const entry of walkSync(root, { includeDirs: false, followSymlinks: false })) {
if (entry.name !== ".gitignore") continue;
const dir = entry.path.substring(0, entry.path.length - "/.gitignore".length);
const text = Deno.readTextFileSync(entry.path);
const patterns = text
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));
const dirFromRoot = posix.relative(root.replaceAll("\\", "/"), dir.replaceAll("\\", "/"));
sources.push({ dirFromRoot, patterns });
}
return sources;
}
function buildIgnore(extraHardExcludes: string[] = []) {
const ig = (ignore as any)();
ig.add([".git/", "node_modules/", "deno.lock", ...extraHardExcludes]);
return ig;
}
function addGitIgnoreSources(ig: ignore.Ignore, sources: IgnoreSource[]) {
for (const { dirFromRoot, patterns } of sources) {
const base = dirFromRoot === "." ? "" : dirFromRoot + "/";
for (const raw of patterns) {
if (raw.startsWith("/")) {
ig.add(raw); // anchored to root
} else if (raw.startsWith("!")) {
const pat = raw.slice(1);
ig.add("!" + (pat.includes("/") ? base + pat : base + pat));
} else {
ig.add((raw.includes("/") ? base + raw : base + raw));
}
}
}
}
export function* walkPublishable(root = Deno.cwd()) {
root = root.replaceAll("\\", "/");
const ig = buildIgnore([]);
const sources = loadGitIgnoresSimple(root);
addGitIgnoreSources(ig, sources);
// Walk everything and filter with the ignore matcher.
// Give the matcher POSIX-style relative paths.
for (const entry of walkSync(root, { includeFiles: true, includeDirs: false, followSymlinks: false })) {
const rel = posix.relative(root, entry.path.replaceAll("\\", "/"));
if (rel === "" || ig.ignores(rel)) continue;
yield { path: entry.path, relative: rel, entry };
}
}
export const ScanPublishableFiles = (path: string) => {
const files: I_File[] = [];
if(!existsSync(path)) return files;
for(const walkEntry of walkPublishable(path)) {
const { path: full_path, entry: { name } } = walkEntry;
const relative_path = relative(path, full_path);
files.push({
name,
full_path,
relative_path,
is_directory: walkEntry.entry.isDirectory
});
}
return files;
}