Skip to content

Commit

Permalink
refactor(find): use callback for fs.stat and more caching
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikg committed Sep 1, 2023
1 parent 67afb9c commit 1ac5e8b
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-dogs-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tsconfck': minor
---

perf(find): switch to fs.stat callback for async and increase cache usage
9 changes: 0 additions & 9 deletions packages/tsconfck/src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 45 additions & 48 deletions packages/tsconfck/src/find.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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> | 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<string|undefined>}
* @param {Promise<string|null>} 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);
}
}
});
}
11 changes: 6 additions & 5 deletions packages/tsconfck/src/parse-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = {
Expand All @@ -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);
Expand Down
12 changes: 7 additions & 5 deletions packages/tsconfck/src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('./public.d.ts').TSConfckParseResult>}
* @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);
Expand All @@ -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',
Expand All @@ -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)) {
Expand All @@ -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<import('./public.d.ts').TSConfckParseResult>}
*/
Expand Down Expand Up @@ -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<void>}
*/
async function parseExtends(result, cache) {
Expand Down
2 changes: 1 addition & 1 deletion packages/tsconfck/src/public.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TSConfckCache } from './cache';
import { TSConfckCache } from './cache.js';

export interface TSConfckFindOptions {
/**
Expand Down
40 changes: 26 additions & 14 deletions packages/tsconfck/src/util.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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<any>}
Expand All @@ -26,13 +28,19 @@ export async function loadTS() {

/**
* @param {string} filename
* @param {import('./cache.js').TSConfckCache} [cache]
* @returns {Promise<string|void>}
*/
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()) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down

0 comments on commit 1ac5e8b

Please sign in to comment.