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

Commit

Permalink
Merge pull request #40 from denieler/feat-reuse-typescript-program
Browse files Browse the repository at this point in the history
feat: Reuse typescript program in between parses
  • Loading branch information
strothj authored Apr 2, 2019
2 parents a4b4d58 + 9659593 commit 261c505
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 2 deletions.
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,
};
}

0 comments on commit 261c505

Please sign in to comment.