diff --git a/packages/twoslash/src/core.ts b/packages/twoslash/src/core.ts index 0f3916d..fc94651 100644 --- a/packages/twoslash/src/core.ts +++ b/packages/twoslash/src/core.ts @@ -1,5 +1,5 @@ import type { ErrorLevel, NodeError, NodeWithoutPosition, Position, Range } from 'twoslash-protocol' -import type { CompilerOptions, CompletionEntry, CompletionTriggerKind, DiagnosticCategory, JsxEmit } from 'typescript' +import type { CompilerOptions, CompletionEntry, CompletionTriggerKind, DiagnosticCategory, JsxEmit, System } from 'typescript' import type { CompilerOptionDeclaration, CreateTwoslashOptions, TwoslashExecuteOptions, TwoslashInstance, TwoslashOptions, TwoslashReturn, TwoslashReturnMeta, VirtualFile } from './types' import { createFSBackedSystem, createSystem, createVirtualTypeScriptEnvironment } from '@typescript/vfs' @@ -28,15 +28,7 @@ export function createTwoslasher(createOptions: CreateTwoslashOptions = {}): Two const vfs = createOptions.fsMap || new Map() const system = useFS ? createSystem(vfs) - : { - ...createFSBackedSystem(vfs, _root, ts, createOptions.tsLibDirectory), - // To work with non-hoisted packages structure - realpath(path: string) { - if (vfs.has(path)) - return path - return ts.sys.realpath?.(path) || path - }, - } + : createCacheableFSBackedSystem(vfs, _root, ts, createOptions.tsLibDirectory, createOptions.fsCache) const fsRoot = useFS ? '/' : `${_root}/` const cache = createOptions.cache === false @@ -508,6 +500,55 @@ export function createTwoslasher(createOptions: CreateTwoslashOptions = {}): Two return twoslasher } +function createCacheableFSBackedSystem( + vfs: Map, + root: string, + ts: TS, + tsLibDirectory?: string, + enableFsCache = true, +): System { + function withCache(fn: (key: string) => T) { + const cache = new Map() + return (key: string) => { + const cached = cache.get(key) + if (cached !== undefined) + return cached + + const result = fn(key) + cache.set(key, result) + return result + } + } + const cachedReadFile = withCache(ts.sys.readFile) + + const cachedTs = enableFsCache + ? { + ...ts, + sys: { + ...ts.sys, + directoryExists: withCache(ts.sys.directoryExists), + fileExists: withCache(ts.sys.fileExists), + ...(ts.sys.realpath ? { realpath: withCache(ts.sys.realpath) } : {}), + readFile(path, encoding) { + if (encoding === undefined) + return cachedReadFile(path) + return ts.sys.readFile(path, encoding) + }, + } satisfies System, + } + : ts + + return { + ...createFSBackedSystem(vfs, root, cachedTs, tsLibDirectory), + // To work with non-hoisted packages structure + realpath(path: string) { + if (vfs.has(path)) + return path + return cachedTs.sys.realpath?.(path) || path + }, + } +} + /** * Run Twoslash on a string of code * diff --git a/packages/twoslash/src/types/options.ts b/packages/twoslash/src/types/options.ts index da663d8..79974fa 100644 --- a/packages/twoslash/src/types/options.ts +++ b/packages/twoslash/src/types/options.ts @@ -85,4 +85,11 @@ export interface CreateTwoslashOptions extends TwoslashExecuteOptions { * Cache the ts envs based on compiler options, defaults to true */ cache?: boolean | Map + + /** + * Cache file system requests + * + * @default true + */ + fsCache?: boolean }