Skip to content

Commit

Permalink
chore(typescript): refactor compiler host (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
NotWoods authored Feb 20, 2020
1 parent a72d189 commit 0fdde33
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 78 deletions.
34 changes: 34 additions & 0 deletions packages/typescript/src/diagnostics/emit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PluginContext } from 'rollup';

import { DiagnosticsHost } from './host';
import diagnosticToWarning from './toWarning';

// `Cannot compile modules into 'es6' when targeting 'ES5' or lower.`
const CANNOT_COMPILE_ESM = 1204;

/**
* For each type error reported by Typescript, emit a Rollup warning or error.
*/
export default function emitDiagnostics(
ts: typeof import('typescript'),
context: PluginContext,
host: DiagnosticsHost,
diagnostics: readonly import('typescript').Diagnostic[] | undefined
) {
if (!diagnostics) return;
const { noEmitOnError } = host.getCompilationSettings();

diagnostics
.filter((diagnostic) => diagnostic.code !== CANNOT_COMPILE_ESM)
.forEach((diagnostic) => {
// Build a Rollup warning object from the diagnostics object.
const warning = diagnosticToWarning(ts, host, diagnostic);

// Errors are fatal. Otherwise emit warnings.
if (noEmitOnError && diagnostic.category === ts.DiagnosticCategory.Error) {
context.error(warning);
} else {
context.warn(warning);
}
});
}
38 changes: 38 additions & 0 deletions packages/typescript/src/diagnostics/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type FormatDiagnosticsHost = import('typescript').FormatDiagnosticsHost;

export interface DiagnosticsHost extends FormatDiagnosticsHost {
getCompilationSettings(): import('typescript').CompilerOptions;
}

/**
* Create a format diagnostics host to use with the Typescript type checking APIs.
* Typescript hosts are used to represent the user's system,
* with an API for checking case sensitivity etc.
* @param compilerOptions Typescript compiler options. Affects functions such as `getNewLine`.
* @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
*/
export default function createFormattingHost(
ts: typeof import('typescript'),
compilerOptions: import('typescript').CompilerOptions
): DiagnosticsHost {
return {
/** Returns the compiler options for the project. */
getCompilationSettings: () => compilerOptions,
/** Returns the current working directory. */
getCurrentDirectory: () => process.cwd(),
/** Returns the string that corresponds with the selected `NewLineKind`. */
getNewLine() {
switch (compilerOptions.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return ts.sys.newLine;
}
},
/** Returns a lower case name on case insensitive systems, otherwise the original name. */
getCanonicalFileName: (fileName) =>
ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase()
};
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
import { PluginContext, RollupLogProps } from 'rollup';

// `Cannot compile modules into 'es6' when targeting 'ES5' or lower.`
const CANNOT_COMPILE_ESM = 1204;

/**
* For each type error reported by Typescript, emit a Rollup warning or error.
*/
export function emitDiagnostics(
ts: typeof import('typescript'),
context: PluginContext,
host: import('typescript').FormatDiagnosticsHost &
Pick<import('typescript').LanguageServiceHost, 'getCompilationSettings'>,
diagnostics: readonly import('typescript').Diagnostic[] | undefined
) {
if (!diagnostics) return;
const { noEmitOnError } = host.getCompilationSettings();

diagnostics
.filter((diagnostic) => diagnostic.code !== CANNOT_COMPILE_ESM)
.forEach((diagnostic) => {
// Build a Rollup warning object from the diagnostics object.
const warning = diagnosticToWarning(ts, host, diagnostic);

// Errors are fatal. Otherwise emit warnings.
if (noEmitOnError && diagnostic.category === ts.DiagnosticCategory.Error) {
context.error(warning);
} else {
context.warn(warning);
}
});
}
import { RollupLogProps } from 'rollup';

/**
* Converts a Typescript type error into an equivalent Rollup warning object.
*/
export function diagnosticToWarning(
export default function diagnosticToWarning(
ts: typeof import('typescript'),
host: import('typescript').FormatDiagnosticsHost | null,
diagnostic: import('typescript').Diagnostic
Expand Down
39 changes: 10 additions & 29 deletions packages/typescript/src/host.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import createModuleResolver, { Resolver } from './resolver';
import createFormattingHost, { DiagnosticsHost } from './diagnostics/host';
import createModuleResolutionHost, { ModuleResolutionHost } from './moduleResolution/host';
import createModuleResolver, { Resolver } from './moduleResolution/resolver';

type BaseHost = import('typescript').LanguageServiceHost &
import('typescript').ModuleResolutionHost &
import('typescript').FormatDiagnosticsHost;
type BaseHost = import('typescript').LanguageServiceHost & ModuleResolutionHost & DiagnosticsHost;

export interface TypescriptHost extends BaseHost {
/**
Expand Down Expand Up @@ -35,6 +35,10 @@ interface File {

/**
* Create a language service host to use with the Typescript compiler & type checking APIs.
* Typescript hosts are used to represent the user's system,
* with an API for reading files, checking directories and case sensitivity etc.
* This host creates a local file cache which can be updated with `addFile`.
*
* @param parsedOptions Parsed options for Typescript.
* @param parsedOptions.options Typescript compiler options. Affects functions such as `getNewLine`.
* @param parsedOptions.fileNames Declaration files to include for typechecking.
Expand Down Expand Up @@ -79,16 +83,10 @@ export default function createHost(

let resolver: Resolver;
const host: TypescriptHost = {
getCompilationSettings: () => parsedOptions.options,
getCurrentDirectory: () => process.cwd(),
getNewLine: () => getNewLine(ts, parsedOptions.options.newLine),
getCanonicalFileName: (fileName) =>
ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
...createModuleResolutionHost(ts),
...createFormattingHost(ts, parsedOptions.options),
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getDefaultLibFileName: ts.getDefaultLibFilePath,
getDirectories: ts.sys.getDirectories,
directoryExists: ts.sys.directoryExists,
realpath: ts.sys.realpath,
readDirectory: ts.sys.readDirectory,
readFile(fileName, encoding) {
const file = files.get(fileName);
Expand All @@ -109,20 +107,3 @@ export default function createHost(

return host;
}

/**
* Returns the string that corresponds with the selected `NewLineKind`.
*/
function getNewLine(
ts: typeof import('typescript'),
kind: import('typescript').NewLineKind | undefined
) {
switch (kind) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return ts.sys.newLine;
}
}
16 changes: 7 additions & 9 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,27 @@ import { Plugin } from 'rollup';

import { RollupTypescriptOptions } from '../types';

import { diagnosticToWarning, emitDiagnostics } from './diagnostics';
import emitDiagnostics from './diagnostics/emit';
import createFormattingHost from './diagnostics/host';
import getDocumentRegistry from './documentRegistry';
import createHost from './host';
import { getPluginOptions, parseTypescriptConfig } from './options';
import { emitParsedOptionsErrors, getPluginOptions, parseTypescriptConfig } from './options';
import typescriptOutputToRollupTransformation from './outputToRollupTransformation';
import { TSLIB_ID } from './tslib';

export default function typescript(options: RollupTypescriptOptions = {}): Plugin {
const { filter, tsconfig, compilerOptions, tslib, typescript: ts } = getPluginOptions(options);

const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions);
const formatHost = createFormattingHost(ts, parsedOptions.options);
const host = createHost(ts, parsedOptions);
const services = ts.createLanguageService(host, getDocumentRegistry(ts, process.cwd()));

return {
name: 'typescript',

buildStart() {
if (parsedOptions.errors.length > 0) {
parsedOptions.errors.forEach((error) => this.warn(diagnosticToWarning(ts, host, error)));

this.error(`@rollup/plugin-typescript: Couldn't process compiler options`);
}
emitParsedOptionsErrors(ts, this, parsedOptions);
},

resolveId(importee, importer) {
Expand Down Expand Up @@ -68,7 +66,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
const allDiagnostics = ([] as import('typescript').Diagnostic[])
.concat(services.getSyntacticDiagnostics(id))
.concat(services.getSemanticDiagnostics(id));
emitDiagnostics(ts, this, host, allDiagnostics);
emitDiagnostics(ts, this, formatHost, allDiagnostics);

throw new Error(`Couldn't compile ${id}`);
}
Expand All @@ -79,7 +77,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
generateBundle() {
const program = services.getProgram();
if (program == null) return;
emitDiagnostics(ts, this, host, ts.getPreEmitDiagnostics(program));
emitDiagnostics(ts, this, formatHost, ts.getPreEmitDiagnostics(program));
}
};
}
19 changes: 19 additions & 0 deletions packages/typescript/src/moduleResolution/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type ModuleResolutionHost = import('typescript').ModuleResolutionHost;

/**
* Creates a module resolution host to use with the Typescript compiler API.
* Typescript hosts are used to represent the user's system,
* with an API for reading files, checking directories and case sensitivity etc.
* @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
*/
export default function createModuleResolutionHost(
ts: typeof import('typescript')
): ModuleResolutionHost {
return {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
directoryExists: ts.sys.directoryExists,
realpath: ts.sys.realpath,
getDirectories: ts.sys.getDirectories
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type ModuleResolverHost = import('typescript').ModuleResolutionHost &
Pick<import('typescript').FormatDiagnosticsHost, 'getCanonicalFileName'> &
Pick<import('typescript').LanguageServiceHost, 'getCompilationSettings'>;
import { DiagnosticsHost } from '../diagnostics/host';

type ModuleResolutionHost = import('typescript').ModuleResolutionHost;
type ModuleResolverHost = ModuleResolutionHost & DiagnosticsHost;

export type Resolver = (
moduleName: string,
Expand All @@ -9,6 +10,8 @@ export type Resolver = (

/**
* Create a helper for resolving modules using Typescript.
* @param host Typescript host that extends `ModuleResolutionHost`
* with methods for sanitizing filenames and getting compiler options.
*/
export default function createModuleResolver(
ts: typeof import('typescript'),
Expand Down
19 changes: 18 additions & 1 deletion packages/typescript/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { readFileSync } from 'fs';
import { resolve } from 'path';

import { createFilter } from '@rollup/pluginutils';
import { PluginContext } from 'rollup';
import * as defaultTs from 'typescript';

import { RollupTypescriptOptions } from '../types';

import { diagnosticToWarning } from './diagnostics';
import diagnosticToWarning from './diagnostics/toWarning';
import { getTsLibCode } from './tslib';

/** Properties of `CompilerOptions` that are normally enums */
Expand Down Expand Up @@ -240,3 +241,19 @@ export function parseTypescriptConfig(

return parsedConfig;
}

/**
* If errors are detected in the parsed options,
* display all of them as warnings then emit an error.
*/
export function emitParsedOptionsErrors(
ts: typeof import('typescript'),
context: PluginContext,
parsedOptions: import('typescript').ParsedCommandLine
) {
if (parsedOptions.errors.length > 0) {
parsedOptions.errors.forEach((error) => context.warn(diagnosticToWarning(ts, null, error)));

context.error(`@rollup/plugin-typescript: Couldn't process compiler options`);
}
}
4 changes: 1 addition & 3 deletions packages/typescript/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,7 @@ test.serial('should support extends property with node resolution', async (t) =>

const bundle = await rollup({
input: 'main.tsx',
plugins: [
typescript()
],
plugins: [typescript()],
onwarn
});
const code = await getCode(bundle, outputOptions);
Expand Down

0 comments on commit 0fdde33

Please sign in to comment.