From 1ac5e8b5067de895b81a6f83d259dd109dece6ba Mon Sep 17 00:00:00 2001 From: dominikg Date: Fri, 1 Sep 2023 14:47:23 +0200 Subject: [PATCH] refactor(find): use callback for fs.stat and more caching --- .changeset/mighty-dogs-argue.md | 5 ++ packages/tsconfck/src/cache.js | 9 --- packages/tsconfck/src/find.js | 93 +++++++++++++-------------- packages/tsconfck/src/parse-native.js | 11 ++-- packages/tsconfck/src/parse.js | 12 ++-- packages/tsconfck/src/public.d.ts | 2 +- packages/tsconfck/src/util.js | 40 ++++++++---- 7 files changed, 90 insertions(+), 82 deletions(-) create mode 100644 .changeset/mighty-dogs-argue.md diff --git a/.changeset/mighty-dogs-argue.md b/.changeset/mighty-dogs-argue.md new file mode 100644 index 0000000..831cc75 --- /dev/null +++ b/.changeset/mighty-dogs-argue.md @@ -0,0 +1,5 @@ +--- +'tsconfck': minor +--- + +perf(find): switch to fs.stat callback for async and increase cache usage diff --git a/packages/tsconfck/src/cache.js b/packages/tsconfck/src/cache.js index 9b677cf..8428968 100644 --- a/packages/tsconfck/src/cache.js +++ b/packages/tsconfck/src/cache.js @@ -67,15 +67,6 @@ export class TSConfckCache { result.then((parsed) => this.#parsed.set(file, parsed)).catch(() => this.#parsed.delete(file)); } - /** - * @internal - * @private - * @param file - */ - deleteParseResult(file) { - this.#parsed.delete(file); - } - /** * @internal * @private diff --git a/packages/tsconfck/src/find.js b/packages/tsconfck/src/find.js index e595e2b..e862997 100644 --- a/packages/tsconfck/src/find.js +++ b/packages/tsconfck/src/find.js @@ -1,5 +1,5 @@ -import path from 'path'; -import { promises as fs } from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; /** * find the closest tsconfig.json file @@ -15,61 +15,58 @@ export async function find(filename, options) { return cache.getTSConfigPath(dir); } const root = options?.root ? path.resolve(options.root) : null; - - /** @type {(result: string|null)=>void}*/ - let resolvePathPromise; + /** @type {((result: string|null,err?: ErrnoException)=>void)} */ + let done; /** @type {Promise | string | null}*/ - const pathPromise = new Promise((r) => { - resolvePathPromise = r; - }); - while (dir) { - if (cache) { - if (cache.hasTSConfigPath(dir)) { - const cached = cache.getTSConfigPath(dir); - if (cached.then) { - cached.then(resolvePathPromise); - } else { - resolvePathPromise(/**@type {string|null} */ (cached)); - } - return pathPromise; - } else { - cache.setTSConfigPath(dir, pathPromise); - } - } - const tsconfig = await tsconfigInDir(dir); - if (tsconfig) { - resolvePathPromise(tsconfig); - return pathPromise; - } else { - const parent = path.dirname(dir); - if (root === dir || parent === dir) { - // reached root - break; + const promise = new Promise((resolve, reject) => { + done = (result, err) => { + if (err) { + reject(err); + } else if (result === null) { + reject(`no tsconfig file found for ${filename}`); } else { - dir = parent; + resolve(result); } - } - } - resolvePathPromise(null); - throw new Error(`no tsconfig file found for ${filename}`); + }; + }); + findUp(dir, promise, done, options?.cache, root); + return promise; } /** - * test if tsconfig exists in dir + * * @param {string} dir - * @returns {Promise} + * @param {Promise} promise + * @param {((result: string|null,err?: ErrnoException)=>void)} done + * @param {import('./cache.js').TSConfckCache} [cache] + * @param {string} [root] */ -async function tsconfigInDir(dir) { +function findUp(dir, promise, done, cache, root) { const tsconfig = path.join(dir, 'tsconfig.json'); - try { - const stat = await fs.stat(tsconfig); - if (stat.isFile() || stat.isFIFO()) { - return tsconfig; - } - } catch (e) { - // ignore does not exist error - if (e.code !== 'ENOENT') { - throw e; + if (cache) { + if (cache.hasTSConfigPath(dir)) { + const cached = cache.getTSConfigPath(dir); + if (cached.then) { + cached.then(done).catch((err) => done(null, err)); + } else { + done(/**@type {string|null} */ (cached)); + } + } else { + cache.setTSConfigPath(dir, promise); } } + fs.stat(tsconfig, (err, stats) => { + if (stats && (stats.isFile() || stats.isFIFO())) { + done(tsconfig); + } else if (err?.code !== 'ENOENT') { + done(null, err); + } else { + let parent; + if (root === dir || (parent = path.dirname(dir)) === dir) { + done(null); + } else { + findUp(parent, promise, done, cache, root); + } + } + }); } diff --git a/packages/tsconfck/src/parse-native.js b/packages/tsconfck/src/parse-native.js index 8be5e1b..4467f33 100644 --- a/packages/tsconfck/src/parse-native.js +++ b/packages/tsconfck/src/parse-native.js @@ -19,6 +19,7 @@ import { findNative } from './find-native.js'; * @throws {TSConfckParseNativeError} */ export async function parseNative(filename, options) { + /** @type {import('./cache.js').TSConfckCache} */ const cache = options?.cache; if (cache?.hasParseResult(filename)) { return cache.getParseResult(filename); @@ -27,9 +28,9 @@ export async function parseNative(filename, options) { if (options?.resolveWithEmptyIfConfigNotFound) { try { - tsconfigFile = await resolveTSConfig(filename); + tsconfigFile = await resolveTSConfig(filename, cache); if (!tsconfigFile) { - tsconfigFile = await findNative(filename); + tsconfigFile = await findNative(filename, options); } } catch (e) { const notFoundResult = { @@ -41,16 +42,16 @@ export async function parseNative(filename, options) { return notFoundResult; } } else { - tsconfigFile = await resolveTSConfig(filename); + tsconfigFile = await resolveTSConfig(filename, cache); if (!tsconfigFile) { - tsconfigFile = await findNative(filename); + tsconfigFile = await findNative(filename, options); } } /** @type {import('./public.d.ts').TSConfckParseNativeResult} */ let result; if (cache?.hasParseResult(tsconfigFile)) { - result = cache.getParseResult(tsconfigFile); + result = await cache.getParseResult(tsconfigFile); } else { const ts = await loadTS(); result = await parseFile(tsconfigFile, ts, options); diff --git a/packages/tsconfck/src/parse.js b/packages/tsconfck/src/parse.js index a8c1aeb..d84a716 100644 --- a/packages/tsconfck/src/parse.js +++ b/packages/tsconfck/src/parse.js @@ -14,12 +14,13 @@ import { /** * parse the closest tsconfig.json file * - * @param {string} filename - path to a tsconfig.json or a .ts source file (absolute or relative to cwd) + * @param {string} filename - path to a tsconfig .json or a source file or directory (absolute or relative to cwd) * @param {import('./public.d.ts').TSConfckParseOptions} [options] - options * @returns {Promise} * @throws {TSConfckParseError} */ export async function parse(filename, options) { + /** @type {import('./cache.js').TSConfckCache} */ const cache = options?.cache; if (cache?.hasParseResult(filename)) { return cache.getParseResult(filename); @@ -33,9 +34,10 @@ export async function parse(filename, options) { cache?.setParseResult(filename, configPromise); let tsconfigFile; + if (options?.resolveWithEmptyIfConfigNotFound) { try { - tsconfigFile = (await resolveTSConfig(filename)) || (await find(filename, options)); + tsconfigFile = (await resolveTSConfig(filename, cache)) || (await find(filename, options)); } catch (e) { const notFoundResult = { tsconfigFile: 'no_tsconfig_file_found', @@ -45,7 +47,7 @@ export async function parse(filename, options) { return configPromise; } } else { - tsconfigFile = (await resolveTSConfig(filename)) || (await find(filename, options)); + tsconfigFile = (await resolveTSConfig(filename, cache)) || (await find(filename, options)); } let result; if (filename !== tsconfigFile && cache?.hasParseResult(tsconfigFile)) { @@ -61,7 +63,7 @@ export async function parse(filename, options) { /** * * @param {string} tsconfigFile - path to tsconfig file - * @param {TSConfckCache} cache - cache + * @param {import('./cache.js').TSConfckCache} [cache] - cache * @param {boolean} [skipCache] - skip cache * @returns {Promise} */ @@ -124,7 +126,7 @@ async function parseReferences(result, cache) { /** * @param {import('./public.d.ts').TSConfckParseResult} result - * @param {TSConfckCache} [cache] + * @param {import('./cache.js').TSConfckCache}[cache] * @returns {Promise} */ async function parseExtends(result, cache) { diff --git a/packages/tsconfck/src/public.d.ts b/packages/tsconfck/src/public.d.ts index c6efbce..1c9b9c2 100644 --- a/packages/tsconfck/src/public.d.ts +++ b/packages/tsconfck/src/public.d.ts @@ -1,4 +1,4 @@ -import { TSConfckCache } from './cache'; +import { TSConfckCache } from './cache.js'; export interface TSConfckFindOptions { /** diff --git a/packages/tsconfck/src/util.js b/packages/tsconfck/src/util.js index 6159740..c9c3cbc 100644 --- a/packages/tsconfck/src/util.js +++ b/packages/tsconfck/src/util.js @@ -1,5 +1,5 @@ -import path from 'path'; -import { promises as fs } from 'fs'; +import path from 'node:path'; +import { promises as fs } from 'node:fs'; const POSIX_SEP_RE = new RegExp('\\' + path.posix.sep, 'g'); const NATIVE_SEP_RE = new RegExp('\\' + path.sep, 'g'); @@ -11,6 +11,8 @@ const DEFAULT_EXTENSIONS_RE_GROUP = `\\.(?:${DEFAULT_EXTENSIONS.map((ext) => ext '|' )})`; +const IS_POSIX = path.posix.sep === path.sep; + /** * loads typescript async to avoid direct dependency * @returns {Promise} @@ -26,13 +28,19 @@ export async function loadTS() { /** * @param {string} filename + * @param {import('./cache.js').TSConfckCache} [cache] * @returns {Promise} */ -export async function resolveTSConfig(filename) { +export async function resolveTSConfig(filename, cache) { if (path.extname(filename) !== '.json') { return; } const tsconfig = path.resolve(filename); + const dir = path.dirname(tsconfig); + if (cache?.hasTSConfigPath(dir)) { + const cached = await cache.getTSConfigPath(dir); + return cached === tsconfig ? tsconfig : undefined; + } try { const stat = await fs.stat(tsconfig); if (stat.isFile() || stat.isFIFO()) { @@ -57,11 +65,13 @@ export async function resolveTSConfig(filename) { * @param filename {string} filename with posix separators * @returns {string} filename with native separators */ -export function posix2native(filename) { - return path.posix.sep !== path.sep && filename.includes(path.posix.sep) - ? filename.replace(POSIX_SEP_RE, path.sep) - : filename; -} +export const posix2native = IS_POSIX + ? (s) => s + : (filename) => { + return filename.includes(path.posix.sep) + ? filename.replace(POSIX_SEP_RE, path.sep) + : filename; + }; /** * convert native separator to posix separator @@ -73,11 +83,13 @@ export function posix2native(filename) { * @param filename {string} filename with native separators * @returns {string} filename with posix separators */ -export function native2posix(filename) { - return path.posix.sep !== path.sep && filename.includes(path.sep) - ? filename.replace(NATIVE_SEP_RE, path.posix.sep) - : filename; -} +export const native2posix = IS_POSIX + ? (s) => s + : (filename) => { + return filename.includes(path.sep) + ? filename.replace(NATIVE_SEP_RE, path.posix.sep) + : filename; + }; /** * converts params to native separator, resolves path and converts native back to posix @@ -89,7 +101,7 @@ export function native2posix(filename) { * @returns string */ export function resolve2posix(dir, filename) { - if (path.sep === path.posix.sep) { + if (IS_POSIX) { return dir ? path.resolve(dir, filename) : path.resolve(filename); } return native2posix(