Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add concept for pluggable program factories #227

Closed
Closed
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
103 changes: 103 additions & 0 deletions src/BasicProgramFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// tslint:disable-next-line:no-implicit-dependencies
import * as ts from 'typescript'; // Imported for types alone; actual requires take place in methods below
import * as fs from 'fs';
import * as path from 'path';

import { PluggableProgramFactoryInterface } from './PluggableProgramFactory';
import { FilesRegister } from './FilesRegister';
import { FilesWatcher } from './FilesWatcher';

/**
* @deprecated do not call directly from other files - exported just for unit testing
*/
export function loadProgramConfig(
typescript: typeof ts,
configFile: string,
compilerOptions: object
) {
const tsconfig = typescript.readConfigFile(
configFile,
typescript.sys.readFile
).config;

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

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

return parsed;
}

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

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);
});
}

return files.getData(filePath).source;
};

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

const BasicProgramFactory: PluggableProgramFactoryInterface = {
watchExtensions: ['.ts', '.tsx'],

loadProgram(config) {
const programConfig =
config.programConfig ||
loadProgramConfig(
config.typescript,
config.configFile,
config.compilerOptions
);

const program = createProgram(
config.typescript,
programConfig,
config.files,
config.watcher!,
config.oldProgram!
);

return { programConfig, program };
}
};

export default BasicProgramFactory;
126 changes: 17 additions & 109 deletions src/IncrementalChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { WorkSet } from './WorkSet';
import { NormalizedMessage } from './NormalizedMessage';
import { CancellationToken } from './CancellationToken';
import * as minimatch from 'minimatch';
import { VueProgram } from './VueProgram';
import { FsHelper } from './FsHelper';
import { IncrementalCheckerInterface } from './IncrementalCheckerInterface';
import { PluggableProgramFactoryInterface } from './PluggableProgramFactory';
import BasicProgramFactory from './BasicProgramFactory';

export class IncrementalChecker implements IncrementalCheckerInterface {
// it's shared between compilations
Expand Down Expand Up @@ -59,36 +60,11 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
private workNumber: number = 0,
private workDivision: number = 1,
private checkSyntacticErrors: boolean = false,
private vue: boolean = false
private pluggableProgramFactory: PluggableProgramFactoryInterface = BasicProgramFactory
) {
this.hasFixedConfig = typeof this.linterConfigFile === 'string';
}

public static loadProgramConfig(
typescript: typeof ts,
configFile: string,
compilerOptions: object
) {
const tsconfig = typescript.readConfigFile(
configFile,
typescript.sys.readFile
).config;

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

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

return parsed;
}

private getLinterConfig: (
file: string
) => ConfigurationFile | undefined = makeGetLinterConfig(
Expand All @@ -97,47 +73,6 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
this.context
);

private 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;

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);
});
}

return files.getData(filePath).source;
};

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

private createLinter(program: ts.Program) {
// tslint:disable-next-line:no-implicit-dependencies
const tslint = require('tslint');
Expand All @@ -161,9 +96,7 @@ export class IncrementalChecker implements IncrementalCheckerInterface {

public nextIteration() {
if (!this.watcher) {
const watchExtensions = this.vue
? ['.ts', '.tsx', '.vue']
: ['.ts', '.tsx'];
const watchExtensions = this.pluggableProgramFactory.watchExtensions;
this.watcher = new FilesWatcher(this.watchPaths, watchExtensions);

// connect watcher with register
Expand Down Expand Up @@ -193,50 +126,25 @@ export class IncrementalChecker implements IncrementalCheckerInterface {
}
}

this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram();
const { program, programConfig } = this.pluggableProgramFactory.loadProgram(
{
typescript: this.typescript,
configFile: this.programConfigFile,
programConfig: this.programConfig,
compilerOptions: this.compilerOptions,
files: this.files,
watcher: this.watcher,
oldProgram: this.program
}
);
this.programConfig = programConfig;
this.program = program;

if (this.linterConfigFile) {
this.linter = this.createLinter(this.program!);
}
}

private loadVueProgram() {
this.programConfig =
this.programConfig ||
VueProgram.loadProgramConfig(
this.typescript,
this.programConfigFile,
this.compilerOptions
);

return VueProgram.createProgram(
this.typescript,
this.programConfig,
path.dirname(this.programConfigFile),
this.files,
this.watcher!,
this.program!
);
}

private loadDefaultProgram() {
this.programConfig =
this.programConfig ||
IncrementalChecker.loadProgramConfig(
this.typescript,
this.programConfigFile,
this.compilerOptions
);

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

public getDiagnostics(cancellationToken: CancellationToken) {
const { program } = this;
if (!program) {
Expand Down
45 changes: 45 additions & 0 deletions src/PluggableProgramFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// tslint:disable-next-line:no-implicit-dependencies
import * as ts from 'typescript'; // Imported for types alone; actual requires take place in methods below
import { FilesRegister } from './FilesRegister';
import { FilesWatcher } from './FilesWatcher';

export interface CreateProgramConfig {
typescript: typeof ts;
configFile: string;
programConfig?: ts.ParsedCommandLine;
compilerOptions: object;
files: FilesRegister;
watcher?: FilesWatcher;
oldProgram?: ts.Program;
}

export interface CreateProgramResult {
programConfig: ts.ParsedCommandLine;
program: ts.Program;
}

export interface PluggableProgramFactoryInterface {
loadProgram(config: CreateProgramConfig): CreateProgramResult;
watchExtensions: string[];
}

export function loadPluggableProgramFactory(
pluggableProgramFactoryImport: string = __dirname + '/BasicProgramFactory'
) {
let factory = require(pluggableProgramFactoryImport);
factory = factory.default || factory;

if (!Array.isArray(factory.watchExtensions)) {
throw new Error(
'pluggableProgramFactoryImport does not implement watchExtensions as an Array'
);
}

if (typeof factory.loadProgram !== 'function') {
throw new Error(
'pluggableProgramFactoryImport does not implement loadProgram as a function'
);
}

return factory as PluggableProgramFactoryInterface;
}
31 changes: 31 additions & 0 deletions src/VueProgramFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as path from 'path';

import { PluggableProgramFactoryInterface } from './PluggableProgramFactory';
import { VueProgram } from './VueProgram';

const VueProgramFactory: PluggableProgramFactoryInterface = {
watchExtensions: ['.ts', '.tsx', '.vue'],

loadProgram(config) {
const programConfig =
config.programConfig ||
VueProgram.loadProgramConfig(
config.typescript,
config.configFile,
config.compilerOptions
);

const program = VueProgram.createProgram(
config.typescript,
programConfig,
path.dirname(config.configFile),
config.files,
config.watcher!,
config.oldProgram!
);

return { programConfig, program };
}
};

export default VueProgramFactory;
Loading