mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Switch to virtualFileSystemWithWatch, move other files back
Doesn't quite build yet, uploading so I can use github to view the diff.
This commit is contained in:
+573
-5
@@ -1,6 +1,492 @@
|
||||
namespace fakes {
|
||||
export class CompilerHost extends FakeCompilerHost {
|
||||
const processExitSentinel = new Error("System exit");
|
||||
|
||||
export interface SystemOptions {
|
||||
executingFilePath?: string;
|
||||
newLine?: "\r\n" | "\n";
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake `ts.System` that leverages a virtual file system.
|
||||
*/
|
||||
export class System implements ts.System {
|
||||
public readonly vfs: vfs.FileSystem;
|
||||
public readonly args: string[] = [];
|
||||
public readonly output: string[] = [];
|
||||
public readonly newLine: string;
|
||||
public readonly useCaseSensitiveFileNames: boolean;
|
||||
public exitCode: number | undefined;
|
||||
|
||||
private readonly _executingFilePath: string | undefined;
|
||||
private readonly _env: Record<string, string> | undefined;
|
||||
|
||||
constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) {
|
||||
this.vfs = vfs.isReadonly ? vfs.shadow() : vfs;
|
||||
this.useCaseSensitiveFileNames = !this.vfs.ignoreCase;
|
||||
this.newLine = newLine;
|
||||
this._executingFilePath = executingFilePath;
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
private testTerminalWidth = Number.parseInt(this.getEnvironmentVariable("TS_TEST_TERMINAL_WIDTH"));
|
||||
getWidthOfTerminal = Number.isNaN(this.testTerminalWidth) ? undefined : () => this.testTerminalWidth;
|
||||
|
||||
// Pretty output
|
||||
writeOutputIsTTY() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public write(message: string) {
|
||||
this.output.push(message);
|
||||
}
|
||||
|
||||
public readFile(path: string) {
|
||||
try {
|
||||
const content = this.vfs.readFileSync(path, "utf8");
|
||||
return content === undefined ? undefined : Utils.removeByteOrderMark(content);
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
|
||||
this.vfs.mkdirpSync(vpath.dirname(path));
|
||||
this.vfs.writeFileSync(path, writeByteOrderMark ? Utils.addUTF8ByteOrderMark(data) : data);
|
||||
}
|
||||
|
||||
public deleteFile(path: string) {
|
||||
this.vfs.unlinkSync(path);
|
||||
}
|
||||
|
||||
public fileExists(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.isFile() : false;
|
||||
}
|
||||
|
||||
public directoryExists(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.isDirectory() : false;
|
||||
}
|
||||
|
||||
public createDirectory(path: string): void {
|
||||
this.vfs.mkdirpSync(path);
|
||||
}
|
||||
|
||||
public getCurrentDirectory() {
|
||||
return this.vfs.cwd();
|
||||
}
|
||||
|
||||
public getDirectories(path: string) {
|
||||
const result: string[] = [];
|
||||
try {
|
||||
for (const file of this.vfs.readdirSync(path)) {
|
||||
if (this.vfs.statSync(vpath.combine(path, file)).isDirectory()) {
|
||||
result.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /*ignore*/ }
|
||||
return result;
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
|
||||
return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path), path => this.realpath(path));
|
||||
}
|
||||
|
||||
public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries {
|
||||
const files: string[] = [];
|
||||
const directories: string[] = [];
|
||||
try {
|
||||
for (const file of this.vfs.readdirSync(path)) {
|
||||
try {
|
||||
const stats = this.vfs.statSync(vpath.combine(path, file));
|
||||
if (stats.isFile()) {
|
||||
files.push(file);
|
||||
}
|
||||
else if (stats.isDirectory()) {
|
||||
directories.push(file);
|
||||
}
|
||||
}
|
||||
catch { /*ignored*/ }
|
||||
}
|
||||
}
|
||||
catch { /*ignored*/ }
|
||||
return { files, directories };
|
||||
}
|
||||
|
||||
public exit(exitCode?: number) {
|
||||
this.exitCode = exitCode;
|
||||
throw processExitSentinel;
|
||||
}
|
||||
|
||||
public getFileSize(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats && stats.isFile() ? stats.size : 0;
|
||||
}
|
||||
|
||||
public resolvePath(path: string) {
|
||||
return vpath.resolve(this.vfs.cwd(), path);
|
||||
}
|
||||
|
||||
public getExecutingFilePath() {
|
||||
if (this._executingFilePath === undefined) return ts.notImplemented();
|
||||
return this._executingFilePath;
|
||||
}
|
||||
|
||||
public getModifiedTime(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.mtime : undefined!; // TODO: GH#18217
|
||||
}
|
||||
|
||||
public setModifiedTime(path: string, time: Date) {
|
||||
this.vfs.utimesSync(path, time, time);
|
||||
}
|
||||
|
||||
public createHash(data: string): string {
|
||||
return `${ts.generateDjb2Hash(data)}-${data}`;
|
||||
}
|
||||
|
||||
public realpath(path: string) {
|
||||
try {
|
||||
return this.vfs.realpathSync(path);
|
||||
}
|
||||
catch {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public getEnvironmentVariable(name: string): string {
|
||||
return (this._env && this._env[name])!; // TODO: GH#18217
|
||||
}
|
||||
|
||||
private _getStats(path: string) {
|
||||
try {
|
||||
return this.vfs.existsSync(path) ? this.vfs.statSync(path) : undefined;
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
now() {
|
||||
return new Date(this.vfs.time());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake `ts.ParseConfigHost` that leverages a virtual file system.
|
||||
*/
|
||||
export class ParseConfigHost implements ts.ParseConfigHost {
|
||||
public readonly sys: System;
|
||||
|
||||
constructor(sys: System | vfs.FileSystem) {
|
||||
if (sys instanceof vfs.FileSystem) sys = new System(sys);
|
||||
this.sys = sys;
|
||||
}
|
||||
|
||||
public get vfs() {
|
||||
return this.sys.vfs;
|
||||
}
|
||||
|
||||
public get useCaseSensitiveFileNames() {
|
||||
return this.sys.useCaseSensitiveFileNames;
|
||||
}
|
||||
|
||||
public fileExists(fileName: string): boolean {
|
||||
return this.sys.fileExists(fileName);
|
||||
}
|
||||
|
||||
public directoryExists(directoryName: string): boolean {
|
||||
return this.sys.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
public readFile(path: string): string | undefined {
|
||||
return this.sys.readFile(path);
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
|
||||
return this.sys.readDirectory(path, extensions, excludes, includes, depth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {server.ServerHost} but that would create a circular dependency
|
||||
*/
|
||||
export class FakeServerHost extends System {
|
||||
timeoutCallbacks = new Callbacks()
|
||||
immediateCallbacks = new Callbacks()
|
||||
readonly watchedFiles = ts.createMultiMap<ts.Path, TestFileWatcher>();
|
||||
readonly fsWatches = ts.createMultiMap<ts.Path, TestFsWatcher>();
|
||||
readonly fsWatchesRecursive = ts.createMultiMap<ts.Path, TestFsWatcher>();
|
||||
readonly currentDirectory: string // TODO: Needed? Probably not
|
||||
readonly toPath: (f: string) => ts.Path;
|
||||
watchFile: ts.HostWatchFile
|
||||
watchDirectory: ts.HostWatchDirectory
|
||||
constructor(vfs: vfs.FileSystem, options: SystemOptions = {}) {
|
||||
super(vfs, options)
|
||||
|
||||
this.currentDirectory = '/'; // THIS IS FINE
|
||||
this.toPath = s => ts.toPath(s, this.currentDirectory, s => s);
|
||||
|
||||
const { watchFile, watchDirectory } = ts.createSystemWatchFunctions({
|
||||
// We dont have polling watch file
|
||||
// it is essentially fsWatch but lets get that separate from fsWatch and
|
||||
// into watchedFiles for easier testing
|
||||
pollingWatchFile: /*tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ?
|
||||
createSingleFileWatcherPerName(
|
||||
this.watchFileWorker.bind(this),
|
||||
this.useCaseSensitiveFileNames
|
||||
) :*/
|
||||
this.watchFileWorker.bind(this),
|
||||
getModifiedTime: this.getModifiedTime.bind(this),
|
||||
setTimeout: this.setTimeout.bind(this),
|
||||
clearTimeout: this.clearTimeout.bind(this),
|
||||
fsWatch: this.fsWatch.bind(this),
|
||||
fileExists: this.fileExists.bind(this),
|
||||
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
|
||||
getCurrentDirectory: this.getCurrentDirectory.bind(this),
|
||||
// TODO: Tests usually set "run without recursive watches" to true, except for two tests
|
||||
// watchOptions/with excludeFiles option${...}
|
||||
// and the same, but excludeDirectories
|
||||
// .............guessing that it's true on a real FS
|
||||
fsSupportsRecursiveFsWatch: true, /*tscWatchDirectory ? false : !runWithoutRecursiveWatches */
|
||||
directoryExists: this.directoryExists.bind(this),
|
||||
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
|
||||
realpath: this.realpath.bind(this),
|
||||
tscWatchFile: undefined,
|
||||
tscWatchDirectory: undefined,
|
||||
defaultWatchFileKind: () => undefined, // () => this.defaultWatchFileKind?.(),
|
||||
})
|
||||
this.watchFile = watchFile
|
||||
this.watchDirectory = watchDirectory
|
||||
}
|
||||
setTimeout(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any {
|
||||
return this.timeoutCallbacks.register(callback, args)
|
||||
}
|
||||
clearTimeout(timeoutId: any): void {
|
||||
this.timeoutCallbacks.unregister(timeoutId)
|
||||
}
|
||||
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
|
||||
return this.immediateCallbacks.register(callback, args)
|
||||
}
|
||||
clearImmediate(timeoutId: any): void {
|
||||
this.immediateCallbacks.unregister(timeoutId)
|
||||
}
|
||||
watchFileWorker(fileName: string, cb: ts.FileWatcherCallback, pollingInterval: ts.PollingInterval) {
|
||||
console.log('watchfileworker')
|
||||
return createWatcher(
|
||||
this.watchedFiles,
|
||||
this.toFullPath(fileName),
|
||||
{ fileName, cb, pollingInterval }
|
||||
);
|
||||
}
|
||||
|
||||
fsWatch(
|
||||
fileOrDirectory: string,
|
||||
_entryKind: ts.FileSystemEntryKind,
|
||||
cb: ts.FsWatchCallback,
|
||||
recursive: boolean,
|
||||
fallbackPollingInterval: ts.PollingInterval,
|
||||
fallbackOptions: ts.WatchOptions | undefined): ts.FileWatcher {
|
||||
console.log('fswatch')
|
||||
/*return this.runWithFallbackPolling ?
|
||||
this.watchFile(
|
||||
fileOrDirectory,
|
||||
createFileWatcherCallback(cb),
|
||||
fallbackPollingInterval,
|
||||
fallbackOptions
|
||||
) :*/
|
||||
return createWatcher(
|
||||
recursive ? this.fsWatchesRecursive : this.fsWatches,
|
||||
this.toFullPath(fileOrDirectory),
|
||||
{
|
||||
directoryName: fileOrDirectory,
|
||||
cb,
|
||||
fallbackPollingInterval,
|
||||
fallbackOptions
|
||||
}
|
||||
);
|
||||
}
|
||||
toFullPath(s: string) {
|
||||
return this.toPath(this.toNormalizedAbsolutePath(s));
|
||||
}
|
||||
toNormalizedAbsolutePath(s: string) {
|
||||
return ts.getNormalizedAbsolutePath(s, this.currentDirectory);
|
||||
}
|
||||
}
|
||||
function createWatcher<T>(map: ts.MultiMap<ts.Path, T>, path: ts.Path, callback: T): ts.FileWatcher {
|
||||
map.add(path, callback);
|
||||
return { close: () => map.remove(path, callback) };
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
type TimeOutCallback = () => any;
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
interface TestFileWatcher {
|
||||
cb: ts.FileWatcherCallback;
|
||||
fileName: string;
|
||||
pollingInterval: ts.PollingInterval;
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
interface TestFsWatcher {
|
||||
cb: ts.FsWatchCallback;
|
||||
directoryName: string;
|
||||
fallbackPollingInterval: ts.PollingInterval;
|
||||
fallbackOptions: ts.WatchOptions | undefined;
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
class Callbacks {
|
||||
private map: TimeOutCallback[] = [];
|
||||
private nextId = 1;
|
||||
getNextId() {
|
||||
return this.nextId;
|
||||
}
|
||||
register(cb: (...args: any[]) => void, args: any[]) {
|
||||
const timeoutId = this.nextId;
|
||||
this.nextId++;
|
||||
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
|
||||
return timeoutId;
|
||||
}
|
||||
unregister(id: any) {
|
||||
if (typeof id === "number") {
|
||||
delete this.map[id];
|
||||
}
|
||||
}
|
||||
count() {
|
||||
// ??????????????????????????????????????????????????????????????????
|
||||
let n = 0;
|
||||
for (const _ in this.map) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
invoke(invokeKey?: number) {
|
||||
if (invokeKey) {
|
||||
this.map[invokeKey]();
|
||||
delete this.map[invokeKey];
|
||||
return;
|
||||
}
|
||||
// Note: invoking a callback may result in new callbacks been queued,
|
||||
// so do not clear the entire callback list regardless. Only remove the
|
||||
// ones we have invoked.
|
||||
for (const key in this.map) {
|
||||
this.map[key]();
|
||||
delete this.map[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A fake `ts.CompilerHost` that leverages a virtual file system.
|
||||
*/
|
||||
export class CompilerHost implements ts.CompilerHost {
|
||||
public readonly sys: System;
|
||||
public readonly defaultLibLocation: string;
|
||||
public readonly outputs: documents.TextDocument[] = [];
|
||||
private readonly _outputsMap: collections.SortedMap<string, number>;
|
||||
public readonly traces: string[] = [];
|
||||
public readonly shouldAssertInvariants = !Harness.lightMode;
|
||||
protected _setParentNodes: boolean;
|
||||
protected _sourceFiles: collections.SortedMap<string, ts.SourceFile>;
|
||||
private _parseConfigHost: ParseConfigHost | undefined;
|
||||
private _newLine: string;
|
||||
|
||||
constructor(sys: System | vfs.FileSystem, options = ts.getDefaultCompilerOptions(), setParentNodes = false) {
|
||||
if (sys instanceof vfs.FileSystem) sys = new System(sys);
|
||||
this.sys = sys;
|
||||
this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || "";
|
||||
this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine);
|
||||
this._sourceFiles = new collections.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" });
|
||||
this._setParentNodes = setParentNodes;
|
||||
this._outputsMap = new collections.SortedMap(this.vfs.stringComparer);
|
||||
}
|
||||
|
||||
public get vfs() {
|
||||
return this.sys.vfs;
|
||||
}
|
||||
|
||||
public get parseConfigHost() {
|
||||
return this._parseConfigHost || (this._parseConfigHost = new ParseConfigHost(this.sys));
|
||||
}
|
||||
|
||||
public getCurrentDirectory(): string {
|
||||
return this.sys.getCurrentDirectory();
|
||||
}
|
||||
|
||||
public useCaseSensitiveFileNames(): boolean {
|
||||
return this.sys.useCaseSensitiveFileNames;
|
||||
}
|
||||
|
||||
public getNewLine(): string {
|
||||
return this._newLine;
|
||||
}
|
||||
|
||||
public getCanonicalFileName(fileName: string): string {
|
||||
return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
|
||||
}
|
||||
|
||||
public deleteFile(fileName: string) {
|
||||
this.sys.deleteFile(fileName);
|
||||
}
|
||||
|
||||
public fileExists(fileName: string): boolean {
|
||||
return this.sys.fileExists(fileName);
|
||||
}
|
||||
|
||||
public directoryExists(directoryName: string): boolean {
|
||||
return this.sys.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
public getModifiedTime(fileName: string) {
|
||||
return this.sys.getModifiedTime(fileName);
|
||||
}
|
||||
|
||||
public setModifiedTime(fileName: string, time: Date) {
|
||||
return this.sys.setModifiedTime(fileName, time);
|
||||
}
|
||||
|
||||
public getDirectories(path: string): string[] {
|
||||
return this.sys.getDirectories(path);
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
|
||||
return this.sys.readDirectory(path, extensions, exclude, include, depth);
|
||||
}
|
||||
|
||||
public readFile(path: string): string | undefined {
|
||||
return this.sys.readFile(path);
|
||||
}
|
||||
|
||||
public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) {
|
||||
if (writeByteOrderMark) content = Utils.addUTF8ByteOrderMark(content);
|
||||
this.sys.writeFile(fileName, content);
|
||||
|
||||
const document = new documents.TextDocument(fileName, content);
|
||||
document.meta.set("fileName", fileName);
|
||||
this.vfs.filemeta(fileName).set("document", document);
|
||||
if (!this._outputsMap.has(document.file)) {
|
||||
this._outputsMap.set(document.file, this.outputs.length);
|
||||
this.outputs.push(document);
|
||||
}
|
||||
this.outputs[this._outputsMap.get(document.file)!] = document;
|
||||
}
|
||||
|
||||
public trace(s: string): void {
|
||||
this.traces.push(s);
|
||||
}
|
||||
|
||||
public realpath(path: string): string {
|
||||
return this.sys.realpath(path);
|
||||
}
|
||||
|
||||
public getDefaultLibLocation(): string {
|
||||
return vpath.resolve(this.getCurrentDirectory(), this.defaultLibLocation);
|
||||
}
|
||||
|
||||
public getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
|
||||
}
|
||||
|
||||
public getSourceFile(fileName: string, languageVersion: number): ts.SourceFile | undefined {
|
||||
const canonicalFileName = this.getCanonicalFileName(vpath.resolve(this.getCurrentDirectory(), fileName));
|
||||
const existing = this._sourceFiles.get(canonicalFileName);
|
||||
@@ -27,7 +513,6 @@ namespace fakes {
|
||||
|
||||
const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes || this.shouldAssertInvariants);
|
||||
if (this.shouldAssertInvariants) {
|
||||
// TODO: Move this into a subclass
|
||||
Utils.assertInvariants(parsed, /*parent*/ undefined);
|
||||
}
|
||||
|
||||
@@ -64,7 +549,37 @@ namespace fakes {
|
||||
|
||||
}
|
||||
|
||||
function indentedText(indent: number, text: string) {
|
||||
export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]];
|
||||
export interface ExpectedDiagnosticMessageChain {
|
||||
message: ExpectedDiagnosticMessage;
|
||||
next?: ExpectedDiagnosticMessageChain[];
|
||||
}
|
||||
|
||||
export interface ExpectedDiagnosticLocation {
|
||||
file: string;
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
export interface ExpectedDiagnosticRelatedInformation extends ExpectedDiagnosticMessageChain {
|
||||
location?: ExpectedDiagnosticLocation;
|
||||
}
|
||||
|
||||
export enum DiagnosticKind {
|
||||
Error = "Error",
|
||||
Status = "Status"
|
||||
}
|
||||
export interface ExpectedErrorDiagnostic extends ExpectedDiagnosticRelatedInformation {
|
||||
relatedInformation?: ExpectedDiagnosticRelatedInformation[];
|
||||
}
|
||||
|
||||
export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic;
|
||||
|
||||
export interface SolutionBuilderDiagnostic {
|
||||
kind: DiagnosticKind;
|
||||
diagnostic: ts.Diagnostic;
|
||||
}
|
||||
|
||||
function indentedText(indent: number, text: string) {
|
||||
if (!indent) return text;
|
||||
let indentText = "";
|
||||
for (let i = 0; i < indent; i++) {
|
||||
@@ -120,6 +635,34 @@ ${indentText}${text}`;
|
||||
return text;
|
||||
}
|
||||
|
||||
export const version = "FakeTSVersion";
|
||||
|
||||
export function patchHostForBuildInfoReadWrite<T extends ts.System>(sys: T) {
|
||||
const originalReadFile = sys.readFile;
|
||||
sys.readFile = (path, encoding) => {
|
||||
const value = originalReadFile.call(sys, path, encoding);
|
||||
if (!value || !ts.isBuildInfoFile(path)) return value;
|
||||
const buildInfo = ts.getBuildInfo(value);
|
||||
ts.Debug.assert(buildInfo.version === version);
|
||||
buildInfo.version = ts.version;
|
||||
return ts.getBuildInfoText(buildInfo);
|
||||
};
|
||||
const originalWrite = sys.write;
|
||||
sys.write = msg => originalWrite.call(sys, msg.replace(ts.version, version));
|
||||
|
||||
if (sys.writeFile) {
|
||||
const originalWriteFile = sys.writeFile;
|
||||
sys.writeFile = (fileName: string, content: string, writeByteOrderMark: boolean) => {
|
||||
if (!ts.isBuildInfoFile(fileName)) return originalWriteFile.call(sys, fileName, content, writeByteOrderMark);
|
||||
const buildInfo = ts.getBuildInfo(content);
|
||||
buildInfo.version = version;
|
||||
originalWriteFile.call(sys, fileName, ts.getBuildInfoText(buildInfo), writeByteOrderMark);
|
||||
};
|
||||
}
|
||||
return sys;
|
||||
}
|
||||
|
||||
|
||||
function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) {
|
||||
const text = expectedDiagnosticMessageChainToText(diagnosticMessage);
|
||||
if (location) {
|
||||
@@ -146,13 +689,36 @@ ${indentText}${text}`;
|
||||
expectedErrorDiagnosticToText(errorOrStatus);
|
||||
}
|
||||
|
||||
export class SolutionBuilderHost extends FakeSolutionBuilderHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
|
||||
export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
|
||||
createProgram: ts.CreateProgram<ts.BuilderProgram>;
|
||||
private constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
|
||||
super(sys, options, setParentNodes);
|
||||
this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram;
|
||||
}
|
||||
static create(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
|
||||
const host = new SolutionBuilderHost(sys, options, setParentNodes, createProgram);
|
||||
patchHostForBuildInfoReadWrite(host.sys);
|
||||
return host;
|
||||
}
|
||||
|
||||
createHash(data: string) {
|
||||
return `${ts.generateDjb2Hash(data)}-${data}`;
|
||||
}
|
||||
|
||||
diagnostics: SolutionBuilderDiagnostic[] = [];
|
||||
|
||||
reportDiagnostic(diagnostic: ts.Diagnostic) {
|
||||
this.diagnostics.push({ kind: DiagnosticKind.Error, diagnostic });
|
||||
}
|
||||
|
||||
reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) {
|
||||
this.diagnostics.push({ kind: DiagnosticKind.Status, diagnostic });
|
||||
}
|
||||
|
||||
clearDiagnostics() {
|
||||
this.diagnostics.length = 0;
|
||||
}
|
||||
|
||||
assertDiagnosticMessages(...expectedDiagnostics: ExpectedDiagnostic[]) {
|
||||
const actual = this.diagnostics.slice().map(diagnosticToText);
|
||||
const expected = expectedDiagnostics.map(expectedDiagnosticToText);
|
||||
@@ -178,6 +744,8 @@ Actual All:: ${JSON.stringify(this.diagnostics.slice().map(diagnosticToText), /*
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
now() {
|
||||
return this.sys.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
],
|
||||
|
||||
"files": [
|
||||
"collectionsImpl.ts",
|
||||
"util.ts",
|
||||
"documentsUtil.ts",
|
||||
"vpathUtil.ts",
|
||||
"vfsUtil.ts",
|
||||
"compilerImpl.ts",
|
||||
"evaluatorImpl.ts",
|
||||
"fakesHosts.ts",
|
||||
|
||||
@@ -78,6 +78,26 @@ namespace Utils {
|
||||
return indentation;
|
||||
}
|
||||
|
||||
export function getByteOrderMarkLength(text: string): number {
|
||||
if (text.length >= 1) {
|
||||
const ch0 = text.charCodeAt(0);
|
||||
if (ch0 === 0xfeff) return 1;
|
||||
if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0;
|
||||
if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0;
|
||||
if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function removeByteOrderMark(text: string): string {
|
||||
const length = getByteOrderMarkLength(text);
|
||||
return length ? text.slice(length) : text;
|
||||
}
|
||||
|
||||
export function addUTF8ByteOrderMark(text: string) {
|
||||
return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
|
||||
}
|
||||
|
||||
export function theory<T extends any[]>(name: string, cb: (...args: T) => void, data: T[]) {
|
||||
for (const entry of data) {
|
||||
it(`${name}(${entry.map(formatTheoryDatum).join(", ")})`, () => cb(...entry));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -758,7 +758,7 @@ namespace ts.server {
|
||||
readonly toCanonicalFileName: (f: string) => string;
|
||||
|
||||
public host: ServerHost;
|
||||
public fs: vfs.FileSystem | undefined;
|
||||
public fs: vfs.VirtualServerHost | undefined;
|
||||
public readonly logger: Logger;
|
||||
public readonly cancellationToken: HostCancellationToken;
|
||||
public readonly useSingleInferredProject: boolean;
|
||||
@@ -3690,19 +3690,12 @@ namespace ts.server {
|
||||
/* @internal */
|
||||
updateFileSystem(createdFiles: Iterator<protocol.FileSystemRequestArgs> | undefined, updatedFiles?: Iterator<protocol.FileSystemRequestArgs>, deletedFiles?: string[]) {
|
||||
// TODO: Maybe it is somehow gauche or verboten to use protocol types but the translation in applyChangesInOpenFiles seems stupid
|
||||
// It is also WEIRD that we use iterators here not arrays
|
||||
// 1. set some internal tsserver state for mocked FS (if it hasn't already been set, this might not be the first message)
|
||||
if (!this.fs) {
|
||||
this.fs = new vfs.FileSystem(/*ignoreCase*/ true, {
|
||||
files: {
|
||||
"/": {}
|
||||
},
|
||||
cwd: "/", // maybe not needed
|
||||
meta: { } // probably not needed
|
||||
})
|
||||
// -nervous laugh-
|
||||
;(this as any).host = new fakes.FakeServerHost(this.fs, { executingFilePath: "TEST" })
|
||||
;(this.session as any).host = this.host
|
||||
this.fs = vfs.createVirtualServerHost([])
|
||||
;(this as any).host = this.fs
|
||||
;(this.session as any).host = this.fs
|
||||
}
|
||||
// 2. update vfs
|
||||
// I THINK that only vfs needs to update, because none of these files should be open.
|
||||
@@ -3712,25 +3705,24 @@ namespace ts.server {
|
||||
while (!(it = createdFiles.next()).done) {
|
||||
const document = it.value
|
||||
if (document.fileContent) {
|
||||
this.fs.mkdirpSync(vpath.dirname(document.file));
|
||||
this.fs.writeFileSync(document.file, document.fileContent, "utf8");
|
||||
this.fs.filemeta(document.file).set("document", document);
|
||||
// TODO: This isn't recursive, have to create parents AND walk past ones that already exist.
|
||||
this.fs.createDirectory(ts.getDirectoryPath(document.file));
|
||||
this.fs.writeFile(document.file, document.fileContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFiles) {
|
||||
const fileset: vfs.FileSet = {}
|
||||
let it
|
||||
while (!(it = updatedFiles.next()).done) {
|
||||
fileset[it.value.file] = it.value.fileContent
|
||||
}
|
||||
this.fs.apply(fileset)
|
||||
while (!(it = updatedFiles.next()).done)
|
||||
if (it.value.fileContent)
|
||||
// TODO: Also should check that file exists, probably creating if it doesn't
|
||||
this.fs.modifyFile(it.value.file, it.value.fileContent)
|
||||
}
|
||||
if (deletedFiles) {
|
||||
// these files should already be closed, so I THINK deleting it is enough
|
||||
// TODO: Probably want to delete empty parent folders while they are empty too
|
||||
for (const file of deletedFiles) {
|
||||
this.fs.rimrafSync(file)
|
||||
this.fs.deleteFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,643 +0,0 @@
|
||||
/**
|
||||
* Fake implementations of various compiler dependencies.
|
||||
*/
|
||||
namespace fakes {
|
||||
const processExitSentinel = new Error("System exit");
|
||||
|
||||
export interface SystemOptions {
|
||||
executingFilePath?: string;
|
||||
newLine?: "\r\n" | "\n";
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake `ts.System` that leverages a virtual file system.
|
||||
*/
|
||||
export class System implements ts.System {
|
||||
public readonly vfs: vfs.FileSystem;
|
||||
public readonly args: string[] = [];
|
||||
public readonly output: string[] = [];
|
||||
public readonly newLine: string;
|
||||
public readonly useCaseSensitiveFileNames: boolean;
|
||||
public exitCode: number | undefined;
|
||||
|
||||
private readonly _executingFilePath: string | undefined;
|
||||
private readonly _env: Record<string, string> | undefined;
|
||||
|
||||
constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) {
|
||||
this.vfs = vfs.isReadonly ? vfs.shadow() : vfs;
|
||||
this.useCaseSensitiveFileNames = !this.vfs.ignoreCase;
|
||||
this.newLine = newLine;
|
||||
this._executingFilePath = executingFilePath;
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
private testTerminalWidth = Number.parseInt(this.getEnvironmentVariable("TS_TEST_TERMINAL_WIDTH"));
|
||||
getWidthOfTerminal = Number.isNaN(this.testTerminalWidth) ? undefined : () => this.testTerminalWidth;
|
||||
|
||||
// Pretty output
|
||||
writeOutputIsTTY() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public write(message: string) {
|
||||
this.output.push(message);
|
||||
}
|
||||
|
||||
public readFile(path: string) {
|
||||
try {
|
||||
const content = this.vfs.readFileSync(path, "utf8");
|
||||
return content === undefined ? undefined : Utils.removeByteOrderMark(content);
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
|
||||
this.vfs.mkdirpSync(vpath.dirname(path));
|
||||
this.vfs.writeFileSync(path, writeByteOrderMark ? Utils.addUTF8ByteOrderMark(data) : data);
|
||||
}
|
||||
|
||||
public deleteFile(path: string) {
|
||||
this.vfs.unlinkSync(path);
|
||||
}
|
||||
|
||||
public fileExists(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.isFile() : false;
|
||||
}
|
||||
|
||||
public directoryExists(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.isDirectory() : false;
|
||||
}
|
||||
|
||||
public createDirectory(path: string): void {
|
||||
this.vfs.mkdirpSync(path);
|
||||
}
|
||||
|
||||
public getCurrentDirectory() {
|
||||
return this.vfs.cwd();
|
||||
}
|
||||
|
||||
public getDirectories(path: string) {
|
||||
const result: string[] = [];
|
||||
try {
|
||||
for (const file of this.vfs.readdirSync(path)) {
|
||||
if (this.vfs.statSync(vpath.combine(path, file)).isDirectory()) {
|
||||
result.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /*ignore*/ }
|
||||
return result;
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
|
||||
return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path), path => this.realpath(path));
|
||||
}
|
||||
|
||||
public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries {
|
||||
const files: string[] = [];
|
||||
const directories: string[] = [];
|
||||
try {
|
||||
for (const file of this.vfs.readdirSync(path)) {
|
||||
try {
|
||||
const stats = this.vfs.statSync(vpath.combine(path, file));
|
||||
if (stats.isFile()) {
|
||||
files.push(file);
|
||||
}
|
||||
else if (stats.isDirectory()) {
|
||||
directories.push(file);
|
||||
}
|
||||
}
|
||||
catch { /*ignored*/ }
|
||||
}
|
||||
}
|
||||
catch { /*ignored*/ }
|
||||
return { files, directories };
|
||||
}
|
||||
|
||||
public exit(exitCode?: number) {
|
||||
this.exitCode = exitCode;
|
||||
throw processExitSentinel;
|
||||
}
|
||||
|
||||
public getFileSize(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats && stats.isFile() ? stats.size : 0;
|
||||
}
|
||||
|
||||
public resolvePath(path: string) {
|
||||
return vpath.resolve(this.vfs.cwd(), path);
|
||||
}
|
||||
|
||||
public getExecutingFilePath() {
|
||||
if (this._executingFilePath === undefined) return ts.notImplemented();
|
||||
return this._executingFilePath;
|
||||
}
|
||||
|
||||
public getModifiedTime(path: string) {
|
||||
const stats = this._getStats(path);
|
||||
return stats ? stats.mtime : undefined!; // TODO: GH#18217
|
||||
}
|
||||
|
||||
public setModifiedTime(path: string, time: Date) {
|
||||
this.vfs.utimesSync(path, time, time);
|
||||
}
|
||||
|
||||
public createHash(data: string): string {
|
||||
return `${ts.generateDjb2Hash(data)}-${data}`;
|
||||
}
|
||||
|
||||
public realpath(path: string) {
|
||||
try {
|
||||
return this.vfs.realpathSync(path);
|
||||
}
|
||||
catch {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public getEnvironmentVariable(name: string): string {
|
||||
return (this._env && this._env[name])!; // TODO: GH#18217
|
||||
}
|
||||
|
||||
private _getStats(path: string) {
|
||||
try {
|
||||
return this.vfs.existsSync(path) ? this.vfs.statSync(path) : undefined;
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
now() {
|
||||
return new Date(this.vfs.time());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake `ts.ParseConfigHost` that leverages a virtual file system.
|
||||
*/
|
||||
export class ParseConfigHost implements ts.ParseConfigHost {
|
||||
public readonly sys: System;
|
||||
|
||||
constructor(sys: System | vfs.FileSystem) {
|
||||
if (sys instanceof vfs.FileSystem) sys = new System(sys);
|
||||
this.sys = sys;
|
||||
}
|
||||
|
||||
public get vfs() {
|
||||
return this.sys.vfs;
|
||||
}
|
||||
|
||||
public get useCaseSensitiveFileNames() {
|
||||
return this.sys.useCaseSensitiveFileNames;
|
||||
}
|
||||
|
||||
public fileExists(fileName: string): boolean {
|
||||
return this.sys.fileExists(fileName);
|
||||
}
|
||||
|
||||
public directoryExists(directoryName: string): boolean {
|
||||
return this.sys.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
public readFile(path: string): string | undefined {
|
||||
return this.sys.readFile(path);
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
|
||||
return this.sys.readDirectory(path, extensions, excludes, includes, depth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {server.ServerHost} but that would create a circular dependency
|
||||
*/
|
||||
export class FakeServerHost extends System {
|
||||
timeoutCallbacks = new Callbacks()
|
||||
immediateCallbacks = new Callbacks()
|
||||
readonly watchedFiles = ts.createMultiMap<ts.Path, TestFileWatcher>();
|
||||
readonly fsWatches = ts.createMultiMap<ts.Path, TestFsWatcher>();
|
||||
readonly fsWatchesRecursive = ts.createMultiMap<ts.Path, TestFsWatcher>();
|
||||
readonly currentDirectory: string // TODO: Needed? Probably not
|
||||
readonly toPath: (f: string) => ts.Path;
|
||||
watchFile: ts.HostWatchFile
|
||||
watchDirectory: ts.HostWatchDirectory
|
||||
constructor(vfs: vfs.FileSystem, options: SystemOptions = {}) {
|
||||
super(vfs, options)
|
||||
|
||||
this.currentDirectory = '/'; // THIS IS FINE
|
||||
this.toPath = s => ts.toPath(s, this.currentDirectory, s => s);
|
||||
|
||||
const { watchFile, watchDirectory } = ts.createSystemWatchFunctions({
|
||||
// We dont have polling watch file
|
||||
// it is essentially fsWatch but lets get that separate from fsWatch and
|
||||
// into watchedFiles for easier testing
|
||||
pollingWatchFile: /*tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ?
|
||||
createSingleFileWatcherPerName(
|
||||
this.watchFileWorker.bind(this),
|
||||
this.useCaseSensitiveFileNames
|
||||
) :*/
|
||||
this.watchFileWorker.bind(this),
|
||||
getModifiedTime: this.getModifiedTime.bind(this),
|
||||
setTimeout: this.setTimeout.bind(this),
|
||||
clearTimeout: this.clearTimeout.bind(this),
|
||||
fsWatch: this.fsWatch.bind(this),
|
||||
fileExists: this.fileExists.bind(this),
|
||||
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
|
||||
getCurrentDirectory: this.getCurrentDirectory.bind(this),
|
||||
// TODO: Tests usually set "run without recursive watches" to true, except for two tests
|
||||
// watchOptions/with excludeFiles option${...}
|
||||
// and the same, but excludeDirectories
|
||||
// .............guessing that it's true on a real FS
|
||||
fsSupportsRecursiveFsWatch: true, /*tscWatchDirectory ? false : !runWithoutRecursiveWatches */
|
||||
directoryExists: this.directoryExists.bind(this),
|
||||
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
|
||||
realpath: this.realpath.bind(this),
|
||||
tscWatchFile: undefined,
|
||||
tscWatchDirectory: undefined,
|
||||
defaultWatchFileKind: () => undefined, // () => this.defaultWatchFileKind?.(),
|
||||
})
|
||||
this.watchFile = watchFile
|
||||
this.watchDirectory = watchDirectory
|
||||
}
|
||||
setTimeout(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any {
|
||||
return this.timeoutCallbacks.register(callback, args)
|
||||
}
|
||||
clearTimeout(timeoutId: any): void {
|
||||
this.timeoutCallbacks.unregister(timeoutId)
|
||||
}
|
||||
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
|
||||
return this.immediateCallbacks.register(callback, args)
|
||||
}
|
||||
clearImmediate(timeoutId: any): void {
|
||||
this.immediateCallbacks.unregister(timeoutId)
|
||||
}
|
||||
watchFileWorker(fileName: string, cb: ts.FileWatcherCallback, pollingInterval: ts.PollingInterval) {
|
||||
console.log('watchfileworker')
|
||||
return createWatcher(
|
||||
this.watchedFiles,
|
||||
this.toFullPath(fileName),
|
||||
{ fileName, cb, pollingInterval }
|
||||
);
|
||||
}
|
||||
|
||||
fsWatch(
|
||||
fileOrDirectory: string,
|
||||
_entryKind: ts.FileSystemEntryKind,
|
||||
cb: ts.FsWatchCallback,
|
||||
recursive: boolean,
|
||||
fallbackPollingInterval: ts.PollingInterval,
|
||||
fallbackOptions: ts.WatchOptions | undefined): ts.FileWatcher {
|
||||
console.log('fswatch')
|
||||
/*return this.runWithFallbackPolling ?
|
||||
this.watchFile(
|
||||
fileOrDirectory,
|
||||
createFileWatcherCallback(cb),
|
||||
fallbackPollingInterval,
|
||||
fallbackOptions
|
||||
) :*/
|
||||
return createWatcher(
|
||||
recursive ? this.fsWatchesRecursive : this.fsWatches,
|
||||
this.toFullPath(fileOrDirectory),
|
||||
{
|
||||
directoryName: fileOrDirectory,
|
||||
cb,
|
||||
fallbackPollingInterval,
|
||||
fallbackOptions
|
||||
}
|
||||
);
|
||||
}
|
||||
toFullPath(s: string) {
|
||||
return this.toPath(this.toNormalizedAbsolutePath(s));
|
||||
}
|
||||
toNormalizedAbsolutePath(s: string) {
|
||||
return ts.getNormalizedAbsolutePath(s, this.currentDirectory);
|
||||
}
|
||||
}
|
||||
function createWatcher<T>(map: ts.MultiMap<ts.Path, T>, path: ts.Path, callback: T): ts.FileWatcher {
|
||||
map.add(path, callback);
|
||||
return { close: () => map.remove(path, callback) };
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
type TimeOutCallback = () => any;
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
interface TestFileWatcher {
|
||||
cb: ts.FileWatcherCallback;
|
||||
fileName: string;
|
||||
pollingInterval: ts.PollingInterval;
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
interface TestFsWatcher {
|
||||
cb: ts.FsWatchCallback;
|
||||
directoryName: string;
|
||||
fallbackPollingInterval: ts.PollingInterval;
|
||||
fallbackOptions: ts.WatchOptions | undefined;
|
||||
}
|
||||
/** Copied from virtualFileSystemWithWatch */
|
||||
class Callbacks {
|
||||
private map: TimeOutCallback[] = [];
|
||||
private nextId = 1;
|
||||
getNextId() {
|
||||
return this.nextId;
|
||||
}
|
||||
register(cb: (...args: any[]) => void, args: any[]) {
|
||||
const timeoutId = this.nextId;
|
||||
this.nextId++;
|
||||
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
|
||||
return timeoutId;
|
||||
}
|
||||
unregister(id: any) {
|
||||
if (typeof id === "number") {
|
||||
delete this.map[id];
|
||||
}
|
||||
}
|
||||
count() {
|
||||
// ??????????????????????????????????????????????????????????????????
|
||||
let n = 0;
|
||||
for (const _ in this.map) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
invoke(invokeKey?: number) {
|
||||
if (invokeKey) {
|
||||
this.map[invokeKey]();
|
||||
delete this.map[invokeKey];
|
||||
return;
|
||||
}
|
||||
// Note: invoking a callback may result in new callbacks been queued,
|
||||
// so do not clear the entire callback list regardless. Only remove the
|
||||
// ones we have invoked.
|
||||
for (const key in this.map) {
|
||||
this.map[key]();
|
||||
delete this.map[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A fake `ts.CompilerHost` that leverages a virtual file system.
|
||||
*/
|
||||
export class FakeCompilerHost implements ts.CompilerHost {
|
||||
public readonly sys: System;
|
||||
public readonly defaultLibLocation: string;
|
||||
public readonly outputs: documents.TextDocument[] = [];
|
||||
private readonly _outputsMap: collections.SortedMap<string, number>;
|
||||
public readonly traces: string[] = [];
|
||||
|
||||
protected _setParentNodes: boolean;
|
||||
protected _sourceFiles: collections.SortedMap<string, ts.SourceFile>;
|
||||
private _parseConfigHost: ParseConfigHost | undefined;
|
||||
private _newLine: string;
|
||||
|
||||
constructor(sys: System | vfs.FileSystem, options = ts.getDefaultCompilerOptions(), setParentNodes = false) {
|
||||
if (sys instanceof vfs.FileSystem) sys = new System(sys);
|
||||
this.sys = sys;
|
||||
this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || "";
|
||||
this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine);
|
||||
this._sourceFiles = new collections.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" });
|
||||
this._setParentNodes = setParentNodes;
|
||||
this._outputsMap = new collections.SortedMap(this.vfs.stringComparer);
|
||||
}
|
||||
|
||||
public get vfs() {
|
||||
return this.sys.vfs;
|
||||
}
|
||||
|
||||
public get parseConfigHost() {
|
||||
return this._parseConfigHost || (this._parseConfigHost = new ParseConfigHost(this.sys));
|
||||
}
|
||||
|
||||
public getCurrentDirectory(): string {
|
||||
return this.sys.getCurrentDirectory();
|
||||
}
|
||||
|
||||
public useCaseSensitiveFileNames(): boolean {
|
||||
return this.sys.useCaseSensitiveFileNames;
|
||||
}
|
||||
|
||||
public getNewLine(): string {
|
||||
return this._newLine;
|
||||
}
|
||||
|
||||
public getCanonicalFileName(fileName: string): string {
|
||||
return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
|
||||
}
|
||||
|
||||
public deleteFile(fileName: string) {
|
||||
this.sys.deleteFile(fileName);
|
||||
}
|
||||
|
||||
public fileExists(fileName: string): boolean {
|
||||
return this.sys.fileExists(fileName);
|
||||
}
|
||||
|
||||
public directoryExists(directoryName: string): boolean {
|
||||
return this.sys.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
public getModifiedTime(fileName: string) {
|
||||
return this.sys.getModifiedTime(fileName);
|
||||
}
|
||||
|
||||
public setModifiedTime(fileName: string, time: Date) {
|
||||
return this.sys.setModifiedTime(fileName, time);
|
||||
}
|
||||
|
||||
public getDirectories(path: string): string[] {
|
||||
return this.sys.getDirectories(path);
|
||||
}
|
||||
|
||||
public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
|
||||
return this.sys.readDirectory(path, extensions, exclude, include, depth);
|
||||
}
|
||||
|
||||
public readFile(path: string): string | undefined {
|
||||
return this.sys.readFile(path);
|
||||
}
|
||||
|
||||
public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) {
|
||||
if (writeByteOrderMark) content = Utils.addUTF8ByteOrderMark(content);
|
||||
this.sys.writeFile(fileName, content);
|
||||
|
||||
const document = new documents.TextDocument(fileName, content);
|
||||
document.meta.set("fileName", fileName);
|
||||
this.vfs.filemeta(fileName).set("document", document);
|
||||
if (!this._outputsMap.has(document.file)) {
|
||||
this._outputsMap.set(document.file, this.outputs.length);
|
||||
this.outputs.push(document);
|
||||
}
|
||||
this.outputs[this._outputsMap.get(document.file)!] = document;
|
||||
}
|
||||
|
||||
public trace(s: string): void {
|
||||
this.traces.push(s);
|
||||
}
|
||||
|
||||
public realpath(path: string): string {
|
||||
return this.sys.realpath(path);
|
||||
}
|
||||
|
||||
public getDefaultLibLocation(): string {
|
||||
return vpath.resolve(this.getCurrentDirectory(), this.defaultLibLocation);
|
||||
}
|
||||
|
||||
public getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
|
||||
}
|
||||
|
||||
public getSourceFile(fileName: string, languageVersion: number): ts.SourceFile | undefined {
|
||||
const canonicalFileName = this.getCanonicalFileName(vpath.resolve(this.getCurrentDirectory(), fileName));
|
||||
const existing = this._sourceFiles.get(canonicalFileName);
|
||||
if (existing) return existing;
|
||||
|
||||
const content = this.readFile(canonicalFileName);
|
||||
if (content === undefined) return undefined;
|
||||
|
||||
// A virtual file system may shadow another existing virtual file system. This
|
||||
// allows us to reuse a common virtual file system structure across multiple
|
||||
// tests. If a virtual file is a shadow, it is likely that the file will be
|
||||
// reused across multiple tests. In that case, we cache the SourceFile we parse
|
||||
// so that it can be reused across multiple tests to avoid the cost of
|
||||
// repeatedly parsing the same file over and over (such as lib.d.ts).
|
||||
const cacheKey = this.vfs.shadowRoot && `SourceFile[languageVersion=${languageVersion},setParentNodes=${this._setParentNodes}]`;
|
||||
if (cacheKey) {
|
||||
const meta = this.vfs.filemeta(canonicalFileName);
|
||||
const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined;
|
||||
if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) {
|
||||
this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata);
|
||||
return sourceFileFromMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes);
|
||||
this._sourceFiles.set(canonicalFileName, parsed);
|
||||
|
||||
if (cacheKey) {
|
||||
// store the cached source file on the unshadowed file with the same version.
|
||||
const stats = this.vfs.statSync(canonicalFileName);
|
||||
|
||||
let fs = this.vfs;
|
||||
while (fs.shadowRoot) {
|
||||
try {
|
||||
const shadowRootStats = fs.shadowRoot.existsSync(canonicalFileName) ? fs.shadowRoot.statSync(canonicalFileName) : undefined!; // TODO: GH#18217
|
||||
if (shadowRootStats.dev !== stats.dev ||
|
||||
shadowRootStats.ino !== stats.ino ||
|
||||
shadowRootStats.mtimeMs !== stats.mtimeMs) {
|
||||
break;
|
||||
}
|
||||
|
||||
fs = fs.shadowRoot;
|
||||
}
|
||||
catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fs !== this.vfs) {
|
||||
fs.filemeta(canonicalFileName).set(cacheKey, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]];
|
||||
export interface ExpectedDiagnosticMessageChain {
|
||||
message: ExpectedDiagnosticMessage;
|
||||
next?: ExpectedDiagnosticMessageChain[];
|
||||
}
|
||||
|
||||
export interface ExpectedDiagnosticLocation {
|
||||
file: string;
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
export interface ExpectedDiagnosticRelatedInformation extends ExpectedDiagnosticMessageChain {
|
||||
location?: ExpectedDiagnosticLocation;
|
||||
}
|
||||
|
||||
export enum DiagnosticKind {
|
||||
Error = "Error",
|
||||
Status = "Status"
|
||||
}
|
||||
export interface ExpectedErrorDiagnostic extends ExpectedDiagnosticRelatedInformation {
|
||||
relatedInformation?: ExpectedDiagnosticRelatedInformation[];
|
||||
}
|
||||
|
||||
export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic;
|
||||
|
||||
export interface SolutionBuilderDiagnostic {
|
||||
kind: DiagnosticKind;
|
||||
diagnostic: ts.Diagnostic;
|
||||
}
|
||||
|
||||
export const version = "FakeTSVersion";
|
||||
|
||||
export function patchHostForBuildInfoReadWrite<T extends ts.System>(sys: T) {
|
||||
const originalReadFile = sys.readFile;
|
||||
sys.readFile = (path, encoding) => {
|
||||
const value = originalReadFile.call(sys, path, encoding);
|
||||
if (!value || !ts.isBuildInfoFile(path)) return value;
|
||||
const buildInfo = ts.getBuildInfo(value);
|
||||
ts.Debug.assert(buildInfo.version === version);
|
||||
buildInfo.version = ts.version;
|
||||
return ts.getBuildInfoText(buildInfo);
|
||||
};
|
||||
const originalWrite = sys.write;
|
||||
sys.write = msg => originalWrite.call(sys, msg.replace(ts.version, version));
|
||||
|
||||
if (sys.writeFile) {
|
||||
const originalWriteFile = sys.writeFile;
|
||||
sys.writeFile = (fileName: string, content: string, writeByteOrderMark: boolean) => {
|
||||
if (!ts.isBuildInfoFile(fileName)) return originalWriteFile.call(sys, fileName, content, writeByteOrderMark);
|
||||
const buildInfo = ts.getBuildInfo(content);
|
||||
buildInfo.version = version;
|
||||
originalWriteFile.call(sys, fileName, ts.getBuildInfoText(buildInfo), writeByteOrderMark);
|
||||
};
|
||||
}
|
||||
return sys;
|
||||
}
|
||||
|
||||
export class FakeSolutionBuilderHost extends FakeCompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
|
||||
createProgram: ts.CreateProgram<ts.BuilderProgram>;
|
||||
|
||||
constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
|
||||
super(sys, options, setParentNodes);
|
||||
this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram;
|
||||
}
|
||||
|
||||
static create(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
|
||||
const host = new FakeSolutionBuilderHost(sys, options, setParentNodes, createProgram);
|
||||
patchHostForBuildInfoReadWrite(host.sys);
|
||||
return host;
|
||||
}
|
||||
|
||||
createHash(data: string) {
|
||||
return `${ts.generateDjb2Hash(data)}-${data}`;
|
||||
}
|
||||
|
||||
diagnostics: SolutionBuilderDiagnostic[] = [];
|
||||
|
||||
reportDiagnostic(diagnostic: ts.Diagnostic) {
|
||||
this.diagnostics.push({ kind: DiagnosticKind.Error, diagnostic });
|
||||
}
|
||||
|
||||
reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) {
|
||||
this.diagnostics.push({ kind: DiagnosticKind.Status, diagnostic });
|
||||
}
|
||||
|
||||
clearDiagnostics() {
|
||||
this.diagnostics.length = 0;
|
||||
}
|
||||
|
||||
now() {
|
||||
return this.sys.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,6 @@
|
||||
],
|
||||
|
||||
"files": [
|
||||
"collectionsImpl.ts",
|
||||
"documentsUtil.ts",
|
||||
"vpathUtil.ts",
|
||||
"vfsUtil.ts",
|
||||
"util.ts",
|
||||
"fakesHosts.ts"
|
||||
"virtualFileSystemWithWatch.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Common utilities
|
||||
*/
|
||||
namespace Utils {
|
||||
export function getByteOrderMarkLength(text: string): number {
|
||||
if (text.length >= 1) {
|
||||
const ch0 = text.charCodeAt(0);
|
||||
if (ch0 === 0xfeff) return 1;
|
||||
if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0;
|
||||
if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0;
|
||||
if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function removeByteOrderMark(text: string): string {
|
||||
const length = getByteOrderMarkLength(text);
|
||||
return length ? text.slice(length) : text;
|
||||
}
|
||||
|
||||
export function addUTF8ByteOrderMark(text: string) {
|
||||
return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user