diff --git a/src/async.js b/src/async.js index e445936..ccd760f 100644 --- a/src/async.js +++ b/src/async.js @@ -1,20 +1,25 @@ import { join, resolve } from 'path'; -import { readdir, stat } from 'fs'; +import { lstat, readdir, realpath } from 'fs'; import { promisify } from 'util'; -const toStats = promisify(stat); -const toRead = promisify(readdir); +const real = promisify(realpath); +const toStats = promisify(lstat); +const list = promisify(readdir); -export async function totalist(dir, callback, pre='') { - dir = resolve('.', dir); - await toRead(dir).then(arr => { +export async function totalist(dir, callback, prefix, cache) { + cache = cache || new Set; + dir = await real(resolve('.', dir)); + if (cache.has(dir)) return; + + cache.add(dir); + await list(dir).then(arr => { return Promise.all( arr.map(str => { let abs = join(dir, str); return toStats(abs).then(stats => { return stats.isDirectory() - ? totalist(abs, callback, join(pre, str)) - : callback(join(pre, str), abs, stats) + ? totalist(abs, callback, join(prefix || '', str), cache) + : callback(join(prefix || '', str), abs, stats) }); }) ); diff --git a/src/sync.d.ts b/src/sync.d.ts index 5f2d6d5..04f929e 100644 --- a/src/sync.d.ts +++ b/src/sync.d.ts @@ -1,3 +1,3 @@ import { Stats } from 'fs'; export type Caller = (relPath: string, absPath: string, stats: Stats) => any; -export function totalist(dir: string, callback: Caller, prefix?: string): void; +export function totalist(dir: string, callback: Caller, prefix?: string, cache?: Set): void; diff --git a/src/sync.js b/src/sync.js index e751c3f..7f14c8d 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1,15 +1,18 @@ import { join, resolve } from 'path'; -import { readdirSync, statSync } from 'fs'; +import { lstatSync, readdirSync, realpathSync } from 'fs'; -export function totalist(dir, callback, pre='') { - dir = resolve('.', dir); - let arr = readdirSync(dir); - let i=0, abs, stats; - for (; i < arr.length; i++) { - abs = join(dir, arr[i]); - stats = statSync(abs); - stats.isDirectory() - ? totalist(abs, callback, join(pre, arr[i])) - : callback(join(pre, arr[i]), abs, stats); +export function totalist(dir, callback, prefix, cache) { + cache = cache || new Set; + dir = realpathSync(resolve('.', dir)); + if (!cache.has(dir)) { + cache.add(dir); + let i=0, abs, stats; + let arr = readdirSync(dir); + for (; i < arr.length; i++) { + stats = lstatSync(abs = join(dir, arr[i])); + stats.isDirectory() + ? totalist(abs, callback, join(prefix || '', arr[i]), cache) + : callback(join(prefix || '', arr[i]), abs, stats); + } } } diff --git a/test/async.js b/test/async.js index d08f417..c064456 100644 --- a/test/async.js +++ b/test/async.js @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { join, normalize } from 'path'; @@ -52,4 +53,32 @@ test('(async) usage :: absolute', async () => { }); +test('usage :: symbolic directory', async () => { + let symlink = join(fixtures, 'symlink'); + fs.symlinkSync(fixtures, symlink); + + try { + let count = 0; + let items = {}; + + await totalist(fixtures, (name, abs, stats) => { + count++; + assert.not(stats.isDirectory(), 'file is not a directory'); + assert.ok(abs.startsWith(fixtures), '~> `abs` is absolute'); + assert.not(name.startsWith(fixtures), '~> `name` is NOT absolute'); + assert.is(stats.isSymbolicLink(), abs === symlink); + + items[abs] = (items[abs] || 0) + 1; + }); + + const keys = Object.keys(items); + assert.is(count, 7, 'saw 7 files total'); + assert.is(keys.length, 7, '~> unique file paths'); + assert.ok(keys.every(k => items[k] === 1), '~> saw each item once'); + } finally { + fs.unlinkSync(symlink); + } +}); + + test.run(); diff --git a/test/sync.js b/test/sync.js index a88c8dc..3a9714c 100644 --- a/test/sync.js +++ b/test/sync.js @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { join, normalize } from 'path'; @@ -51,4 +52,32 @@ test('usage :: absolute', () => { }); +test('usage :: symbolic directory', () => { + let symlink = join(fixtures, 'symlink'); + fs.symlinkSync(fixtures, symlink); + + try { + let count = 0; + let items = {}; + + totalist(fixtures, (name, abs, stats) => { + count++; + assert.not(stats.isDirectory(), 'file is not a directory'); + assert.ok(abs.startsWith(fixtures), '~> `abs` is absolute'); + assert.not(name.startsWith(fixtures), '~> `name` is NOT absolute'); + assert.is(stats.isSymbolicLink(), abs === symlink); + + items[abs] = (items[abs] || 0) + 1; + }); + + const keys = Object.keys(items); + assert.is(count, 7, 'saw 7 files total'); + assert.is(keys.length, 7, '~> unique file paths'); + assert.ok(keys.every(k => items[k] === 1), '~> saw each item once'); + } finally { + fs.unlinkSync(symlink); + } +}); + + test.run();