Skip to content

Commit

Permalink
chore(typescript): Always use ParsedCommandLine (#162)
Browse files Browse the repository at this point in the history
* chore(typescript): Always use ParsedCommandLine

* Update packages/typescript/tsconfig.json

Co-Authored-By: Andrew Powell <[email protected]>

* Add default options and enforce noEmit

Co-authored-by: Andrew Powell <[email protected]>
  • Loading branch information
NotWoods and shellscape committed Jan 23, 2020
1 parent 4ecf8a3 commit 8d35423
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 117 deletions.
62 changes: 16 additions & 46 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,23 @@
import { createFilter } from '@rollup/pluginutils';
import * as path from 'path';

import { Plugin } from 'rollup';
import * as defaultTs from 'typescript';

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

import {
adjustCompilerOptions,
getDefaultOptions,
parseCompilerOptions,
readTsConfig,
validateModuleType
} from './options';
import { diagnosticToWarning, emitDiagnostics } from './diagnostics';
import { getTsLibCode, TSLIB_ID } from './tslib';
import { getPluginOptions, parseTypescriptConfig } from './options';
import { TSLIB_ID } from './tslib';

export default function typescript(options: RollupTypescriptOptions = {}): Plugin {
const opts = Object.assign({}, options);

const filter = createFilter(
opts.include || ['*.ts+(|x)', '**/*.ts+(|x)'],
opts.exclude || ['*.d.ts', '**/*.d.ts']
);
delete opts.include;
delete opts.exclude;

// Allow users to override the TypeScript version used for transpilation and tslib version used for helpers.
const ts: typeof import('typescript') = opts.typescript || defaultTs;
delete opts.typescript;

const tslib = getTsLibCode(opts);
delete opts.tslib;

// Load options from `tsconfig.json` unless explicitly asked not to.
const tsConfig =
opts.tsconfig === false ? { compilerOptions: {} } : readTsConfig(ts, opts.tsconfig);
delete opts.tsconfig;

// Since the CompilerOptions aren't designed for the Rollup
// use case, we'll adjust them for use with Rollup.
tsConfig.compilerOptions = adjustCompilerOptions(tsConfig.compilerOptions);

Object.assign(tsConfig.compilerOptions, getDefaultOptions(), adjustCompilerOptions(opts));

// Verify that we're targeting ES2015 modules.
validateModuleType(tsConfig.compilerOptions.module);

const { options: compilerOptions, errors } = parseCompilerOptions(ts, tsConfig);
const { filter, tsconfig, compilerOptions, tslib, typescript: ts } = getPluginOptions(options);
const parsedConfig = parseTypescriptConfig(ts, tsconfig, compilerOptions);

return {
name: 'typescript',

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

this.error(`@rollup/plugin-typescript: Couldn't process compiler options`);
}
Expand All @@ -64,9 +29,14 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
}

if (!importer) return null;
const containingFile = importer.split('\\').join('/');
const containingFile = importer.split(path.win32.sep).join(path.posix.sep);

const result = ts.nodeModuleNameResolver(importee, containingFile, compilerOptions, ts.sys);
const result = ts.nodeModuleNameResolver(
importee,
containingFile,
parsedConfig.options,
ts.sys
);

if (result.resolvedModule && result.resolvedModule.resolvedFileName) {
if (result.resolvedModule.resolvedFileName.endsWith('.d.ts')) {
Expand All @@ -92,7 +62,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
const transformed = ts.transpileModule(code, {
fileName: id,
reportDiagnostics: true,
compilerOptions
compilerOptions: parsedConfig.options
});

emitDiagnostics(ts, this, transformed.diagnostics);
Expand Down
275 changes: 221 additions & 54 deletions packages/typescript/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,248 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';

export function getDefaultOptions() {
import { createFilter } from '@rollup/pluginutils';
import * as defaultTs from 'typescript';

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

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

/** Properties of `CompilerOptions` that are normally enums */
interface EnumCompilerOptions {
module: string;
moduleResolution: string;
newLine: string;
jsx: string;
target: string;
}

/** Typescript compiler options */
type CompilerOptions = import('typescript').CompilerOptions;
/** JSON representation of Typescript compiler options */
type JsonCompilerOptions = Omit<CompilerOptions, keyof EnumCompilerOptions> & EnumCompilerOptions;
/** Compiler options set by the plugin user. */
type PartialCustomOptions = Partial<CompilerOptions> | Partial<JsonCompilerOptions>;

const DEFAULT_COMPILER_OPTIONS: PartialCustomOptions = {
module: 'esnext',
sourceMap: true,
noEmitOnError: true
};

const FORCED_COMPILER_OPTIONS: Partial<CompilerOptions> = {
// See: https://github.com/rollup/rollup-plugin-typescript/issues/45
// See: https://github.com/rollup/rollup-plugin-typescript/issues/142
declaration: false,
// Delete the `declarationMap` option, as it will cause an error, because we have
// deleted the `declaration` option.
declarationMap: false,
incremental: false,
// eslint-disable-next-line no-undefined
tsBuildInfoFile: undefined,
// Always use tslib
noEmitHelpers: true,
importHelpers: true,
// Typescript needs to emit the code for us to work with
noEmit: false,
emitDeclarationOnly: false,
// Preventing Typescript from resolving code may break compilation
noResolve: false
};

/**
* Separate the Rollup plugin options from the Typescript compiler options,
* and normalize the Rollup options.
* @returns Object with normalized options:
* - `filter`: Checks if a file should be included.
* - `tsconfig`: Path to a tsconfig, or directive to ignore tsconfig.
* - `compilerOptions`: Custom Typescript compiler options that override tsconfig.
* - `typescript`: Instance of Typescript library (possibly custom).
* - `tslib`: ESM code from the tslib helper library (possibly)
*/
export function getPluginOptions(options: RollupTypescriptOptions) {
const { include, exclude, tsconfig, typescript, tslib, ...compilerOptions } = options;

const filter = createFilter(
include || ['*.ts+(|x)', '**/*.ts+(|x)'],
exclude || ['*.d.ts', '**/*.d.ts']
);

return {
noEmitHelpers: true,
module: 'ESNext',
sourceMap: true,
importHelpers: true
filter,
tsconfig,
compilerOptions: compilerOptions as PartialCustomOptions,
typescript: typescript || defaultTs,
tslib: getTsLibCode(tslib)
};
}

export function readTsConfig(ts: typeof import('typescript'), tsconfigPath: string | undefined) {
if (tsconfigPath && !ts.sys.fileExists(tsconfigPath)) {
throw new Error(`Could not find specified tsconfig.json at ${tsconfigPath}`);
}
const existingTsConfig = tsconfigPath || ts.findConfigFile(process.cwd(), ts.sys.fileExists);
if (!existingTsConfig) {
return {};
}

const tsconfig = ts.readConfigFile(existingTsConfig, (path) => readFileSync(path, 'utf8'));
/**
* Finds the path to the tsconfig file relative to the current working directory.
* @param relativePath Relative tsconfig path given by the user.
* If `false` is passed, then a null path is returned.
* @returns The absolute path, or null if the file does not exist.
*/
function getTsConfigPath(ts: typeof import('typescript'), relativePath: string | false) {
if (relativePath === false) return null;

if (!tsconfig.config || !tsconfig.config.compilerOptions) return { compilerOptions: {} };
// Resolve path to file. `tsConfigOption` defaults to 'tsconfig.json'.
const tsConfigPath = resolve(process.cwd(), relativePath || 'tsconfig.json');

const extendedTsConfig: string = tsconfig.config.extends;
if (tsconfigPath && extendedTsConfig) {
tsconfig.config.extends = resolve(process.cwd(), existingTsConfig, '..', extendedTsConfig);
if (!ts.sys.fileExists(tsConfigPath)) {
if (relativePath) {
// If an explicit path was provided but no file was found, throw
throw new Error(`Could not find specified tsconfig.json at ${tsConfigPath}`);
} else {
return null;
}
}

return tsconfig.config;
return tsConfigPath;
}

export function adjustCompilerOptions(options: any) {
const opts = Object.assign({}, options);
// Set `sourceMap` to `inlineSourceMap` if it's a boolean
// under the assumption that both are never specified simultaneously.
if (typeof opts.inlineSourceMap === 'boolean') {
opts.sourceMap = opts.inlineSourceMap;
delete opts.inlineSourceMap;
/**
* Tries to read the tsconfig file at `tsConfigPath`.
* @param tsConfigPath Absolute path to tsconfig JSON file.
* @param explicitPath If true, the path was set by the plugin user.
* If false, the path was computed automatically.
*/
function readTsConfigFile(ts: typeof import('typescript'), tsConfigPath: string) {
const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
if (error) {
throw Object.assign(Error(), diagnosticToWarning(ts, error));
}

// Delete some options to prevent compilation error.
// See: https://github.com/rollup/rollup-plugin-typescript/issues/45
// See: https://github.com/rollup/rollup-plugin-typescript/issues/142
delete opts.declaration;
// Delete the `declarationMap` option, as it will cause an error, because we have
// deleted the `declaration` option.
delete opts.declarationMap;
delete opts.incremental;
delete opts.tsBuildInfoFile;
return opts;
const extendedTsConfig: string = config?.extends;
if (extendedTsConfig) {
// Get absolute path of `extends`, starting at basedir of the tsconfig file.
config.extends = resolve(process.cwd(), tsConfigPath, '..', extendedTsConfig);
}

return config || {};
}

export function parseCompilerOptions(ts: typeof import('typescript'), tsConfig: any) {
const parsed = ts.convertCompilerOptionsFromJson(tsConfig.compilerOptions, process.cwd());
/**
* Returns true if any of the `compilerOptions` contain an enum value (i.e.: ts.ScriptKind) rather than a string.
* This indicates that the internal CompilerOptions type is used rather than the JsonCompilerOptions.
*/
function containsEnumOptions(
compilerOptions: PartialCustomOptions
): compilerOptions is Partial<CompilerOptions> {
const enums: Array<keyof EnumCompilerOptions> = [
'module',
'target',
'jsx',
'moduleResolution',
'newLine'
];
return enums.some((prop) => prop in compilerOptions && typeof compilerOptions[prop] === 'number');
}

// let typescript load inheritance chain if there are base configs
const extendedConfig = tsConfig.extends
? ts.parseJsonConfigFileContent(tsConfig, ts.sys, process.cwd(), parsed.options)
: null;
/**
* Mutates the compiler options to normalize some values for Rollup.
* @param compilerOptions Compiler options to _mutate_.
*/
function normalizeCompilerOptions(
ts: typeof import('typescript'),
compilerOptions: CompilerOptions
) {
/* eslint-disable no-param-reassign */

return {
options: extendedConfig?.options || parsed.options,
errors: parsed.errors.concat(extendedConfig?.errors || [])
};
if (compilerOptions.inlineSourceMap) {
// Force separate source map files for Rollup to work with.
compilerOptions.sourceMap = true;
compilerOptions.inlineSourceMap = false;
} else if (typeof compilerOptions.sourceMap !== 'boolean') {
// Default to using source maps.
// If the plugin user sets sourceMap to false we keep that option.
compilerOptions.sourceMap = true;
}

switch (compilerOptions.module) {
case ts.ModuleKind.ES2015:
case ts.ModuleKind.ESNext:
case ts.ModuleKind.CommonJS:
// OK module type
return;
case ts.ModuleKind.None:
case ts.ModuleKind.AMD:
case ts.ModuleKind.UMD:
case ts.ModuleKind.System: {
// Invalid module type
const moduleType = ts.ModuleKind[compilerOptions.module];
throw new Error(
`@rollup/plugin-typescript: The module kind should be 'ES2015' or 'ESNext, found: '${moduleType}'`
);
}
default:
// Unknown or unspecified module type, force ESNext
compilerOptions.module = ts.ModuleKind.ESNext;
}
}

/**
* Verify that we're targeting ES2015 modules.
* @param moduleType `tsConfig.compilerOptions.module`
* Parse the Typescript config to use with the plugin.
* @param ts Typescript library instance.
* @param tsconfig Path to the tsconfig file, or `false` to ignore the file.
* @param compilerOptions Options passed to the plugin directly for Typescript.
*
* @returns Parsed tsconfig.json file with some important properties:
* - `options`: Parsed compiler options.
* - `fileNames` Type definition files that should be included in the build.
* - `errors`: Any errors from parsing the config file.
*/
export function validateModuleType(moduleType: string) {
const esModuleTypes = new Set(['ES2015', 'ES6', 'ESNEXT', 'COMMONJS']);
export function parseTypescriptConfig(
ts: typeof import('typescript'),
tsconfig: RollupTypescriptOptions['tsconfig'],
compilerOptions: PartialCustomOptions
): import('typescript').ParsedCommandLine {
const cwd = process.cwd();
let parsedConfig: import('typescript').ParsedCommandLine;

// Resolve path to file. If file is not found, pass undefined path to `parseJsonConfigFileContent`.
// eslint-disable-next-line no-undefined
const tsConfigPath = getTsConfigPath(ts, tsconfig) || undefined;
const tsConfigFile = tsConfigPath ? readTsConfigFile(ts, tsConfigPath) : {};

if (!esModuleTypes.has(moduleType.toUpperCase())) {
throw new Error(
`@rollup/plugin-typescript: The module kind should be 'ES2015' or 'ESNext, found: '${moduleType}'`
// If compilerOptions has enums, it represents an CompilerOptions object instead of parsed JSON.
// This determines where the data is passed to the parser.
if (containsEnumOptions(compilerOptions)) {
parsedConfig = ts.parseJsonConfigFileContent(
{
...tsConfigFile,
compilerOptions: {
...DEFAULT_COMPILER_OPTIONS,
...tsConfigFile.compilerOptions
}
},
ts.sys,
cwd,
{ ...compilerOptions, ...FORCED_COMPILER_OPTIONS },
tsConfigPath
);
} else {
parsedConfig = ts.parseJsonConfigFileContent(
{
...tsConfigFile,
compilerOptions: {
...DEFAULT_COMPILER_OPTIONS,
...tsConfigFile.compilerOptions,
...compilerOptions
}
},
ts.sys,
cwd,
FORCED_COMPILER_OPTIONS,
tsConfigPath
);
}

// We only want to automatically add ambient declaration files.
// Normal script files are handled by Rollup.
parsedConfig.fileNames = parsedConfig.fileNames.filter((file) => file.endsWith('.d.ts'));
normalizeCompilerOptions(ts, parsedConfig.options);

return parsedConfig;
}
Loading

0 comments on commit 8d35423

Please sign in to comment.