Skip to content

Commit

Permalink
feat: Bundle typescript package in ts-morph/common (#934)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Feb 18, 2021
1 parent 3dd5c72 commit 1cccd02
Show file tree
Hide file tree
Showing 37 changed files with 7,600 additions and 250 deletions.
19 changes: 0 additions & 19 deletions docs/setup/file-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,6 @@ The current working directory on this file system will be `/`.

This file system can also be imported and created via the `InMemoryFileSystemHost` export.

#### `lib.d.ts` files

Since ts-morph 6.0, the in memory file system will have the [`lib.d.ts` files](https://github.com/Microsoft/TypeScript/tree/master/lib) loaded into the `/node_modules/typescript/lib` folder by default.

If you want the old behaviour, you can specify to skip loading them by providing a `skipLoadingLibFiles` option:

```ts ignore-error: 1109
import { Project, FileSystemHost } from "ts-morph";

const project = new Project({
useInMemoryFileSystem: true,
skipLoadingLibFiles: true
});

console.log(project.getFileSystem().directoryExistsSync("/node_modules")); // false
```

When using a non-default file system, the library will search for these files in `path.join(fs.getCurrentDirectory(), "node_modules/typescript/lib"))`.

### Custom File System

It's possible to use your own custom file system by implementing the `FileSystemHost` interface then passing in an instance of this when creating a new `Project` instance:
Expand Down
21 changes: 10 additions & 11 deletions packages/bootstrap/lib/ts-morph-bootstrap.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ export interface FileSystemHost {
export declare class InMemoryFileSystemHost implements FileSystemHost {
/**
* Constructor.
* @param options - Options for creating the file system.
*/
constructor(options?: InMemoryFileSystemHostOptions);
constructor();
/** @inheritdoc */
isCaseSensitive(): boolean;
/** @inheritdoc */
Expand Down Expand Up @@ -124,14 +123,6 @@ export declare class InMemoryFileSystemHost implements FileSystemHost {
globSync(patterns: ReadonlyArray<string>): string[];
}

export interface InMemoryFileSystemHostOptions {
/**
* Set this to true to not load the /node_modules/typescript/lib files on construction.
* @default false
*/
skipLoadingLibFiles?: boolean;
}

/** Host for implementing custom module and/or type reference directive resolution. */
export interface ResolutionHost {
resolveModuleNames?: ts.LanguageServiceHost["resolveModuleNames"];
Expand Down Expand Up @@ -194,8 +185,16 @@ export interface ProjectOptions {
skipAddingFilesFromTsConfig?: boolean;
/** Skip resolving file dependencies when providing a ts config file path and adding the files from tsconfig. @default false */
skipFileDependencyResolution?: boolean;
/** Skip loading the lib files when using an in-memory file system. @default false */
/**
* Skip loading the lib files. Unlike the compiler API, ts-morph does not load these
* from the node_modules folder, but instead loads them from some other JS code
* and uses a fake path for their existence. If you want to use a custom lib files
* folder path, then provide one using the libFolderPath options.
* @default false
*/
skipLoadingLibFiles?: boolean;
/** The folder to use for loading lib files. */
libFolderPath?: string;
/** Whether to use an in-memory file system. */
useInMemoryFileSystem?: boolean;
/**
Expand Down
19 changes: 12 additions & 7 deletions packages/bootstrap/src/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ export interface ProjectOptions {
skipAddingFilesFromTsConfig?: boolean;
/** Skip resolving file dependencies when providing a ts config file path and adding the files from tsconfig. @default false */
skipFileDependencyResolution?: boolean;
/** Skip loading the lib files when using an in-memory file system. @default false */
/**
* Skip loading the lib files. Unlike the compiler API, ts-morph does not load these
* from the node_modules folder, but instead loads them from some other JS code
* and uses a fake path for their existence. If you want to use a custom lib files
* folder path, then provide one using the libFolderPath options.
* @default false
*/
skipLoadingLibFiles?: boolean;
/** The folder to use for loading lib files. */
libFolderPath?: string;
/** Whether to use an in-memory file system. */
useInMemoryFileSystem?: boolean;
/**
Expand Down Expand Up @@ -93,16 +101,11 @@ function createProjectCommon(options: ProjectOptions) {
function verifyOptions() {
if (options.fileSystem != null && options.useInMemoryFileSystem)
throw new errors.InvalidOperationError("Cannot provide a file system when specifying to use an in-memory file system.");
if (options.skipLoadingLibFiles && !options.useInMemoryFileSystem) {
throw new errors.InvalidOperationError(
`The ${nameof(options.skipLoadingLibFiles)} option can only be true when ${nameof(options.useInMemoryFileSystem)} is true.`,
);
}
}

function getFileSystem() {
if (options.useInMemoryFileSystem)
return new InMemoryFileSystemHost({ skipLoadingLibFiles: options.skipLoadingLibFiles });
return new InMemoryFileSystemHost();
return options.fileSystem ?? new RealFileSystemHost();
}

Expand Down Expand Up @@ -159,6 +162,8 @@ export class Project {
resolutionHost: resolutionHost || {},
getProjectVersion: () => this._sourceFileCache.getProjectVersion().toString(),
isKnownTypesPackageName: options.isKnownTypesPackageName,
libFolderPath: options.libFolderPath,
skipLoadingLibFiles: options.skipLoadingLibFiles,
});
this.languageServiceHost = languageServiceHost;
this.compilerHost = compilerHost;
Expand Down
9 changes: 9 additions & 0 deletions packages/bootstrap/src/SourceFileCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export class SourceFileCache implements TsSourceFileContainer {
return this.sourceFilesByFilePath.get(filePath);
}

addLibFileToCacheByText(filePath: StandardizedFilePath, fileText: string, scriptKind: ScriptKind | undefined) {
return this.documentRegistry.createOrUpdateSourceFile(
filePath,
this.compilerOptions.get(),
ts.ScriptSnapshot.fromString(fileText),
scriptKind,
);
}

async addOrGetSourceFileFromFilePath(filePath: StandardizedFilePath, options: { scriptKind: ScriptKind | undefined; }): Promise<ts.SourceFile | undefined> {
let sourceFile = this.sourceFilesByFilePath.get(filePath);
if (sourceFile == null && await this.fileSystemWrapper.fileExists(filePath)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/bootstrap/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { CompilerOptionsContainer, FileSystemHost, InMemoryFileSystemHost, InMemoryFileSystemHostOptions, ResolutionHost, ResolutionHostFactory,
SettingsContainer, ts } from "@ts-morph/common";
export { CompilerOptionsContainer, FileSystemHost, InMemoryFileSystemHost, ResolutionHost, ResolutionHostFactory, SettingsContainer,
ts } from "@ts-morph/common";
export * from "./Project";
88 changes: 66 additions & 22 deletions packages/bootstrap/src/tests/projectTests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InMemoryFileSystemHost, ts } from "@ts-morph/common";
import { getLibFiles, InMemoryFileSystemHost, ts } from "@ts-morph/common";
import { expect } from "chai";
import { EOL } from "os";
import { createProject, createProjectSync, Project, ProjectOptions } from "../Project";
Expand All @@ -10,32 +10,46 @@ describe(nameof(Project), () => {

function doTestsForProject(create: (options: ProjectOptions) => Promise<Project>) {
it("should add the files from tsconfig.json by default with the target in the tsconfig.json", async () => {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();
fileSystem.writeFileSync("tsconfig.json", `{ "compilerOptions": { "rootDir": "test", "target": "ES5" }, "include": ["test"] }`);
fileSystem.writeFileSync("/otherFile.ts", "");
fileSystem.writeFileSync("/test/file.ts", "");
fileSystem.writeFileSync("/test/test2/file2.ts", "");
const project = await create({ tsConfigFilePath: "tsconfig.json", fileSystem });
const project = await create({
tsConfigFilePath: "tsconfig.json",
fileSystem,
skipLoadingLibFiles: true,
});
expect(project.getSourceFiles().map(s => s.fileName).sort()).to.deep.equal(["/test/file.ts", "/test/test2/file2.ts"].sort());
expect(project.getSourceFiles().map(s => s.languageVersion)).to.deep.equal([ts.ScriptTarget.ES5, ts.ScriptTarget.ES5]);
});

it("should add the files from tsconfig.json by default and also take into account the passed in compiler options", async () => {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();
fileSystem.writeFileSync("tsconfig.json", `{ "compilerOptions": { "target": "ES5" } }`);
fileSystem.writeFileSync("/otherFile.ts", "");
fileSystem.writeFileSync("/test/file.ts", "");
fileSystem.writeFileSync("/test/test2/file2.ts", "");
const project = await create({ tsConfigFilePath: "tsconfig.json", compilerOptions: { rootDir: "/test/test2" }, fileSystem });
const project = await create({
tsConfigFilePath: "tsconfig.json",
compilerOptions: { rootDir: "/test/test2" },
fileSystem,
skipLoadingLibFiles: true,
});
expect(project.getSourceFiles().map(s => s.fileName).sort()).to.deep.equal(["/otherFile.ts", "/test/file.ts", "/test/test2/file2.ts"].sort());
});

it("should not add the files from tsconfig.json when specifying not to", async () => {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();
fileSystem.writeFileSync("tsconfig.json", `{ "compilerOptions": { "rootDir": "test", "target": "ES5" } }`);
fileSystem.writeFileSync("/test/file.ts", "");
fileSystem.writeFileSync("/test/test2/file2.ts", "");
const project = await create({ tsConfigFilePath: "tsconfig.json", skipAddingFilesFromTsConfig: true, fileSystem });
const project = await create({
tsConfigFilePath: "tsconfig.json",
skipAddingFilesFromTsConfig: true,
fileSystem,
skipLoadingLibFiles: true,
});
expect(project.getSourceFiles().map(s => s.fileName).sort()).to.deep.equal([]);
});

Expand Down Expand Up @@ -211,24 +225,56 @@ describe(nameof(Project), () => {
describe(nameof<ProjectOptions>(o => o.skipLoadingLibFiles), () => {
it("should not skip loading lib files when empty", async () => {
const project = await create({ useInMemoryFileSystem: true });
const result = project.fileSystem.readDirSync("/node_modules/typescript/lib");
expect(result.some(r => r.includes("lib.d.ts"))).to.be.true;
const sourceFile = project.createSourceFile("test.ts", "const t: String = '';");
const program = project.createProgram();
expect(ts.getPreEmitDiagnostics(program).length).to.equal(0);

const typeChecker = program.getTypeChecker();
const varDecl = (sourceFile.statements[0] as ts.VariableStatement).declarationList.declarations[0];
const varDeclType = typeChecker.getTypeAtLocation(varDecl.type!);
const stringDec = varDeclType.getSymbol()!.declarations[0];
expect(stringDec.getSourceFile().fileName).to.equal("/node_modules/typescript/lib/lib.es5.d.ts");
});

it("should not skip loading lib files when true", async () => {
it("should skip loading lib files when true", async () => {
const project = await create({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
expect(project.fileSystem.directoryExistsSync("/node_modules")).to.be.false;
const sourceFile = project.createSourceFile("test.ts", "const t: String = '';");
const program = project.createProgram();
expect(ts.getPreEmitDiagnostics(program).length).to.equal(10);

const typeChecker = program.getTypeChecker();
const varDecl = (sourceFile.statements[0] as ts.VariableStatement).declarationList.declarations[0];
const varDeclType = typeChecker.getTypeAtLocation(varDecl.type!);
expect(varDeclType.getSymbol()).to.be.undefined;
});

it("should throw when providing skipLoadingLibFiles without using n in-memory file system", async () => {
it("should throw when providing skipLoadingLibFiles and a libFolderPath", async () => {
try {
await create({ skipLoadingLibFiles: true });
await create({ skipLoadingLibFiles: true, libFolderPath: "" });
expect.fail("should have thrown");
} catch (err) {
expect(err.message).to.equal("The skipLoadingLibFiles option can only be true when useInMemoryFileSystem is true.");
expect(err.message).to.equal("Cannot set skipLoadingLibFiles to true when libFolderPath is provided.");
}
});
});

describe(nameof<ProjectOptions>(o => o.libFolderPath), () => {
it("should support specifying a different folder for the lib files", async () => {
const fileSystem = new InMemoryFileSystemHost();
for (const file of getLibFiles())
fileSystem.writeFileSync(`/other/${file.fileName}`, file.text);
const project = await create({ fileSystem, libFolderPath: "/other" });
const sourceFile = project.createSourceFile("test.ts", "const t: String = '';");
const program = project.createProgram();
expect(ts.getPreEmitDiagnostics(program).length).to.equal(0);

const typeChecker = program.getTypeChecker();
const varDecl = (sourceFile.statements[0] as ts.VariableStatement).declarationList.declarations[0];
const varDeclType = typeChecker.getTypeAtLocation(varDecl.type!);
const stringDec = varDeclType.getSymbol()!.declarations[0];
expect(stringDec.getSourceFile().fileName).to.equal("/other/lib.es5.d.ts");
});
});
}
});

Expand Down Expand Up @@ -376,14 +422,14 @@ describe(nameof(Project), () => {
});

it("should add the files from tsconfig.json", async () => {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();
fileSystem.writeFileSync("tsconfig.json",
`{ "compilerOptions": { "rootDir": "test", "target": "ES5" }, "include": ["test"], "exclude": ["/test/exclude"] }`);
fileSystem.writeFileSync("/otherFile.ts", "");
fileSystem.writeFileSync("/test/file.ts", "");
fileSystem.writeFileSync("/test/test2/file2.ts", "");
fileSystem.writeFileSync("/test/exclude/file.ts", "");
const project = await createProject({ fileSystem });
const project = await createProject({ fileSystem, skipLoadingLibFiles: true });
expect(project.getSourceFiles()).to.deep.equal([]);
const returnedFiles = await action(project, "tsconfig.json");
const expectedFiles = ["/test/file.ts", "/test/test2/file2.ts"].sort();
Expand All @@ -401,12 +447,12 @@ describe(nameof(Project), () => {

function doTestsForMethod(action: (project: Project, globs: string | readonly string[]) => Promise<ts.SourceFile[]>) {
it("should add the source files based on a file glob", async () => {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();
fileSystem.writeFileSync("/otherFile.ts", "");
fileSystem.writeFileSync("/test/file.ts", "");
fileSystem.writeFileSync("/test/test2/file2.ts", "");
fileSystem.writeFileSync("/test/other/file.ts", "");
const project = await createProject({ fileSystem });
const project = await createProject({ fileSystem, skipLoadingLibFiles: true });
expect(project.getSourceFiles()).to.deep.equal([]);
const returnedFiles = await action(project, "/test/**/*.ts");
const expectedFiles = ["/test/file.ts", "/test/test2/file2.ts", "/test/other/file.ts"].sort();
Expand All @@ -427,7 +473,7 @@ describe(nameof(Project), () => {
});

async function fileDependencyResolutionSetup(options: ProjectOptions = {}, create: (options: ProjectOptions) => Promise<Project>) {
const fileSystem = new InMemoryFileSystemHost({ skipLoadingLibFiles: true });
const fileSystem = new InMemoryFileSystemHost();

fileSystem.writeFileSync("/package.json", `{ "name": "testing", "version": "0.0.1" }`);
fileSystem.writeFileSync("/node_modules/library/package.json",
Expand All @@ -443,7 +489,7 @@ describe(nameof(Project), () => {
fileSystem.writeFileSync("/other/referenced-file.d.ts", "declare function nameof(): void;");
fileSystem.writeFileSync("/tsconfig.json", `{ "files": ["src/main.ts"] }`);

const project = await create({ tsConfigFilePath: "tsconfig.json", fileSystem, ...options });
const project = await create({ tsConfigFilePath: "tsconfig.json", fileSystem, ...options, skipLoadingLibFiles: true });
return {
project,
initialFiles: ["/src/main.ts"],
Expand Down Expand Up @@ -560,7 +606,6 @@ describe(nameof(Project), () => {
expect(moduleResolutionHost.getDirectories!("/")).to.deep.equal([
"/dir1",
"/dir2",
"/node_modules",
]);
});

Expand All @@ -573,7 +618,6 @@ describe(nameof(Project), () => {
"/dir1",
"/dir2",
"/dir3",
"/node_modules",
]);
});

Expand Down
Loading

0 comments on commit 1cccd02

Please sign in to comment.