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

Commit 261c505

Browse files
authored
Merge pull request #40 from denieler/feat-reuse-typescript-program
feat: Reuse typescript program in between parses
2 parents a4b4d58 + 9659593 commit 261c505

File tree

2 files changed

+120
-2
lines changed

2 files changed

+120
-2
lines changed

src/loader.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import loader from "./loader";
88
const mockLoaderContextAsyncCallback = jest.fn();
99
const mockLoaderContextCacheable = jest.fn();
1010
const mockLoaderContextResourcePath = jest.fn();
11+
const mockLoaderContextContext = jest.fn();
1112

1213
beforeEach(() => {
1314
mockLoaderContextAsyncCallback.mockReset();
1415
mockLoaderContextCacheable.mockReset();
1516
mockLoaderContextResourcePath.mockReset();
17+
mockLoaderContextContext.mockReset();
1618
mockLoaderContextResourcePath.mockImplementation(() =>
1719
path.resolve(__dirname, "./__fixtures__/components/Simple.tsx"),
1820
);
21+
mockLoaderContextContext.mockImplementation(() =>
22+
path.resolve(__dirname, "./__fixtures__/"),
23+
);
1924
});
2025

2126
it("marks the loader as being cacheable", () => {
@@ -31,9 +36,10 @@ function executeLoaderWithBoundContext() {
3136
async: mockLoaderContextAsyncCallback,
3237
cacheable: mockLoaderContextCacheable,
3338
resourcePath: mockLoaderContextResourcePath(),
39+
context: mockLoaderContextContext(),
3440
} as Pick<
3541
webpack.loader.LoaderContext,
36-
"async" | "cacheable" | "resourcePath"
42+
"async" | "cacheable" | "resourcePath" | "context"
3743
>) as webpack.loader.LoaderContext,
3844
"// Original Source Code",
3945
);

src/loader.ts

+113-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import webpack from "webpack";
2+
import * as ts from "typescript";
3+
import path from "path";
4+
import fs from "fs";
25
// TODO: Import from "react-docgen-typescript" directly when
36
// https://github.com/styleguidist/react-docgen-typescript/pull/104 is hopefully
47
// merged in. Will be considering to make a peer dependency as that point.
@@ -14,6 +17,14 @@ import validateOptions from "./validateOptions";
1417
import generateDocgenCodeBlock from "./generateDocgenCodeBlock";
1518
import { getOptions } from "loader-utils";
1619

20+
export interface TSFile {
21+
text?: string;
22+
version: number;
23+
}
24+
25+
let languageService: ts.LanguageService | null = null;
26+
const files: Map<string, TSFile> = new Map<string, TSFile>();
27+
1728
export default function loader(
1829
this: webpack.loader.LoaderContext,
1930
source: string,
@@ -72,13 +83,50 @@ function processResource(
7283
// Configure parser using settings provided to loader.
7384
// See: node_modules/react-docgen-typescript/lib/parser.d.ts
7485
let parser: FileParser = withDefaultConfig(parserOptions);
86+
87+
let compilerOptions: ts.CompilerOptions = {
88+
allowJs: true,
89+
};
90+
let tsConfigFile: ts.ParsedCommandLine | null = null;
91+
7592
if (options.tsconfigPath) {
7693
parser = withCustomConfig(options.tsconfigPath, parserOptions);
94+
95+
tsConfigFile = getTSConfigFile(options.tsconfigPath!);
96+
compilerOptions = tsConfigFile.options;
97+
98+
const filesToLoad = tsConfigFile.fileNames;
99+
loadFiles(filesToLoad);
77100
} else if (options.compilerOptions) {
78101
parser = withCompilerOptions(options.compilerOptions, parserOptions);
102+
compilerOptions = options.compilerOptions;
103+
}
104+
105+
if (!tsConfigFile) {
106+
const basePath = path.dirname(context.context);
107+
tsConfigFile = getDefaultTSConfigFile(basePath);
108+
109+
const filesToLoad = tsConfigFile.fileNames;
110+
loadFiles(filesToLoad);
79111
}
80112

81-
const componentDocs = parser.parse(context.resourcePath);
113+
const componentDocs = parser.parseWithProgramProvider(
114+
context.resourcePath,
115+
() => {
116+
if (languageService) {
117+
return languageService.getProgram()!;
118+
}
119+
120+
const servicesHost = createServiceHost(compilerOptions, files);
121+
122+
languageService = ts.createLanguageService(
123+
servicesHost,
124+
ts.createDocumentRegistry(),
125+
);
126+
127+
return languageService!.getProgram()!;
128+
},
129+
);
82130

83131
// Return amended source code if there is docgen information available.
84132
if (componentDocs.length) {
@@ -94,3 +142,67 @@ function processResource(
94142
// Return unchanged source code if no docgen information was available.
95143
return source;
96144
}
145+
146+
function getTSConfigFile(tsconfigPath: string): ts.ParsedCommandLine {
147+
const basePath = path.dirname(tsconfigPath);
148+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
149+
return ts.parseJsonConfigFileContent(
150+
configFile!.config,
151+
ts.sys,
152+
basePath,
153+
{},
154+
tsconfigPath,
155+
);
156+
}
157+
158+
function getDefaultTSConfigFile(basePath: string): ts.ParsedCommandLine {
159+
return ts.parseJsonConfigFileContent({}, ts.sys, basePath, {});
160+
}
161+
162+
function loadFiles(filesToLoad: string[]): void {
163+
let normalizedFilePath: string;
164+
filesToLoad.forEach(filePath => {
165+
normalizedFilePath = path.normalize(filePath);
166+
files.set(normalizedFilePath, {
167+
text: fs.readFileSync(normalizedFilePath, "utf-8"),
168+
version: 0,
169+
});
170+
});
171+
}
172+
173+
function createServiceHost(
174+
compilerOptions: ts.CompilerOptions,
175+
files: Map<string, TSFile>,
176+
): ts.LanguageServiceHost {
177+
return {
178+
getScriptFileNames: () => {
179+
return [...files.keys()];
180+
},
181+
getScriptVersion: fileName => {
182+
const file = files.get(fileName);
183+
return (file && file.version.toString()) || "";
184+
},
185+
getScriptSnapshot: fileName => {
186+
if (!fs.existsSync(fileName)) {
187+
return undefined;
188+
}
189+
190+
let file = files.get(fileName);
191+
192+
if (file === undefined) {
193+
const text = fs.readFileSync(fileName).toString();
194+
195+
file = { version: 0, text };
196+
files.set(fileName, file);
197+
}
198+
199+
return ts.ScriptSnapshot.fromString(file!.text!);
200+
},
201+
getCurrentDirectory: () => process.cwd(),
202+
getCompilationSettings: () => compilerOptions,
203+
getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
204+
fileExists: ts.sys.fileExists,
205+
readFile: ts.sys.readFile,
206+
readDirectory: ts.sys.readDirectory,
207+
};
208+
}

0 commit comments

Comments
 (0)