Skip to content

Commit

Permalink
feat: Add Project#getModuleResolutionHost()
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed May 25, 2019
1 parent 410c8f1 commit 9085100
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 49 deletions.
4 changes: 4 additions & 0 deletions lib/ts-morph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,10 @@ export declare class Project {
formatDiagnosticsWithColorAndContext(diagnostics: ReadonlyArray<Diagnostic>, opts?: {
newLineChar?: "\n" | "\r\n";
}): string;
/**
* Gets a ts.ModuleResolutionHost for the project.
*/
getModuleResolutionHost(): ts.ModuleResolutionHost;
}

/**
Expand Down
106 changes: 57 additions & 49 deletions src/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CompilerOptionsContainer, ManipulationSettings, ManipulationSettingsCon
import { SourceFileStructure, OptionalKind } from "./structures";
import { WriterFunction } from "./types";
import { ts, CompilerOptions } from "./typescript";
import { IterableUtils, FileUtils, matchGlobs, TsConfigResolver } from "./utils";
import { IterableUtils, FileUtils, matchGlobs, TsConfigResolver, Memoize } from "./utils";

/** Options for creating a project. */
export interface ProjectOptions {
Expand Down Expand Up @@ -68,7 +68,7 @@ export class Project {
const compilerOptions = getCompilerOptions();

// initialize the compiler resolution host
const resolutionHost = !options.resolutionHost ? undefined : options.resolutionHost(getModuleResolutionHost(this), () => {
const resolutionHost = !options.resolutionHost ? undefined : options.resolutionHost(this.getModuleResolutionHost(), () => {
if (this._context == null)
throw new errors.InvalidOperationError("Cannot get the compiler options until the project has initialized. " +
"Please ensure `getCompilerOptions` is called within the functions of the resolution host.");
Expand Down Expand Up @@ -109,53 +109,6 @@ export class Project {
return options.compilerOptions.charset || defaultEncoding;
return defaultEncoding;
}

function getModuleResolutionHost(project: Project): ts.ModuleResolutionHost {
return {
directoryExists: dirName => {
const context = getContext();
if (context.compilerFactory.containsDirectoryAtPath(dirName))
return true;
return fileSystemWrapper.directoryExistsSync(dirName);
},
fileExists: fileName => {
const context = getContext();
if (context.compilerFactory.containsSourceFileAtPath(fileName))
return true;
return fileSystemWrapper.fileExistsSync(fileName);
},
readFile: fileName => {
const context = getContext();
const sourceFile = context.compilerFactory.getSourceFileFromCacheFromFilePath(fileName);
if (sourceFile != null)
return sourceFile.getFullText();

try {
return fileSystemWrapper.readFileSync(fileName, project._context.getEncoding());
} catch (err) {
// this is what the compiler api does
if (FileUtils.isNotExistsError(err))
return undefined;
throw err;
}
},
getCurrentDirectory: () => fileSystemWrapper.getCurrentDirectory(),
getDirectories: path => {
const dirs = new Set<string>(fileSystemWrapper.readDirSync(path));
for (const dir of getContext().compilerFactory.getChildDirectoriesOfDirectory(path))
dirs.add(dir.getPath());
return Array.from(dirs);
},
realpath: path => fileSystemWrapper.realpathSync(path)
};

function getContext() {
if (project._context == null)
throw new errors.InvalidOperationError("Cannot use the module resolution host until the project has finished initializing.");

return project._context;
}
}
}

/** Gets the manipulation settings. */
Expand Down Expand Up @@ -640,6 +593,61 @@ export class Project {
getNewLine: () => opts.newLineChar || require("os").EOL
});
}

/**
* Gets a ts.ModuleResolutionHost for the project.
*/
@Memoize
getModuleResolutionHost(): ts.ModuleResolutionHost {
// defer getting the context because this could be created in the constructor
const project = this;

return {
directoryExists: dirName => {
const context = getContext();
if (context.compilerFactory.containsDirectoryAtPath(dirName))
return true;
return context.fileSystemWrapper.directoryExistsSync(dirName);
},
fileExists: fileName => {
const context = getContext();
if (context.compilerFactory.containsSourceFileAtPath(fileName))
return true;
return context.fileSystemWrapper.fileExistsSync(fileName);
},
readFile: fileName => {
const context = getContext();
const sourceFile = context.compilerFactory.getSourceFileFromCacheFromFilePath(fileName);
if (sourceFile != null)
return sourceFile.getFullText();

try {
return context.fileSystemWrapper.readFileSync(fileName, project._context.getEncoding());
} catch (err) {
// this is what the compiler api does
if (FileUtils.isNotExistsError(err))
return undefined;
throw err;
}
},
getCurrentDirectory: () => getContext().fileSystemWrapper.getCurrentDirectory(),
getDirectories: path => {
const context = getContext();
const dirs = new Set<string>(context.fileSystemWrapper.readDirSync(path));
for (const dir of context.compilerFactory.getChildDirectoriesOfDirectory(path))
dirs.add(dir.getPath());
return Array.from(dirs);
},
realpath: path => getContext().fileSystemWrapper.realpathSync(path)
};

function getContext() {
if (project._context == null)
throw new errors.InvalidOperationError("Cannot use the module resolution host until the project has finished initializing.");

return project._context;
}
}
}

function normalizeAmbientModuleName(moduleName: string) {
Expand Down
99 changes: 99 additions & 0 deletions src/tests/projectTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,105 @@ describe(nameof(Project), () => {
testForCarriageReturnLineFeed(text);
});
});

describe(nameof<Project>(p => p.getModuleResolutionHost), () => {
function setup() {
const project = new Project({ useVirtualFileSystem: true });
const moduleResolutionHost = project.getModuleResolutionHost();
return {
project,
fileSystem: project.getFileSystem(),
moduleResolutionHost
};
}

it("should get if a directory exists on the file system", () => {
const { moduleResolutionHost, fileSystem } = setup();
fileSystem.mkdirSync("/dir");
expect(moduleResolutionHost.directoryExists!("/dir")).to.be.true;
expect(moduleResolutionHost.directoryExists!("/dir2")).to.be.false;
});

it("should get if a directory exists in the project", () => {
const { moduleResolutionHost, project } = setup();
project.createDirectory("/dir");
expect(moduleResolutionHost.directoryExists!("/dir")).to.be.true;
});

it("should get if a file exists on the file system", () => {
const { moduleResolutionHost, fileSystem } = setup();
fileSystem.writeFileSync("/file.ts", "");
expect(moduleResolutionHost.fileExists!("/file.ts")).to.be.true;
expect(moduleResolutionHost.fileExists!("/file2.ts")).to.be.false;
});

it("should get if a file exists in the project", () => {
const { moduleResolutionHost, project } = setup();
project.createSourceFile("/file.ts", "");
expect(moduleResolutionHost.fileExists!("/file.ts")).to.be.true;
});

it("should read the contents of a file when it exists on the file system", () => {
const { moduleResolutionHost, fileSystem } = setup();
const contents = "test";
fileSystem.writeFileSync("/file.ts", contents);
expect(moduleResolutionHost.readFile!("/file.ts")).to.equal(contents);
});

it("should read the contents of a file when it exists in the project", () => {
const { moduleResolutionHost, project } = setup();
const contents = "test";
project.createSourceFile("/file.ts", contents);
expect(moduleResolutionHost.readFile!("/file.ts")).to.equal(contents);
});

it("should return undefined when reading a file that doesn't exist", () => {
const { moduleResolutionHost } = setup();
expect(moduleResolutionHost.readFile!("/file.ts")).to.be.undefined;
});

it("should get the current directory", () => {
const { moduleResolutionHost } = setup();
expect(moduleResolutionHost.getCurrentDirectory!()).to.equal("/");
});

it("should read the directories in a folder on the file system", () => {
const { moduleResolutionHost, fileSystem } = setup();
fileSystem.mkdirSync("/dir1");
fileSystem.mkdirSync("/dir2");
expect(moduleResolutionHost.getDirectories!("/")).to.deep.equal([
"/dir1",
"/dir2"
]);
});

it("should read the directories in a folder combining that with directores that exist in the project", () => {
const { moduleResolutionHost, fileSystem, project } = setup();
fileSystem.mkdirSync("/dir1");
project.createDirectory("/dir2").saveSync(); // exists on both file system and project
project.createDirectory("/dir3");
expect(moduleResolutionHost.getDirectories!("/")).to.deep.equal([
"/dir1",
"/dir2",
"/dir3",
]);
});

it("should get the real path", () => {
const { moduleResolutionHost, fileSystem } = setup();
fileSystem.realpathSync = path => path + "_RealPath";
expect(moduleResolutionHost.realpath!("test")).to.equal("/test_RealPath");
});

it("should not have a trace function", () => {
const { moduleResolutionHost } = setup();
// This hasn't been implemented and I'm not sure it will be.
// Looking at the compiler API code, it seems this writes to
// stdout. Probably best to let people implement this themselves
// if they want it.
expect(moduleResolutionHost.trace).to.be.undefined;
});
});
});

function assertHasDirectories(project: Project, dirPaths: string[]) {
Expand Down

0 comments on commit 9085100

Please sign in to comment.