Skip to content

Commit

Permalink
WIP extracting filesystem and cache functions into create* factories
Browse files Browse the repository at this point in the history
  • Loading branch information
cspotcode committed Mar 1, 2021
1 parent 5c6efa4 commit 821e5a7
Showing 1 changed file with 127 additions and 73 deletions.
200 changes: 127 additions & 73 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ export interface CreateOptions {
* best results.
*/
require?: Array<string>;
readFile?: (path: string) => string | undefined;
fileExists?: (path: string) => boolean;
readFile?: ReadFileFunction;
fileExists?: FileExistsFunction;
transformers?:
| _ts.CustomTransformers
| ((p: _ts.Program) => _ts.CustomTransformers);
Expand Down Expand Up @@ -375,6 +375,9 @@ export interface TsConfigOptions
| 'experimentalEsmLoader'
> {}

export type ReadFileFunction = (path: string) => string | undefined;
export type FileExistsFunction = (path: string) => boolean;

/**
* Like `Object.assign`, but ignores `undefined` properties.
*/
Expand Down Expand Up @@ -902,18 +905,104 @@ export function create(rawOptions: CreateOptions = {}): Service {
};
}

/**
* Create filesystem access functions usable in `*Host` implementations which
* implement appropriate caching
*/
function createCachedFilesystemFunctions(opts: {
readFile: ReadFileFunction;
fileExists: FileExistsFunction;
}) {
const { readFile: _readFile, fileExists: _fileExists } = opts;
const readFile = cachedLookup(debugFn('readFile', _readFile));
const readDirectory = ts.sys.readDirectory;
const getDirectories = cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
);
const fileExists = cachedLookup(debugFn('fileExists', _fileExists));
const directoryExists = cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
);
const resolvePath = cachedLookup(
debugFn('resolvePath', ts.sys.resolvePath)
);
const realpath = ts.sys.realpath
? cachedLookup(debugFn('realpath', ts.sys.realpath))
: undefined;
return {
readFile,
readDirectory,
getDirectories,
fileExists,
directoryExists,
resolvePath,
realpath,
};
}

function createUpdateMemoryCacheFunction(opts: {
onProjectMustUpdate: () => void;
isFileKnownToBeInternal: ReturnType<
typeof createResolverFunctions
>['isFileKnownToBeInternal'];
markBucketOfFilenameInternal: ReturnType<
typeof createResolverFunctions
>['markBucketOfFilenameInternal'];
rootFileNames: Set<string>;
fileVersions: Map<string, number>;
fileContents: Map<string, string>;
}) {
const {
onProjectMustUpdate,
isFileKnownToBeInternal,
fileContents,
fileVersions,
markBucketOfFilenameInternal,
rootFileNames,
} = opts;
const updateMemoryCache = (contents: string, fileName: string) => {
let projectMustUpdate = false;
// Add to `rootFiles` as necessary, either to make TS include a file it has not seen,
// or to trigger a re-classification of files from external to internal.
if (!rootFileNames.has(fileName) && !isFileKnownToBeInternal(fileName)) {
markBucketOfFilenameInternal(fileName);
rootFileNames.add(fileName);
projectMustUpdate = true;
}

const previousVersion = fileVersions.get(fileName) || 0;
const previousContents = fileContents.get(fileName);
// Avoid incrementing cache when nothing has changed.
if (contents !== previousContents) {
fileVersions.set(fileName, previousVersion + 1);
fileContents.set(fileName, contents);
projectMustUpdate = true;
}
if (projectMustUpdate) onProjectMustUpdate();
};
return { updateMemoryCache };
}

// Use full language services when the fast option is disabled.
if (!transpileOnly) {
const {
readFile: cachedReadFile,
fileExists: cachedFileExists,
directoryExists,
getDirectories,
readDirectory,
realpath,
resolvePath,
} = createCachedFilesystemFunctions({ readFile, fileExists });
const fileContents = new Map<string, string>();
const rootFileNames = new Set(config.fileNames);
const cachedReadFile = cachedLookup(debugFn('readFile', readFile));
const fileVersions = new Map(
Array.from(rootFileNames).map((fileName) => [fileName, 0])
);

// Use language services by default (TODO: invert next major version).
if (!options.compilerHost) {
let projectVersion = 1;
const fileVersions = new Map(
Array.from(rootFileNames).map((fileName) => [fileName, 0])
);

const getCustomTransformers = () => {
if (typeof transformers === 'function') {
Expand Down Expand Up @@ -950,17 +1039,11 @@ export function create(rawOptions: CreateOptions = {}): Service {
return ts.ScriptSnapshot.fromString(contents);
},
readFile: cachedReadFile,
readDirectory: ts.sys.readDirectory,
getDirectories: cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
),
realpath: ts.sys.realpath
? cachedLookup(debugFn('realpath', ts.sys.realpath))
: undefined,
readDirectory,
getDirectories,
fileExists: cachedFileExists,
directoryExists,
realpath,
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
Expand All @@ -985,29 +1068,17 @@ export function create(rawOptions: CreateOptions = {}): Service {
);
const service = ts.createLanguageService(serviceHost, registry);

const updateMemoryCache = (contents: string, fileName: string) => {
// Add to `rootFiles` as necessary, either to make TS include a file it has not seen,
// or to trigger a re-classification of files from external to internal.
if (
!rootFileNames.has(fileName) &&
!isFileKnownToBeInternal(fileName)
) {
markBucketOfFilenameInternal(fileName);
rootFileNames.add(fileName);
// Increment project version for every change to rootFileNames.
projectVersion++;
}

const previousVersion = fileVersions.get(fileName) || 0;
const previousContents = fileContents.get(fileName);
// Avoid incrementing cache when nothing has changed.
if (contents !== previousContents) {
fileVersions.set(fileName, previousVersion + 1);
fileContents.set(fileName, contents);
// Increment project version for every file change.
const { updateMemoryCache } = createUpdateMemoryCacheFunction({
rootFileNames,
fileContents,
fileVersions,
isFileKnownToBeInternal,
markBucketOfFilenameInternal,
onProjectMustUpdate() {
// Increment project version for every file change or addition to rootFileNames
projectVersion++;
}
};
},
});

let previousProgram: _ts.Program | undefined = undefined;

Expand Down Expand Up @@ -1070,6 +1141,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
return { name, comment };
};
} else {
// options.compilerHost === true
const sys: _ts.System & _ts.FormatDiagnosticsHost = {
...ts.sys,
...diagnosticHost,
Expand All @@ -1080,18 +1152,12 @@ export function create(rawOptions: CreateOptions = {}): Service {
if (contents) fileContents.set(fileName, contents);
return contents;
},
readDirectory: ts.sys.readDirectory,
getDirectories: cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
),
resolvePath: cachedLookup(debugFn('resolvePath', ts.sys.resolvePath)),
realpath: ts.sys.realpath
? cachedLookup(debugFn('realpath', ts.sys.realpath))
: undefined,
readDirectory,
getDirectories,
fileExists: cachedFileExists,
directoryExists,
resolvePath,
realpath,
};

const host: _ts.CompilerHost = ts.createIncrementalCompilerHost
Expand Down Expand Up @@ -1147,26 +1213,14 @@ export function create(rawOptions: CreateOptions = {}): Service {
: transformers;

// Set the file contents into cache manually.
const updateMemoryCache = (contents: string, fileName: string) => {
const previousContents = fileContents.get(fileName);
const contentsChanged = previousContents !== contents;
if (contentsChanged) {
fileContents.set(fileName, contents);
}

// Add to `rootFiles` when discovered by compiler for the first time.
let addedToRootFileNames = false;
if (
!rootFileNames.has(fileName) &&
!isFileKnownToBeInternal(fileName)
) {
markBucketOfFilenameInternal(fileName);
rootFileNames.add(fileName);
addedToRootFileNames = true;
}

// Update program when file changes.
if (addedToRootFileNames || contentsChanged) {
const { updateMemoryCache } = createUpdateMemoryCacheFunction({
rootFileNames,
fileVersions,
fileContents,
isFileKnownToBeInternal,
markBucketOfFilenameInternal,
onProjectMustUpdate() {
// Update program when file changes.
builderProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
Array.from(rootFileNames),
config.options,
Expand All @@ -1175,8 +1229,8 @@ export function create(rawOptions: CreateOptions = {}): Service {
config.errors,
config.projectReferences
);
}
};
},
});

getOutput = (code: string, fileName: string) => {
const output: [string, string] = ['', ''];
Expand Down

0 comments on commit 821e5a7

Please sign in to comment.