From 78b32ccadad2bf700e6fbb8330f22a866806434a Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Fri, 16 Oct 2026 13:19:17 -0700 Subject: [PATCH 1/2] feat: Add JSDoc & generate TypeScript types --- .github/workflows/dev.yml | 3 + index.d.ts | 110 +++++++++++++++++++++++++++++ index.d.ts.map | 1 + index.js | 144 +++++++++++++++++++++++++++++++++++--- node.d.ts | 19 +++++ package-lock.json | 17 ++++- package.json | 10 ++- 7 files changed, 292 insertions(+), 12 deletions(-) create mode 100644 index.d.ts create mode 100644 index.d.ts.map create mode 100644 node.d.ts diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 99279de..703bf8d 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -45,6 +45,9 @@ jobs: - name: Check format run: npm run format-check + - name: Check types + run: npm run types-check + - name: Coveralls uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 with: diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2d91da6 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,110 @@ +/** + * @typedef {{ [extension: string]: * }} ExtensionMap Object mapping of extensions to some value. + */ +/** + * @typedef {object} LookupConfig Configuration used to lookup a file. + * @property {string | null} [path] The base path to search within. + * @property {string | null} [name] The name of the file without an extension. + * @property {string | Array | ExtensionMap | null} [extensions] The extensions the file being looked up may have. + * @property {string | null} [cwd] The current working directory that `path + name` is resolved against. + * @property {boolean | null} [findUp] Whether file lookup should recurse up the directory structure. + */ +/** + * @typedef {object} Found Information about the found file. + * @property {string} path The absolute path of the found file. + * @property {string | ExtensionMap} extension The extension that matched from configured extensions. + */ +/** + * Find a file given configuration and defaults describing lookup behavior. + * + * @param {LookupConfig | string} [pathObj] The user-provided configuration for file lookup. + * @param {LookupConfig} [defaultObj] Default configuration for file lookup. + * @returns {Found | null} Information about the found file or `null` if it cannot be found. + */ +export default function fined( + pathObj?: LookupConfig | string, + defaultObj?: LookupConfig, +): Found | null; +/** + * Object mapping of extensions to some value. + */ +export type ExtensionMap = { + [extension: string]: any; +}; +/** + * Configuration used to lookup a file. + */ +export type LookupConfig = { + /** + * The base path to search within. + */ + path?: string | null | undefined; + /** + * The name of the file without an extension. + */ + name?: string | null | undefined; + /** + * The extensions the file being looked up may have. + */ + extensions?: string | string[] | ExtensionMap | null | undefined; + /** + * The current working directory that `path + name` is resolved against. + */ + cwd?: string | null | undefined; + /** + * Whether file lookup should recurse up the directory structure. + */ + findUp?: boolean | null | undefined; +}; +/** + * Information about the found file. + */ +export type Found = { + /** + * The absolute path of the found file. + */ + path: string; + /** + * The extension that matched from configured extensions. + */ + extension: string | ExtensionMap; +}; +/** + * The expanded configuration after joining config & defaults and applying normalization. + */ +export type ExpandedConfig = { + /** + * The file basename to resolve. + */ + path: string; + /** + * The root directory to resolve from. + */ + basedir: string; + /** + * Whether file lookup should recurse up the directory structure. + */ + findUp: boolean; + /** + * Array of extensions to lookup + */ + extArr: string[]; + /** + * Object mapping of extensions if provided by user. + */ + extMap: ExtensionMap | null; +}; +/** + * Information about the found file without the {@link ExtensionMap} mapping. + */ +export type FoundFile = { + /** + * The absolute path of the found file. + */ + path: string; + /** + * The extension that matched from configured extensions. + */ + extension: string; +}; +//# sourceMappingURL=index.d.ts.map diff --git a/index.d.ts.map b/index.d.ts.map new file mode 100644 index 0000000..eeda9ee --- /dev/null +++ b/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAKA;;GAEG;AAEH;;;;;;;GAOG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AACH,wCAJW,YAAY,GAAG,MAAM,eACrB,YAAY,GACV,KAAK,GAAG,IAAI,CAKxB;;;;2BA5BY;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,GAAC,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAczB,MAAM;;;;eACN,MAAM,GAAG,YAAY;;;;;;;;;UAiBrB,MAAM;;;;aACN,MAAM;;;;YACN,OAAO;;;;YACP,MAAM,EAAE;;;;YACR,YAAY,GAAG,IAAI;;;;;;;;;UAKnB,MAAM;;;;eACN,MAAM"} \ No newline at end of file diff --git a/index.js b/index.js index af3cc1d..e8e67a5 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,60 @@ import os from "node:os"; import fs from "node:fs"; import path from "node:path"; +import process from "node:process"; +/** + * @typedef {{ [extension: string]: * }} ExtensionMap Object mapping of extensions to some value. + */ + +/** + * @typedef {object} LookupConfig Configuration used to lookup a file. + * @property {string | null} [path] The base path to search within. + * @property {string | null} [name] The name of the file without an extension. + * @property {string | Array | ExtensionMap | null} [extensions] The extensions the file being looked up may have. + * @property {string | null} [cwd] The current working directory that `path + name` is resolved against. + * @property {boolean | null} [findUp] Whether file lookup should recurse up the directory structure. + */ + +/** + * @typedef {object} Found Information about the found file. + * @property {string} path The absolute path of the found file. + * @property {string | ExtensionMap} extension The extension that matched from configured extensions. + */ + +/** + * Find a file given configuration and defaults describing lookup behavior. + * + * @param {LookupConfig | string} [pathObj] The user-provided configuration for file lookup. + * @param {LookupConfig} [defaultObj] Default configuration for file lookup. + * @returns {Found | null} Information about the found file or `null` if it cannot be found. + */ export default function fined(pathObj, defaultObj) { const expandedPath = expandPath(pathObj, defaultObj); return expandedPath ? findWithExpandedPath(expandedPath) : null; } +/** + * @typedef {object} ExpandedConfig The expanded configuration after joining config & defaults and applying normalization. + * @property {string} path The file basename to resolve. + * @property {string} basedir The root directory to resolve from. + * @property {boolean} findUp Whether file lookup should recurse up the directory structure. + * @property {string[]} extArr Array of extensions to lookup + * @property {ExtensionMap | null} extMap Object mapping of extensions if provided by user. + */ + +/** + * @typedef {object} FoundFile Information about the found file without the {@link ExtensionMap} mapping. + * @property {string} path The absolute path of the found file. + * @property {string} extension The extension that matched from configured extensions. + */ + +/** + * + * @param {LookupConfig | string} [pathObj] + * @param {LookupConfig} [defaultObj] + * @returns {ExpandedConfig | null} Information about the found file or `null` if not found. + */ function expandPath(pathObj, defaultObj) { if (isString(pathObj)) { pathObj = { path: pathObj }; @@ -14,6 +62,7 @@ function expandPath(pathObj, defaultObj) { pathObj = defaults(pathObj, defaultObj); + /** @type string */ let filePath; if (!isString(pathObj.path)) { return null; @@ -33,6 +82,7 @@ function expandPath(pathObj, defaultObj) { let extArr = createExtensionArray(pathObj.extensions); const extMap = createExtensionMap(pathObj.extensions); + /** @type string */ let basedir = isString(pathObj.cwd) ? pathObj.cwd.toString() : "."; basedir = path.resolve(expandTilde(basedir)); @@ -64,6 +114,12 @@ function expandPath(pathObj, defaultObj) { }; } +/** + * Resolves an expanded configuration to a file. + * + * @param {ExpandedConfig} expanded The expanded configuration to lookup. + * @returns {Found | null} Information about the found file or `null` if it cannot be found. + */ function findWithExpandedPath(expanded) { const found = expanded.findUp ? findUpFile(expanded.basedir, expanded.path, expanded.extArr) @@ -74,13 +130,24 @@ function findWithExpandedPath(expanded) { } if (expanded.extMap) { - found.extension = pick(expanded.extMap, found.extension); + return { + ...found, + extension: pick(expanded.extMap, found.extension), + }; } return found; } -function findFile(basedir, relpath, extArr) { - const noExtPath = path.resolve(basedir, relpath); +/** + * Non-recursive lookup of file + each extension. + * + * @param {string} basedir The directory to resolve against. + * @param {string} filename The filename (without extension) to resolve. + * @param {Array} extArr The extensions to resolve against the filename. + * @returns {FoundFile | null} Information about the found file or `null` if not found. + */ +function findFile(basedir, filename, extArr) { + const noExtPath = path.resolve(basedir, filename); for (const ext of extArr) { const filepath = noExtPath + ext; try { @@ -94,10 +161,18 @@ function findFile(basedir, relpath, extArr) { return null; } -function findUpFile(basedir, filepath, extArr) { +/** + * Recursive lookup of file + each extension. + * + * @param {string} basedir The directory to resolve against. + * @param {string} filename The filename (without extension) to resolve. + * @param {Array} extArr The extensions to resolve against the filename. + * @returns {FoundFile | null} Information about the found file or `null` if not found. + */ +function findUpFile(basedir, filename, extArr) { let lastdir; do { - const found = findFile(basedir, filepath, extArr); + const found = findFile(basedir, filename, extArr); if (found) { return found; } @@ -109,24 +184,37 @@ function findUpFile(basedir, filepath, extArr) { return null; } +/** + * Builds an array of extensions to match against. Will always contain `[""]` at minimum. + * + * @param {string | Array | ExtensionMap | null} [exts] The configured extensions. + * @returns {Array} All extensions to lookup. + */ function createExtensionArray(exts) { if (isString(exts)) { return [exts]; } if (Array.isArray(exts)) { - exts = exts.filter(isString); - return exts.length > 0 ? exts : [""]; + const filteredExts = exts.filter(isString); + return filteredExts.length > 0 ? filteredExts : [""]; } if (isObject(exts)) { - exts = Object.keys(exts); - return exts.length > 0 ? exts : [""]; + const extKeys = Object.keys(exts); + return extKeys.length > 0 ? extKeys : [""]; } return [""]; } +/** + * Builds an object mapping of extensions to match against if provided as configuration. + * Will always contain `{ "": null }` if an empty object is provided. + * + * @param {?} exts The configured extensions. + * @returns {ExtensionMap | null} Object mapping of extensions if provided or `null` otherwise. + */ function createExtensionMap(exts) { if (!isObject(exts)) { return null; @@ -139,6 +227,13 @@ function createExtensionMap(exts) { return exts; } +/** + * Creates a new object with values defaulted if not provided by `userObj`. + * + * @param {LookupConfig} [userObj] The user-provided object. + * @param {LookupConfig} [defaultObj] An object providing default values. + * @returns {LookupConfig} The new object with values defaulted. + */ function defaults(userObj, defaultObj) { if (!isObject(userObj)) { userObj = {}; @@ -155,19 +250,44 @@ function defaults(userObj, defaultObj) { return Object.assign({}, defaultObj, filteredObj); } +/** + * Creates a new object by extracting exactly the matched key, or an empty object if no match. + * + * @param {{ [s: string]: *; }} map The original object to pick from. + * @param {string} match The key to pick. + * @returns {{ [s: string]: *; }} The new object with only the matched key, otherwise an empty object if no match. + */ function pick(map, match) { const entries = Object.entries(map).filter(([key]) => key === match); return Object.fromEntries(entries); } +/** + * Determines in a value is a non-null, non-array object. + * + * @param {?} value The value to check. + * @returns {value is {}} `true` if the value is a non-null, non-array object. + */ function isObject(value) { return value && typeof value === "object" && !Array.isArray(value); } +/** + * Determines if an object has no keys. + * + * @param {object} object The object to check. + * @returns {boolean} `true` if the object has no keys, otherwise `false`. + */ function isEmpty(object) { return !Object.keys(object).length; } +/** + * Determines if a vale is a string or String object. + * + * @param {?} value The value to check. + * @returns {value is string | String} `true` if the value is a string or String object, otherwise `false`. + */ function isString(value) { if (typeof value === "string") { return true; @@ -184,6 +304,12 @@ function isString(value) { * Copyright (c) 2015 Jon Schlinkert. * Licensed under the MIT license. */ +/** + * Expands `~` and `~+` at the start of a file path to home or pwd respectively. + * + * @param {string} filepath The file path to expand. + * @returns {string} A file path with `~` and `~+` expanded. + */ function expandTilde(filepath) { const home = os.homedir(); diff --git a/node.d.ts b/node.d.ts new file mode 100644 index 0000000..fbfd90f --- /dev/null +++ b/node.d.ts @@ -0,0 +1,19 @@ +declare module "node:process" { + function cwd(): string; +} + +declare module "node:os" { + function homedir(): string; +} + +declare module "node:path" { + function join(p1: string, p2: string): string; + function resolve(p1: string, p2?: string): string; + function parse(p1: string): { root: string; ext: string }; + function isAbsolute(p1: string): boolean; + function dirname(p1: string): string; +} + +declare module "node:fs" { + function statSync(p1: string): asserts p1; +} diff --git a/package-lock.json b/package-lock.json index c5eea55..5b3e116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "devDependencies": { "eslint": "10.11.0", "eslint-config-gulp": "7.0.0", - "prettier": "3.9.6" + "prettier": "3.9.6", + "typescript": "6.0.3" }, "engines": { "node": ">= 22.15.0" @@ -994,6 +995,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index bef4212..e96d617 100644 --- a/package.json +++ b/package.json @@ -18,22 +18,28 @@ }, "type": "module", "main": "./index.js", + "types": "./index.d.ts", "exports": "./index.js", "files": [ "index.js", + "index.d.ts", + "index.d.ts.map", "LICENSE" ], "scripts": { "format-check": "prettier . --check", "format-write": "prettier . --write", "lint": "eslint .", - "test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=test/lcov.info \"test/*.js\"" + "test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=test/lcov.info \"test/*.js\"", + "types-check": "tsc index.js --checkJs --noEmit --types ./node.d.ts", + "types-write": "tsc index.js --checkJs --declaration --declarationMap --emitDeclarationOnly --types ./node.d.ts" }, "dependencies": {}, "devDependencies": { "eslint": "10.11.0", "eslint-config-gulp": "7.0.0", - "prettier": "3.9.6" + "prettier": "3.9.6", + "typescript": "6.0.3" }, "keywords": [ "find", From 149feb756300d5e5f38c43fe847727c726f68b9c Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Fri, 16 Oct 2026 13:46:41 -0700 Subject: [PATCH 2/2] cleanup generated file --- index.d.ts | 54 -------------------------------------------------- index.d.ts.map | 2 +- index.js | 48 ++++++++++++++++---------------------------- 3 files changed, 18 insertions(+), 86 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2d91da6..e3f9081 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,19 +1,3 @@ -/** - * @typedef {{ [extension: string]: * }} ExtensionMap Object mapping of extensions to some value. - */ -/** - * @typedef {object} LookupConfig Configuration used to lookup a file. - * @property {string | null} [path] The base path to search within. - * @property {string | null} [name] The name of the file without an extension. - * @property {string | Array | ExtensionMap | null} [extensions] The extensions the file being looked up may have. - * @property {string | null} [cwd] The current working directory that `path + name` is resolved against. - * @property {boolean | null} [findUp] Whether file lookup should recurse up the directory structure. - */ -/** - * @typedef {object} Found Information about the found file. - * @property {string} path The absolute path of the found file. - * @property {string | ExtensionMap} extension The extension that matched from configured extensions. - */ /** * Find a file given configuration and defaults describing lookup behavior. * @@ -69,42 +53,4 @@ export type Found = { */ extension: string | ExtensionMap; }; -/** - * The expanded configuration after joining config & defaults and applying normalization. - */ -export type ExpandedConfig = { - /** - * The file basename to resolve. - */ - path: string; - /** - * The root directory to resolve from. - */ - basedir: string; - /** - * Whether file lookup should recurse up the directory structure. - */ - findUp: boolean; - /** - * Array of extensions to lookup - */ - extArr: string[]; - /** - * Object mapping of extensions if provided by user. - */ - extMap: ExtensionMap | null; -}; -/** - * Information about the found file without the {@link ExtensionMap} mapping. - */ -export type FoundFile = { - /** - * The absolute path of the found file. - */ - path: string; - /** - * The extension that matched from configured extensions. - */ - extension: string; -}; //# sourceMappingURL=index.d.ts.map diff --git a/index.d.ts.map b/index.d.ts.map index eeda9ee..0cd518f 100644 --- a/index.d.ts.map +++ b/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAKA;;GAEG;AAEH;;;;;;;GAOG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AACH,wCAJW,YAAY,GAAG,MAAM,eACrB,YAAY,GACV,KAAK,GAAG,IAAI,CAKxB;;;;2BA5BY;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,GAAC,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAczB,MAAM;;;;eACN,MAAM,GAAG,YAAY;;;;;;;;;UAiBrB,MAAM;;;;aACN,MAAM;;;;YACN,OAAO;;;;YACP,MAAM,EAAE;;;;YACR,YAAY,GAAG,IAAI;;;;;;;;;UAKnB,MAAM;;;;eACN,MAAM"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,wCAJW,YAAY,GAAG,MAAM,eACrB,YAAY,GACV,KAAK,GAAG,IAAI,CAKxB;;;;2BAGY;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,GAAC,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAczB,MAAM;;;;eACN,MAAM,GAAG,YAAY"} \ No newline at end of file diff --git a/index.js b/index.js index e8e67a5..22e188a 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,18 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; +/** + * Find a file given configuration and defaults describing lookup behavior. + * + * @param {LookupConfig | string} [pathObj] The user-provided configuration for file lookup. + * @param {LookupConfig} [defaultObj] Default configuration for file lookup. + * @returns {Found | null} Information about the found file or `null` if it cannot be found. + */ +export default function fined(pathObj, defaultObj) { + const expandedPath = expandPath(pathObj, defaultObj); + return expandedPath ? findWithExpandedPath(expandedPath) : null; +} + /** * @typedef {{ [extension: string]: * }} ExtensionMap Object mapping of extensions to some value. */ @@ -23,37 +35,11 @@ import process from "node:process"; */ /** - * Find a file given configuration and defaults describing lookup behavior. + * Creates an expanded config by joining config & defaults and applying normalization. * * @param {LookupConfig | string} [pathObj] The user-provided configuration for file lookup. * @param {LookupConfig} [defaultObj] Default configuration for file lookup. - * @returns {Found | null} Information about the found file or `null` if it cannot be found. - */ -export default function fined(pathObj, defaultObj) { - const expandedPath = expandPath(pathObj, defaultObj); - return expandedPath ? findWithExpandedPath(expandedPath) : null; -} - -/** - * @typedef {object} ExpandedConfig The expanded configuration after joining config & defaults and applying normalization. - * @property {string} path The file basename to resolve. - * @property {string} basedir The root directory to resolve from. - * @property {boolean} findUp Whether file lookup should recurse up the directory structure. - * @property {string[]} extArr Array of extensions to lookup - * @property {ExtensionMap | null} extMap Object mapping of extensions if provided by user. - */ - -/** - * @typedef {object} FoundFile Information about the found file without the {@link ExtensionMap} mapping. - * @property {string} path The absolute path of the found file. - * @property {string} extension The extension that matched from configured extensions. - */ - -/** - * - * @param {LookupConfig | string} [pathObj] - * @param {LookupConfig} [defaultObj] - * @returns {ExpandedConfig | null} Information about the found file or `null` if not found. + * @returns Information about the found file or `null` if not found. */ function expandPath(pathObj, defaultObj) { if (isString(pathObj)) { @@ -117,7 +103,7 @@ function expandPath(pathObj, defaultObj) { /** * Resolves an expanded configuration to a file. * - * @param {ExpandedConfig} expanded The expanded configuration to lookup. + * @param {NonNullable>} expanded The expanded configuration to lookup. * @returns {Found | null} Information about the found file or `null` if it cannot be found. */ function findWithExpandedPath(expanded) { @@ -144,7 +130,7 @@ function findWithExpandedPath(expanded) { * @param {string} basedir The directory to resolve against. * @param {string} filename The filename (without extension) to resolve. * @param {Array} extArr The extensions to resolve against the filename. - * @returns {FoundFile | null} Information about the found file or `null` if not found. + * @returns Information about the found file or `null` if not found. */ function findFile(basedir, filename, extArr) { const noExtPath = path.resolve(basedir, filename); @@ -167,7 +153,7 @@ function findFile(basedir, filename, extArr) { * @param {string} basedir The directory to resolve against. * @param {string} filename The filename (without extension) to resolve. * @param {Array} extArr The extensions to resolve against the filename. - * @returns {FoundFile | null} Information about the found file or `null` if not found. + * @returns Information about the found file or `null` if not found. */ function findUpFile(basedir, filename, extArr) { let lastdir;