From cb8fb3b24f090d831b9ac3ee36e3fa57fc6b050c Mon Sep 17 00:00:00 2001 From: denieler Date: Fri, 29 Mar 2019 23:21:08 +0200 Subject: [PATCH 1/3] feat: Reuse typescript program in between parses --- src/loader.ts | 114 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/loader.ts b/src/loader.ts index c9ae118..1638e74 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -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. @@ -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 = new Map(); + export default function loader( this: webpack.loader.LoaderContext, source: string, @@ -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) { @@ -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, +): ts.LanguageServiceHost { + return { + getScriptFileNames: () => { + return [...Array.from(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, + }; +} From 54fbe701e07ac434490c0836d7d1bd0629c89e0c Mon Sep 17 00:00:00 2001 From: denieler Date: Sat, 30 Mar 2019 08:58:13 +0200 Subject: [PATCH 2/3] fix: unit test --- src/loader.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/loader.spec.ts b/src/loader.spec.ts index 3439ad3..0b20e2a 100644 --- a/src/loader.spec.ts +++ b/src/loader.spec.ts @@ -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", () => { @@ -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", ); From 9659593c865c7b726d878467aa15a289710a5ea9 Mon Sep 17 00:00:00 2001 From: denieler Date: Mon, 1 Apr 2019 10:25:35 +0300 Subject: [PATCH 3/3] fix: code styling --- src/loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loader.ts b/src/loader.ts index 1638e74..0f365dc 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -176,7 +176,7 @@ function createServiceHost( ): ts.LanguageServiceHost { return { getScriptFileNames: () => { - return [...Array.from(files.keys())]; + return [...files.keys()]; }, getScriptVersion: fileName => { const file = files.get(fileName);