Skip to content
This repository has been archived by the owner on Nov 20, 2020. It is now read-only.

feat: Reuse typescript program in between parses #40

Merged
merged 3 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import loader from "./loader";
const mockLoaderContextAsyncCallback = jest.fn();
const mockLoaderContextCacheable = jest.fn();
const mockLoaderContextResourcePath = jest.fn();
const mockLoaderContextContext = jest.fn();

beforeEach(() => {
mockLoaderContextAsyncCallback.mockReset();
mockLoaderContextCacheable.mockReset();
mockLoaderContextResourcePath.mockReset();
mockLoaderContextContext.mockReset();
mockLoaderContextResourcePath.mockImplementation(() =>
path.resolve(__dirname, "./__fixtures__/components/Simple.tsx"),
);
mockLoaderContextContext.mockImplementation(() =>
path.resolve(__dirname, "./__fixtures__/"),
);
});

it("marks the loader as being cacheable", () => {
Expand All @@ -31,9 +36,10 @@ function executeLoaderWithBoundContext() {
async: mockLoaderContextAsyncCallback,
cacheable: mockLoaderContextCacheable,
resourcePath: mockLoaderContextResourcePath(),
context: mockLoaderContextContext(),
} as Pick<
webpack.loader.LoaderContext,
"async" | "cacheable" | "resourcePath"
"async" | "cacheable" | "resourcePath" | "context"
>) as webpack.loader.LoaderContext,
"// Original Source Code",
);
Expand Down
114 changes: 113 additions & 1 deletion src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import webpack from "webpack";
import * as ts from "typescript";
import path from "path";
import fs from "fs";
// TODO: Import from "react-docgen-typescript" directly when
// https://github.com/styleguidist/react-docgen-typescript/pull/104 is hopefully
// merged in. Will be considering to make a peer dependency as that point.
Expand All @@ -14,6 +17,14 @@ import validateOptions from "./validateOptions";
import generateDocgenCodeBlock from "./generateDocgenCodeBlock";
import { getOptions } from "loader-utils";

export interface TSFile {
text?: string;
version: number;
}

let languageService: ts.LanguageService | null = null;
const files: Map<string, TSFile> = new Map<string, TSFile>();

export default function loader(
this: webpack.loader.LoaderContext,
source: string,
Expand Down Expand Up @@ -72,13 +83,50 @@ function processResource(
// Configure parser using settings provided to loader.
// See: node_modules/react-docgen-typescript/lib/parser.d.ts
let parser: FileParser = withDefaultConfig(parserOptions);

let compilerOptions: ts.CompilerOptions = {
allowJs: true,
};
let tsConfigFile: ts.ParsedCommandLine | null = null;

if (options.tsconfigPath) {
parser = withCustomConfig(options.tsconfigPath, parserOptions);

tsConfigFile = getTSConfigFile(options.tsconfigPath!);
compilerOptions = tsConfigFile.options;

const filesToLoad = tsConfigFile.fileNames;
loadFiles(filesToLoad);
} else if (options.compilerOptions) {
parser = withCompilerOptions(options.compilerOptions, parserOptions);
compilerOptions = options.compilerOptions;
}

if (!tsConfigFile) {
const basePath = path.dirname(context.context);
tsConfigFile = getDefaultTSConfigFile(basePath);

const filesToLoad = tsConfigFile.fileNames;
loadFiles(filesToLoad);
}

const componentDocs = parser.parse(context.resourcePath);
const componentDocs = parser.parseWithProgramProvider(
context.resourcePath,
() => {
if (languageService) {
return languageService.getProgram()!;
}

const servicesHost = createServiceHost(compilerOptions, files);

languageService = ts.createLanguageService(
servicesHost,
ts.createDocumentRegistry(),
);

return languageService!.getProgram()!;
},
);

// Return amended source code if there is docgen information available.
if (componentDocs.length) {
Expand All @@ -94,3 +142,67 @@ function processResource(
// Return unchanged source code if no docgen information was available.
return source;
}

function getTSConfigFile(tsconfigPath: string): ts.ParsedCommandLine {
const basePath = path.dirname(tsconfigPath);
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(
configFile!.config,
ts.sys,
basePath,
{},
tsconfigPath,
);
}

function getDefaultTSConfigFile(basePath: string): ts.ParsedCommandLine {
return ts.parseJsonConfigFileContent({}, ts.sys, basePath, {});
}

function loadFiles(filesToLoad: string[]): void {
let normalizedFilePath: string;
filesToLoad.forEach(filePath => {
normalizedFilePath = path.normalize(filePath);
files.set(normalizedFilePath, {
text: fs.readFileSync(normalizedFilePath, "utf-8"),
version: 0,
});
});
}

function createServiceHost(
compilerOptions: ts.CompilerOptions,
files: Map<string, TSFile>,
): ts.LanguageServiceHost {
return {
getScriptFileNames: () => {
return [...files.keys()];
},
getScriptVersion: fileName => {
const file = files.get(fileName);
return (file && file.version.toString()) || "";
},
getScriptSnapshot: fileName => {
if (!fs.existsSync(fileName)) {
return undefined;
}

let file = files.get(fileName);

if (file === undefined) {
const text = fs.readFileSync(fileName).toString();

file = { version: 0, text };
files.set(fileName, file);
}

return ts.ScriptSnapshot.fromString(file!.text!);
},
getCurrentDirectory: () => process.cwd(),
getCompilationSettings: () => compilerOptions,
getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
};
}