@bscotch/pathy
Advanced tools
Comparing version 2.4.1 to 2.5.0
export * from './lib/pathy.js'; | ||
import { Pathy } from './lib/pathy.js'; | ||
import { Pathy, type PathyOptions } from './lib/pathy.js'; | ||
/** | ||
* Shorthand for `new Pathy(...)`. | ||
*/ | ||
export declare function pathy<FileContent = unknown>(path?: string, cwd?: string): Pathy<FileContent>; | ||
export declare function pathy<FileContent = unknown>(path?: string | Pathy, cwd?: string | Pathy): Pathy<FileContent>; | ||
export declare function pathy<FileContent = unknown>(path?: string | Pathy, options?: PathyOptions<FileContent>): Pathy<FileContent>; | ||
//# sourceMappingURL=index.d.ts.map |
export * from './lib/pathy.js'; | ||
import { Pathy } from './lib/pathy.js'; | ||
/** | ||
* Shorthand for `new Pathy(...)`. | ||
*/ | ||
export function pathy(path, cwd) { | ||
return new Pathy(path, cwd); | ||
export function pathy(path, cwdOrOptions) { | ||
return new Pathy(path, cwdOrOptions); | ||
} | ||
//# sourceMappingURL=index.js.map |
@@ -6,4 +6,14 @@ /// <reference types="node" /> | ||
import { PathyStatic } from './pathy.static.js'; | ||
import type { PathyFindParentOptions, PathyInfix, PathyListChildrenOptions, PathyOrString, PathyReadOptions, PathyWriteOptions } from './pathy.types.js'; | ||
import type { PathyFindParentOptions, PathyInfix, PathyListChildrenOptions, PathyOrString, PathyReadOptions, PathyReadOutput, PathyWriteOptions } from './pathy.types.js'; | ||
export type { PathyListChildrenOptions, PathyOrString, PathyReadOptions, PathyWriteOptions, } from './pathy.types.js'; | ||
export interface PathyOptions<T> { | ||
cwd?: PathyOrString; | ||
/** | ||
* Optionally provide a Zod-compatible schema/validator, | ||
* which will be used on read/write to file. | ||
*/ | ||
validator?: { | ||
parse: (v: unknown) => T; | ||
}; | ||
} | ||
/** | ||
@@ -43,3 +53,11 @@ * An **immutable** utility class for reducing the cognitive load of | ||
readonly workingDirectory: string; | ||
/** | ||
* A zod schema or other compatible validator, which will be used | ||
* during file read/write if provided. | ||
*/ | ||
readonly validator?: { | ||
parse: (v: unknown) => FileContent; | ||
}; | ||
constructor(path?: PathyOrString | string[], cwd?: PathyOrString); | ||
constructor(path?: PathyOrString | string[], options?: PathyOptions<FileContent>); | ||
get directory(): string; | ||
@@ -49,2 +67,11 @@ get name(): string; | ||
get extname(): string; | ||
withValidator<T>(validator: { | ||
parse: (v: unknown) => T; | ||
}): Pathy<T>; | ||
/** | ||
* Check if a path has the given extension, ignoring case | ||
* and ensuring the `.` is present even if not provided in | ||
* the args (since that's always confusing). | ||
*/ | ||
hasExtension(ext: string | string[]): boolean; | ||
parseInfix(): PathyInfix; | ||
@@ -105,3 +132,5 @@ /** | ||
*/ | ||
read<Parsed = FileContent, Fallback = never, Encoding extends BufferEncoding | false = 'utf8'>(options?: PathyReadOptions<Parsed, Fallback, Encoding>): Promise<Parsed | Fallback>; | ||
read<Parsed = FileContent, Fallback = never, Encoding extends BufferEncoding | false = 'utf8', Schema extends { | ||
parse: (content: unknown) => Parsed; | ||
} | never = never>(options?: PathyReadOptions<Parsed, Fallback, Encoding, Schema>): Promise<PathyReadOutput<Parsed, Fallback, Schema>>; | ||
/** | ||
@@ -160,2 +189,3 @@ * Write to file at the current path, automatically serializing | ||
listChildren(): Promise<Pathy[]>; | ||
listChildrenSync(): Pathy[]; | ||
/** | ||
@@ -191,3 +221,4 @@ * Copy this file/directory to another location. | ||
append(...paths: string[]): Pathy; | ||
changeExtension(from: string[] | string, to: string): Pathy<unknown>; | ||
changeExtension<T>(to: string): Pathy<T>; | ||
changeExtension<T>(from: string[] | string, to: string): Pathy<T>; | ||
/** | ||
@@ -194,0 +225,0 @@ * Check if this path matches another path after both |
import { __decorate, __metadata } from "tslib"; | ||
import { arrayWrapped } from '@bscotch/utility'; | ||
import { ok } from 'assert'; | ||
@@ -42,4 +43,15 @@ import fs from 'fs'; | ||
workingDirectory; | ||
constructor(path = process.cwd(), cwd = process.cwd()) { | ||
/** | ||
* A zod schema or other compatible validator, which will be used | ||
* during file read/write if provided. | ||
*/ | ||
validator; | ||
constructor(path = process.cwd(), cwdOrOptions) { | ||
super(); | ||
const cwd = Pathy.isStringOrPathy(cwdOrOptions) | ||
? cwdOrOptions | ||
: cwdOrOptions?.cwd || | ||
(Pathy.isPathy(cwdOrOptions) && cwdOrOptions.workingDirectory) || | ||
(Pathy.isPathy(path) && path.workingDirectory) || | ||
process.cwd(); | ||
this.workingDirectory = Pathy.normalize(cwd); | ||
@@ -62,2 +74,13 @@ this.normalized = Pathy.normalize(Array.isArray(path) ? Pathy.join(...path) : path); | ||
} | ||
withValidator(validator) { | ||
return new Pathy(this, { cwd: this.workingDirectory, validator }); | ||
} | ||
/** | ||
* Check if a path has the given extension, ignoring case | ||
* and ensuring the `.` is present even if not provided in | ||
* the args (since that's always confusing). | ||
*/ | ||
hasExtension(ext) { | ||
return arrayWrapped(ext).some((e) => PathyStatic.hasExtension(this, e)); | ||
} | ||
parseInfix() { | ||
@@ -149,3 +172,6 @@ return Pathy.parseInfix(this); | ||
async read(options) { | ||
return await Pathy.read(this, options); | ||
return await Pathy.read(this, { | ||
schema: this.validator, | ||
...options, | ||
}); | ||
} | ||
@@ -157,3 +183,6 @@ /** | ||
async write(data, options) { | ||
return await Pathy.write(this, data, options); | ||
return await Pathy.write(this, data, { | ||
schema: this.validator, | ||
...options, | ||
}); | ||
} | ||
@@ -241,2 +270,9 @@ /** | ||
} | ||
listChildrenSync() { | ||
ok(this.isDirectorySync(), `${this.normalized} is not a directory`); | ||
return fse.readdirSync(this.absolute).map((entry) => { | ||
const path = this.join(entry); | ||
return new Pathy(path, this.workingDirectory); | ||
}); | ||
} | ||
/** | ||
@@ -282,3 +318,3 @@ * Copy this file/directory to another location. | ||
async findInParents(basename, options) { | ||
return await Pathy.findParentPath(this, basename, options); | ||
return (await Pathy.findParentPath(this, basename, options)); | ||
} | ||
@@ -302,7 +338,14 @@ findInParentsSync(basename, options) { | ||
changeExtension(from, to) { | ||
const fromPattern = (Array.isArray(from) ? from : [from]) | ||
.map((p) => (p.startsWith('.') ? p : `.${p}`)) | ||
.join('|'); | ||
to = to.startsWith('.') ? to : `.${to}`; | ||
const newPath = this.absolute.replace(new RegExp(`(${fromPattern})$`), to); | ||
let newPath = this.absolute; | ||
const ensureDot = (ext) => (ext[0] === '.' ? ext : `.${ext}`); | ||
const newExtension = ensureDot(to || from); | ||
if (to === undefined) { | ||
ok(typeof from === 'string', 'Function signature is wrong. If only one argument is provided it must be a string.'); | ||
// Just replace the last .ext, whatever it is | ||
newPath = newPath.replace(/\.[^/.]+$/, newExtension); | ||
} | ||
else { | ||
const fromPattern = arrayWrapped(from).map(ensureDot).join('|'); | ||
newPath = newPath.replace(new RegExp(`(${fromPattern})$`), newPath); | ||
} | ||
return new Pathy(newPath, this.workingDirectory); | ||
@@ -309,0 +352,0 @@ } |
@@ -5,3 +5,3 @@ /// <reference types="node" /> | ||
import type { Pathy } from './pathy.js'; | ||
import type { PathyFindParentOptions, PathyInfix, PathyListChildrenOptions, PathyOrString, PathyReadOptions, PathyWriteOptions } from './pathy.types.js'; | ||
import type { PathyFindParentOptions, PathyInfix, PathyListChildrenOptions, PathyOrString, PathyReadOptions, PathyReadOutput, PathySchema, PathyWriteOptions } from './pathy.types.js'; | ||
/** | ||
@@ -31,2 +31,8 @@ * A base class providing static functions for {@link Pathy}. | ||
/** | ||
* Check if a path has the given extension, ignoring case | ||
* and ensuring the `.` is present even if not provided in | ||
* the args (since that's always confusing). | ||
*/ | ||
static hasExtension(path: PathyOrString, ext: string): boolean; | ||
/** | ||
* Instead of parsing a file as `{name}{.ext}`, | ||
@@ -116,7 +122,7 @@ * also parse out the infix if present: `{name}{.infix}{.ext}`. | ||
*/ | ||
static findParentPath(from: Pathy, basename: string, options?: PathyFindParentOptions): Promise<Pathy | undefined>; | ||
static findParentPath<T = unknown>(from: Pathy, basename: string, options?: PathyFindParentOptions<T>): Promise<Pathy | undefined>; | ||
static findParentPathSync(from: Pathy, basename: string, options?: PathyFindParentOptions): Pathy | undefined; | ||
/** | ||
* For file extensions that indicate identical or similar | ||
* content serilalization, get a normalized extension. This | ||
* content serialization, get a normalized extension. This | ||
* is useful for simplifying parser/serializer lookups. | ||
@@ -141,5 +147,5 @@ * | ||
*/ | ||
static read<Parsed = unknown, Fallback = never, Encoding extends BufferEncoding | false = 'utf8'>(filepath: PathyOrString, options?: PathyReadOptions<Parsed, Fallback, Encoding>): Promise<Parsed | Fallback>; | ||
static read<Parsed = unknown, Fallback = never, Encoding extends BufferEncoding | false = 'utf8', Schema extends PathySchema<Parsed> | never = never>(filepath: PathyOrString, options?: PathyReadOptions<Parsed, Fallback, Encoding, Schema>): Promise<PathyReadOutput<Parsed, Fallback, Schema>>; | ||
static get defaultIgnoredDirs(): readonly ["node_modules", ".git", ".hg", ".svn", ".idea", ".vscode", ".vscode-test"]; | ||
} | ||
//# sourceMappingURL=pathy.static.d.ts.map |
import { __decorate, __metadata } from "tslib"; | ||
import { arrayIsDuplicates, stringIsMatch } from '@bscotch/utility'; | ||
import { arrayIsDuplicates, Sequential, stringIsMatch } from '@bscotch/utility'; | ||
import { ok } from 'assert'; | ||
@@ -10,29 +10,2 @@ import fse from 'fs-extra'; | ||
import { trace } from './pathy.lib.js'; | ||
class WriteLocker { | ||
static writing = new Map(); | ||
static async lock(file) { | ||
if (WriteLocker.writing.has(file)) { | ||
await WriteLocker.waitForUnlock(file); | ||
} | ||
WriteLocker.writing.set(file, []); | ||
} | ||
static unlock(file) { | ||
const callbacks = WriteLocker.writing.get(file); | ||
for (const callback of callbacks || []) { | ||
callback(); | ||
} | ||
WriteLocker.writing.delete(file); | ||
} | ||
static waitForUnlock(file) { | ||
if (WriteLocker.writing.has(file)) { | ||
return new Promise((resolve) => { | ||
const callback = () => { | ||
resolve(undefined); | ||
}; | ||
WriteLocker.writing.get(file).push(callback); | ||
}); | ||
} | ||
return Promise.resolve(); | ||
} | ||
} | ||
/** | ||
@@ -93,2 +66,12 @@ * A base class providing static functions for {@link Pathy}. | ||
/** | ||
* Check if a path has the given extension, ignoring case | ||
* and ensuring the `.` is present even if not provided in | ||
* the args (since that's always confusing). | ||
*/ | ||
static hasExtension(path, ext) { | ||
const normalized = PathyStatic.normalize(path); | ||
const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`; | ||
return normalized.toLowerCase().endsWith(normalizedExt.toLowerCase()); | ||
} | ||
/** | ||
* Instead of parsing a file as `{name}{.ext}`, | ||
@@ -272,2 +255,5 @@ * also parse out the infix if present: `{name}{.infix}{.ext}`. | ||
const _children = []; | ||
const addChild = (child) => { | ||
_children.push((options?.transform ? options.transform(child) : child)); | ||
}; | ||
const ignoredDirs = options.unignoreAll | ||
@@ -312,2 +298,8 @@ ? [] | ||
// Handle dirs first! | ||
const checkDir = async () => { | ||
if (options?.includeDirs) { | ||
addChild(child); | ||
} | ||
return await innerLoop(child, depth + 1); | ||
}; | ||
if (await child.isDirectory()) { | ||
@@ -319,3 +311,3 @@ if (ignoredDirs.includes(child.basename)) { | ||
if ((await options.filter?.(child, children)) !== false) { | ||
await innerLoop(child, depth + 1); | ||
await checkDir(); | ||
} | ||
@@ -325,7 +317,7 @@ } | ||
if (!stringIsMatch(child.basename, options.excludePatterns)) { | ||
await innerLoop(child, depth + 1); | ||
await checkDir(); | ||
} | ||
} | ||
else { | ||
await innerLoop(child, depth + 1); | ||
await checkDir(); | ||
} | ||
@@ -335,2 +327,5 @@ continue; | ||
// Then is a file | ||
if (options?.includeDirs === 'only') { | ||
continue; | ||
} | ||
if (includeExtension) { | ||
@@ -361,3 +356,3 @@ if (!includeExtension(child)) { | ||
} | ||
_children.push((options?.transform ? options.transform(child) : child)); | ||
addChild(child); | ||
if (options?.onInclude) { | ||
@@ -369,3 +364,3 @@ await options.onInclude(child); | ||
await innerLoop(dir); | ||
return _children; | ||
return await Promise.all(_children); | ||
} | ||
@@ -409,3 +404,3 @@ /** | ||
* For file extensions that indicate identical or similar | ||
* content serilalization, get a normalized extension. This | ||
* content serialization, get a normalized extension. This | ||
* is useful for simplifying parser/serializer lookups. | ||
@@ -447,2 +442,3 @@ * | ||
static async write(filepath, data, options) { | ||
data = options?.schema ? options.schema.parse(data) : data; | ||
filepath = PathyStatic.ensureAbsolute(filepath); | ||
@@ -486,5 +482,3 @@ const extension = PathyStatic.normalizedExtension(filepath); | ||
} | ||
await WriteLocker.lock(filepath); | ||
await fse.writeFile(filepath, serialized); | ||
WriteLocker.unlock(filepath); | ||
} | ||
@@ -498,8 +492,12 @@ /** | ||
const hasFallback = options && 'fallback' in options; | ||
const clean = (fileContent) => options?.schema | ||
? options.schema.parse(fileContent) | ||
: fileContent; | ||
ok(doesExist || hasFallback, `File does not exist: ${filepath}`); | ||
const fileInfo = doesExist ? await PathyStatic.stat(filepath) : undefined; | ||
ok(!doesExist || !fileInfo.isDirectory(), `Expected file, found directory: ${filepath}`); | ||
const isDirectory = doesExist && fileInfo.isDirectory(); | ||
ok(!isDirectory, `Expected file, found directory: ${filepath}`); | ||
if (!doesExist) { | ||
ok(hasFallback, `No file found at: ${filepath}`); | ||
return options.fallback; | ||
return clean(options.fallback); | ||
} | ||
@@ -509,9 +507,8 @@ const doNotParse = options?.parse === false; | ||
// Handle the binary case | ||
await WriteLocker.waitForUnlock(filepath.toString()); | ||
const binary = await fse.readFile(filepath.toString()); | ||
if (options?.encoding === false) { | ||
if (customParser) { | ||
return await customParser(binary); | ||
return clean(await customParser(binary)); | ||
} | ||
return binary; | ||
return clean(binary); | ||
} | ||
@@ -522,6 +519,6 @@ // Handle the encoded text case | ||
if (customParser) { | ||
return await customParser(decoded); | ||
return clean(await customParser(decoded)); | ||
} | ||
else if (doNotParse) { | ||
return decoded; | ||
return clean(decoded); | ||
} | ||
@@ -531,14 +528,15 @@ // Attempt to infer the parser to use | ||
if (!fileType) { | ||
return decoded; | ||
return clean(decoded); | ||
} | ||
try { | ||
if (fileType == 'yaml') { | ||
return yaml.parse(decoded); | ||
return clean(yaml.parse(decoded)); | ||
} | ||
else if (fileType == 'json') { | ||
return json5.parse(decoded); | ||
return clean(json5.parse(decoded)); | ||
} | ||
} | ||
catch (err) { | ||
throw new Error(`Unable to parse file: ${filepath}`); | ||
console.error(err); | ||
throw new Error(`Unable to parse file: ${filepath}`, { cause: err }); | ||
} | ||
@@ -577,2 +575,19 @@ throw new Error(`Impossible outcome: should have matched filetype.`); | ||
], PathyStatic, "isParentOf", null); | ||
__decorate([ | ||
Sequential({ | ||
subqueueBy: (p) => PathyStatic.normalize(p), | ||
}), | ||
__metadata("design:type", Function), | ||
__metadata("design:paramtypes", [Object, Object, Object]), | ||
__metadata("design:returntype", Promise) | ||
], PathyStatic, "write", null); | ||
__decorate([ | ||
Sequential({ | ||
shareQueueWith: 'write', | ||
subqueueBy: (p) => PathyStatic.normalize(p), | ||
}), | ||
__metadata("design:type", Function), | ||
__metadata("design:paramtypes", [Object, Object]), | ||
__metadata("design:returntype", Promise) | ||
], PathyStatic, "read", null); | ||
//# sourceMappingURL=pathy.static.js.map |
@@ -5,2 +5,7 @@ /// <reference types="node" /> | ||
type PathyReadEncoding = BufferEncoding | false; | ||
export type PathySchema<Out> = { | ||
parse: (input: unknown) => Out; | ||
}; | ||
export type PathyReadOutput<Parsed, Fallback, Schema extends PathySchema<Parsed> | never> = never extends Schema ? never extends Fallback ? Parsed : // NO SCHEMA, YES FALLBACK | ||
Parsed | Fallback : Parsed; | ||
/** | ||
@@ -10,5 +15,5 @@ * When Pathy reads the file at its current location, | ||
* | ||
* Options for the {@link Pathy.read} method. | ||
* Options for the `Pathy.read()` method. | ||
*/ | ||
export interface PathyReadOptions<Parsed, Fallback, Encoding extends PathyReadEncoding> { | ||
export interface PathyReadOptions<Parsed, Fallback, Encoding extends PathyReadEncoding, Schema extends PathySchema<Parsed> | never> { | ||
/** | ||
@@ -58,2 +63,7 @@ * The file encoding. If this is false, | ||
fallback?: Fallback; | ||
/** | ||
* If provided, the read file (or fallback) will be parsed by | ||
* this schema (Zod-compatible). | ||
*/ | ||
schema?: Schema; | ||
} | ||
@@ -105,2 +115,9 @@ export interface PathyWriteOptions { | ||
onClobber?: 'error' | 'overwrite' | 'skip'; | ||
/** | ||
* If provided, the data will be parsed by | ||
* this schema (Zod-compatible) before being written. | ||
*/ | ||
schema?: { | ||
parse: (input: unknown) => unknown; | ||
}; | ||
} | ||
@@ -214,3 +231,3 @@ /** | ||
*/ | ||
transform?: (path: Pathy) => As; | ||
transform?: (path: Pathy) => As extends Promise<infer U> ? U | Promise<U> : As | Promise<As>; | ||
/** | ||
@@ -235,2 +252,11 @@ * Optionally limit the number of found paths, | ||
maxDepth?: number; | ||
/** | ||
* By default only files are returned when conditions are met. | ||
* Set this to `true` to also return the directories that matched | ||
* (noting that directories and files are handled differently -- | ||
* e.g. "includeExtension" only applies to files). | ||
* | ||
* If set to `'only'`, only directories will be returned. | ||
*/ | ||
includeDirs?: boolean | 'only'; | ||
} | ||
@@ -237,0 +263,0 @@ export type PathyOrString = string | Pathy; |
{ | ||
"name": "@bscotch/pathy", | ||
"version": "2.4.1", | ||
"version": "2.5.0", | ||
"type": "module", | ||
@@ -14,3 +14,3 @@ "exports": { | ||
"dependencies": { | ||
"@bscotch/utility": "6.5.0", | ||
"@bscotch/utility": "6.6.0", | ||
"fs-extra": "10.1.0", | ||
@@ -27,6 +27,7 @@ "json5": "^2.2.1", | ||
"chai": "^4.3.6", | ||
"mocha": "^10.0.0", | ||
"mocha": "^10.1.0", | ||
"rimraf": "^3.0.2", | ||
"type-fest": "^3.0.0", | ||
"typescript": "4.9.1-beta" | ||
"typescript": "4.9.1-beta", | ||
"zod": "3.19.1" | ||
}, | ||
@@ -41,4 +42,4 @@ "publishConfig": { | ||
"test:dev": "mocha --config ../../config/.mocharc.cjs --forbid-only=false --parallel=false --timeout=9999999999", | ||
"watch": "tsc --build --watch" | ||
"watch": "tsc-x --build --watch" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
104488
1714
10
99
+ Added@bscotch/utility@6.6.0(transitive)
- Removed@bscotch/utility@6.5.0(transitive)
Updated@bscotch/utility@6.6.0