Skip to content

Commit

Permalink
feat: accept cache (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber authored Aug 9, 2023
1 parent ae77a37 commit 81f9e77
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 35 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ For TypeScript related tooling to correctly parse `tsconfig.json` file without d

## API

### getTsconfig(searchPath?, configName?)
### getTsconfig(searchPath?, configName?, cache?)
Searches for a `tsconfig.json` file and parses it. Returns `null` if a config file cannot be found, or an object containing the path and parsed TSConfig object if found.

Returns:
Expand Down Expand Up @@ -57,6 +57,13 @@ Default: `tsconfig.json`
The file name of the TypeScript config file.
#### cache
Type: `Map<string, any>`
Default: `new Map()`
Optional cache for fs operations.
#### Example
```ts
Expand All @@ -80,14 +87,21 @@ console.log(getTsconfig('.', 'jsconfig.json'))

---

### parseTsconfig(tsconfigPath)
### parseTsconfig(tsconfigPath, cache?)
The `tsconfig.json` parser used internally by `getTsconfig`. Returns the parsed tsconfig as `TsConfigJsonResolved`.

#### tsconfigPath
Type: `string`

Required path to the tsconfig file.

#### cache
Type: `Map<string, any>`

Default: `new Map()`

Optional cache for fs operations.

#### Example

```ts
Expand Down
9 changes: 7 additions & 2 deletions src/get-tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import type { TsConfigResult } from './types.js';
export const getTsconfig = (
searchPath = process.cwd(),
configName = 'tsconfig.json',
cache: Map<string, any> = new Map(),
): TsConfigResult | null => {
const configFile = findUp(slash(searchPath), configName);
const configFile = findUp(
slash(searchPath),
configName,
cache,
);

if (!configFile) {
return null;
}

const config = parseTsconfig(configFile);
const config = parseTsconfig(configFile, cache);

return {
path: configFile,
Expand Down
27 changes: 21 additions & 6 deletions src/parse-tsconfig/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import fs from 'fs';
import path from 'path';
import slash from 'slash';
import type { TsConfigJson, TsConfigJsonResolved } from '../types.js';
import { normalizePath } from '../utils/normalize-path.js';
import { readJsonc } from '../utils/read-jsonc.js';
import { realpath } from '../utils/fs-cached.js';
import { resolveExtendsPath } from './resolve-extends-path.js';

const resolveExtends = (
extendsPath: string,
directoryPath: string,
cache?: Map<string, any>,
) => {
const resolvedExtendsPath = resolveExtendsPath(
extendsPath,
directoryPath,
cache,
);

if (!resolvedExtendsPath) {
throw new Error(`File '${extendsPath}' not found.`);
}

const extendsConfig = parseTsconfig(resolvedExtendsPath);
const extendsConfig = parseTsconfig(resolvedExtendsPath, cache);
delete extendsConfig.references;

if (extendsConfig.compilerOptions?.baseUrl) {
Expand Down Expand Up @@ -48,25 +50,38 @@ const resolveExtends = (
),
);
}

return extendsConfig;
};

export const parseTsconfig = (
tsconfigPath: string,
cache: Map<string, any> = new Map(),
): TsConfigJsonResolved => {
let realTsconfigPath: string;
try {
realTsconfigPath = fs.realpathSync(tsconfigPath);
realTsconfigPath = realpath(cache, tsconfigPath) as string;
} catch {
throw new Error(`Cannot resolve tsconfig at path: ${tsconfigPath}`);
}
const directoryPath = path.dirname(realTsconfigPath);
let config: TsConfigJson = readJsonc(realTsconfigPath) || {};

/**
* Decided not to cache the TsConfigJsonResolved object because it's
* mutable.
*
* Note how `resolveExtends` can call `parseTsconfig` rescursively
* and actually mutates the object. It can also be mutated in
* user-land.
*
* By only caching fs results, we can avoid serving mutated objects
*/
let config: TsConfigJson = readJsonc(realTsconfigPath, cache) || {};

if (typeof config !== 'object') {
throw new SyntaxError(`Failed to parse tsconfig at: ${tsconfigPath}`);
}

const directoryPath = path.dirname(realTsconfigPath);
if (config.extends) {
const extendsPathList = (
Array.isArray(config.extends)
Expand All @@ -77,7 +92,7 @@ export const parseTsconfig = (
delete config.extends;

for (const extendsPath of extendsPathList.reverse()) {
const extendsConfig = resolveExtends(extendsPath, directoryPath);
const extendsConfig = resolveExtends(extendsPath, directoryPath, cache);
const merged = {
...extendsConfig,
...config,
Expand Down
67 changes: 46 additions & 21 deletions src/parse-tsconfig/resolve-extends-path.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import path from 'path';
import fs from 'fs';
import Module from 'module';
import { resolveExports } from 'resolve-pkg-maps';
import type { PackageJson } from 'type-fest';
import { findUp } from '../utils/find-up.js';
import { readJsonc } from '../utils/read-jsonc.js';

const { existsSync } = fs;
import { stat, exists } from '../utils/fs-cached.js';

const getPnpApi = () => {
const { findPnpApi } = Module;
Expand All @@ -19,10 +17,16 @@ const resolveFromPackageJsonPath = (
packageJsonPath: string,
subpath: string,
ignoreExports?: boolean,
cache?: Map<string, any>,
) => {
const cacheKey = `resolveFromPackageJsonPath:${packageJsonPath}:${subpath}:${ignoreExports}`;
if (cache?.has(cacheKey)) {
return cache.get(cacheKey);
}

let resolvedPath = 'tsconfig.json';

const packageJson = readJsonc(packageJsonPath) as PackageJson;
const packageJson = readJsonc(packageJsonPath, cache) as PackageJson;
if (packageJson) {
if (
!ignoreExports
Expand All @@ -42,11 +46,15 @@ const resolveFromPackageJsonPath = (
}
}

return path.join(
resolvedPath = path.join(
packageJsonPath,
'..',
resolvedPath,
);

cache?.set(cacheKey, resolvedPath);

return resolvedPath;
};

const PACKAGE_JSON = 'package.json';
Expand All @@ -55,6 +63,7 @@ const TS_CONFIG_JSON = 'tsconfig.json';
export const resolveExtendsPath = (
requestedPath: string,
directoryPath: string,
cache?: Map<string, any>,
) => {
let filePath = requestedPath;

Expand All @@ -67,14 +76,14 @@ export const resolveExtendsPath = (
}

if (path.isAbsolute(filePath)) {
if (existsSync(filePath)) {
if (fs.statSync(filePath).isFile()) {
if (exists(cache, filePath)) {
if (stat(cache, filePath)!.isFile()) {
return filePath;
}
} else if (!filePath.endsWith('.json')) {
const jsonPath = `${filePath}.json`;

if (existsSync(jsonPath)) {
if (exists(cache, jsonPath)) {
return jsonPath;
}
}
Expand All @@ -97,9 +106,14 @@ export const resolveExtendsPath = (
);

if (packageJsonPath) {
const resolvedPath = resolveFromPackageJsonPath(packageJsonPath, subpath);
const resolvedPath = resolveFromPackageJsonPath(
packageJsonPath,
subpath,
false,
cache,
);

if (resolvedPath && existsSync(resolvedPath)) {
if (resolvedPath && exists(cache, resolvedPath)) {
return resolvedPath;
}
}
Expand Down Expand Up @@ -128,21 +142,27 @@ export const resolveExtendsPath = (
const packagePath = findUp(
directoryPath,
path.join('node_modules', packageName),
cache,
);

if (!packagePath || !fs.statSync(packagePath).isDirectory()) {
if (!packagePath || !stat(cache, packagePath)!.isDirectory()) {
return;
}

const packageJsonPath = path.join(packagePath, PACKAGE_JSON);
if (existsSync(packageJsonPath)) {
const resolvedPath = resolveFromPackageJsonPath(packageJsonPath, subpath);
if (exists(cache, packageJsonPath)) {
const resolvedPath = resolveFromPackageJsonPath(
packageJsonPath,
subpath,
false,
cache,
);

if (!resolvedPath) {
return;
}

if (existsSync(resolvedPath)) {
if (exists(cache, resolvedPath)) {
return resolvedPath;
}
}
Expand All @@ -153,26 +173,31 @@ export const resolveExtendsPath = (
if (!jsonExtension) {
const fullPackagePathWithJson = `${fullPackagePath}.json`;

if (existsSync(fullPackagePathWithJson)) {
if (exists(cache, fullPackagePathWithJson)) {
return fullPackagePathWithJson;
}
}

if (!existsSync(fullPackagePath)) {
if (!exists(cache, fullPackagePath)) {
return;
}

if (fs.statSync(fullPackagePath).isDirectory()) {
if (stat(cache, fullPackagePath)!.isDirectory()) {
const fullPackageJsonPath = path.join(fullPackagePath, PACKAGE_JSON);
if (existsSync(fullPackageJsonPath)) {
const resolvedPath = resolveFromPackageJsonPath(fullPackageJsonPath, '', true);
if (resolvedPath && existsSync(resolvedPath)) {
if (exists(cache, fullPackageJsonPath)) {
const resolvedPath = resolveFromPackageJsonPath(
fullPackageJsonPath,
'',
true,
cache,
);
if (resolvedPath && exists(cache, resolvedPath)) {
return resolvedPath;
}
}

const tsconfigPath = path.join(fullPackagePath, TS_CONFIG_JSON);
if (existsSync(tsconfigPath)) {
if (exists(cache, tsconfigPath)) {
return tsconfigPath;
}
} else if (jsonExtension) {
Expand Down
5 changes: 3 additions & 2 deletions src/utils/find-up.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import path from 'path';
import fs from 'fs';
import { exists } from './fs-cached.js';

export const findUp = (
searchPath: string,
fileName: string,
cache?: Map<string, any>,
) => {
while (true) {
const configPath = path.posix.join(searchPath, fileName);
if (fs.existsSync(configPath)) {
if (exists(cache, configPath)) {
return configPath;
}

Expand Down
36 changes: 36 additions & 0 deletions src/utils/fs-cached.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import fs from 'fs';

type Fs = typeof fs;

type AnyFunction = (...args: any[]) => any;

type FunctionProperties<Type> = {
[Key in keyof Type as Type[Key] extends AnyFunction ? Key : never]: Type[Key];
};

type FsMethods = FunctionProperties<Fs>;

const cacheFs = <MethodName extends keyof FsMethods>(
name: MethodName,
) => {
const method = fs[name];
return function (
cache?: Map<string, any>,
...args: any[]
): ReturnType<FsMethods[MethodName]> {
const cacheKey = `${name}:${args.join(':')}`;
let result = cache?.get(cacheKey);

if (result === undefined) {
result = Reflect.apply(method, fs, args);
cache?.set(cacheKey, result);
}

return result;
};
};

export const exists = cacheFs('existsSync');
export const realpath = cacheFs('realpathSync');
export const readFile = cacheFs('readFileSync');
export const stat = cacheFs('statSync');
5 changes: 3 additions & 2 deletions src/utils/read-jsonc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';
import { parse } from 'jsonc-parser';
import { readFile } from './fs-cached.js';

export const readJsonc = (
jsonPath: string,
) => parse(fs.readFileSync(jsonPath, 'utf8')) as unknown;
cache?: Map<string, any>,
) => parse(readFile(cache, jsonPath, 'utf8') as string) as unknown;
Loading

0 comments on commit 81f9e77

Please sign in to comment.