Skip to content

Commit

Permalink
add support for type-checking .mdx files
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Mar 8, 2019
1 parent 2dad936 commit e6e1cc3
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 10 deletions.
49 changes: 44 additions & 5 deletions src/IncrementalChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import * as minimatch from 'minimatch';
import { VueProgram } from './VueProgram';
import { FsHelper } from './FsHelper';
import { IncrementalCheckerInterface } from './IncrementalCheckerInterface';
import { MdxProgram } from './MdxProgram';
import { ProgramType } from './ProgramType';

export class IncrementalChecker implements IncrementalCheckerInterface {
// it's shared between compilations
Expand Down Expand Up @@ -59,7 +61,7 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
private workNumber: number = 0,
private workDivision: number = 1,
private checkSyntacticErrors: boolean = false,
private vue: boolean = false
private type: ProgramType = ProgramType.typescript
) {
this.hasFixedConfig = typeof this.linterConfigFile === 'string';
}
Expand Down Expand Up @@ -161,9 +163,18 @@ export class IncrementalChecker implements IncrementalCheckerInterface {

public nextIteration() {
if (!this.watcher) {
const watchExtensions = this.vue
? ['.ts', '.tsx', '.vue']
: ['.ts', '.tsx'];
const watchExtensions = ['.ts', '.tsx'];
switch (this.type) {
case ProgramType.vue:
watchExtensions.push('.vue');
break;
case ProgramType.mdx:
watchExtensions.push(
...MdxProgram.extraExtensions.map(ext => `.${ext}`)
);
break;
}

this.watcher = new FilesWatcher(this.watchPaths, watchExtensions);

// connect watcher with register
Expand Down Expand Up @@ -193,7 +204,17 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
}
}

this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram();
switch (this.type) {
case ProgramType.vue:
this.program = this.loadVueProgram();
break;
case ProgramType.mdx:
this.program = this.loadMdxProgram();
break;
case ProgramType.typescript:
default:
this.program = this.loadDefaultProgram();
}

if (this.linterConfigFile) {
this.linter = this.createLinter(this.program!);
Expand All @@ -219,6 +240,24 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
);
}

private loadMdxProgram() {
this.programConfig =
this.programConfig ||
MdxProgram.loadProgramConfig(
this.typescript,
this.programConfigFile,
this.compilerOptions
);

return MdxProgram.createProgram(
this.typescript,
this.programConfig,
this.files,
this.watcher!,
this.program!
);
}

private loadDefaultProgram() {
this.programConfig =
this.programConfig ||
Expand Down
163 changes: 163 additions & 0 deletions src/MdxProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as fs from 'fs';
import * as path from 'path';
// tslint:disable-next-line:no-implicit-dependencies
import * as ts from 'typescript'; // import for types alone
import { FilesRegister } from './FilesRegister';
import { FilesWatcher } from './FilesWatcher';

interface ResolvedScript {
scriptKind: ts.ScriptKind;
content: string;
}

interface MdxOptions {
footnotes: boolean;
mdPlugins: unknown[];
hastPlugins: unknown[];
compilers: unknown[];
blocks: unknown[];
}

export class MdxProgram {
public static extraExtensions = ['mdx', 'md'];

public static loadProgramConfig(
typescript: typeof ts,
configFile: string,
compilerOptions: object
) {
const parseConfigHost: ts.ParseConfigHost = {
fileExists: typescript.sys.fileExists,
readFile: typescript.sys.readFile,
useCaseSensitiveFileNames: typescript.sys.useCaseSensitiveFileNames,
readDirectory: (rootDir, extensions, excludes, includes, depth) => {
return typescript.sys.readDirectory(
rootDir,
extensions.concat(MdxProgram.extraExtensions),
excludes,
includes,
depth
);
}
};

const tsconfig = typescript.readConfigFile(
configFile,
typescript.sys.readFile
).config;

tsconfig.compilerOptions = tsconfig.compilerOptions || {};
tsconfig.compilerOptions = {
...tsconfig.compilerOptions,
...compilerOptions
};

const parsed = typescript.parseJsonConfigFileContent(
tsconfig,
parseConfigHost,
path.dirname(configFile)
);

parsed.options.allowNonTsExtensions = true;

return parsed;
}

public static isMdx(filePath: string) {
return MdxProgram.extraExtensions.includes(path.extname(filePath).slice(1));
}

public static createProgram(
typescript: typeof ts,
programConfig: ts.ParsedCommandLine,
files: FilesRegister,
watcher: FilesWatcher,
oldProgram: ts.Program
) {
const host = typescript.createCompilerHost(programConfig.options);
const realGetSourceFile = host.getSourceFile;

// We need a host that can parse Vue SFCs (single file components).
host.getSourceFile = (filePath, languageVersion, onError) => {
// first check if watcher is watching file - if not - check it's mtime
if (!watcher.isWatchingFile(filePath)) {
try {
const stats = fs.statSync(filePath);

files.setMtime(filePath, stats.mtime.valueOf());
} catch (e) {
// probably file does not exists
files.remove(filePath);
}
}

// get source file only if there is no source in files register
if (!files.has(filePath) || !files.getData(filePath).source) {
files.mutateData(filePath, data => {
data.source = realGetSourceFile(filePath, languageVersion, onError);
});
}

let source = files.getData(filePath).source;

// get typescript contents from Vue file
if (source && MdxProgram.isMdx(filePath)) {
const resolved = MdxProgram.resolveScriptBlock(typescript, source.text);
source = typescript.createSourceFile(
filePath,
resolved.content,
languageVersion,
true,
resolved.scriptKind
);
}

return source;
};

return typescript.createProgram(
programConfig.fileNames,
programConfig.options,
host,
oldProgram // re-use old program
);
}

public static resolveScriptBlock(
typescript: typeof ts,
content: string
): ResolvedScript {
// We need to import @mdx-js/mdx lazily because it cannot be included it
// as direct dependency because it is an optional dependency of fork-ts-checker-webpack-plugin.
let compiler: { sync(mdx: string, options?: Partial<MdxOptions>): string };
try {
// tslint:disable-next-line
compiler = require('@mdx-js/mdx');
} catch (err) {
throw new Error(
'When you use `mdx` option, make sure to install `@mdx-js/mdx`.'
);
}

const src = compiler.sync(content);
const scriptKind = typescript.ScriptKind.TSX;

const finalContent = `
/* tslint:disable */
import * as React from 'react';
declare class MDXTag extends React.Component<{ name: string; components: any; parentName?: string; props?: any }> {
public render(): JSX.Element;
}
${src}`.replace(
/export default class MDXContent extends React.Component \{/,
`export default class MDXContent extends React.Component<{components: any}> {
private layout: any;
public static isMDXComponent: boolean = true;`
);

return {
scriptKind,
content: finalContent
};
}
}
5 changes: 5 additions & 0 deletions src/ProgramType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ProgramType {
typescript = 'TypeScript',
vue = 'vue',
mdx = 'mdx'
}
25 changes: 21 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { FsHelper } from './FsHelper';
import { Message } from './Message';

import { getForkTsCheckerWebpackPluginHooks, legacyHookMap } from './hooks';
import { ProgramType } from './ProgramType';

const checkerPluginName = 'fork-ts-checker-webpack-plugin';

Expand Down Expand Up @@ -49,6 +50,7 @@ interface Options {
memoryLimit: number;
workers: number;
vue: boolean;
mdx: boolean;
useTypescriptIncrementalApi: boolean;
measureCompilationTime: boolean;
}
Expand Down Expand Up @@ -122,7 +124,7 @@ class ForkTsCheckerWebpackPlugin {

private service?: childProcess.ChildProcess;

private vue: boolean;
private type: ProgramType;

private measureTime: boolean;
private performance: any;
Expand Down Expand Up @@ -205,13 +207,28 @@ class ForkTsCheckerWebpackPlugin {

this.validateVersions();

this.vue = options.vue === true; // default false
if (options.vue && options.mdx) {
throw new Error('cannot check vue and mdx at the same time!');
} else if (options.vue) {
this.type = ProgramType.vue;
} else if (options.mdx) {
this.type = ProgramType.mdx;
} else {
this.type = ProgramType.typescript;
}

this.useTypescriptIncrementalApi =
options.useTypescriptIncrementalApi === undefined
? semver.gte(this.typescriptVersion, '3.0.0') && !this.vue
? semver.gte(this.typescriptVersion, '3.0.0') &&
this.type === ProgramType.typescript
: options.useTypescriptIncrementalApi;

if (this.type === ProgramType.mdx && this.useTypescriptIncrementalApi) {
throw new Error(
'The `mdx` option cannot be used in combination with `useTypescriptIncrementalApi`.'
);
}

this.measureTime = options.measureCompilationTime === true;
if (this.measureTime) {
// Node 8+ only
Expand Down Expand Up @@ -573,7 +590,7 @@ class ForkTsCheckerWebpackPlugin {
MEMORY_LIMIT: this.memoryLimit,
CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors,
USE_INCREMENTAL_API: this.useTypescriptIncrementalApi === true,
VUE: this.vue
TYPE: this.type
},
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
}
Expand Down
3 changes: 2 additions & 1 deletion src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
makeCreateNormalizedMessageFromDiagnostic,
makeCreateNormalizedMessageFromRuleFailure
} from './NormalizedMessageFactories';
import { ProgramType } from './ProgramType';

const typescript: typeof ts = require(process.env.TYPESCRIPT_PATH!);

Expand Down Expand Up @@ -45,7 +46,7 @@ const checker: IncrementalCheckerInterface =
parseInt(process.env.WORK_NUMBER!, 10) || 0,
parseInt(process.env.WORK_DIVISION!, 10) || 1,
process.env.CHECK_SYNTACTIC_ERRORS === 'true',
process.env.VUE === 'true'
(process.env.TYPE || ProgramType.typescript) as ProgramType
);

async function run(cancellationToken: CancellationToken) {
Expand Down

0 comments on commit e6e1cc3

Please sign in to comment.