1.1.2Updated a month ago
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;
}