@rollup/plugin-typescript
Version:
Seamless integration between Rollup and TypeScript.
1,020 lines (1,002 loc) • 45 kB
JavaScript
import * as path from 'path';
import path__default, { resolve as resolve$1, dirname, relative } from 'path';
import * as fs from 'fs';
import fs__default, { readFileSync, promises } from 'fs';
import * as os from 'os';
import { createFilter } from '@rollup/pluginutils';
import typescript$1 from 'typescript';
import { fileURLToPath } from 'url';
import resolve from 'resolve';
/**
* 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
*/
function createFormattingHost(ts, compilerOptions) {
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()
};
}
/**
* Create a helper for resolving modules using Typescript.
* @param ts custom typescript implementation
* @param host Typescript host that extends {@link ModuleResolutionHost}
* @param filter
* with methods for sanitizing filenames and getting compiler options.
*/
function createModuleResolver(ts, host, filter) {
const compilerOptions = host.getCompilationSettings();
const cache = ts.createModuleResolutionCache(process.cwd(), host.getCanonicalFileName, compilerOptions);
const moduleHost = { ...ts.sys, ...host };
return (moduleName, containingFile, redirectedReference, mode) => {
const { resolvedModule } = ts.resolveModuleName(moduleName, containingFile, compilerOptions, moduleHost, cache, redirectedReference, mode);
/**
* If the module's path contains 'node_modules', ts considers it an external library and refuses to compile it,
* so we have to change the value of `isExternalLibraryImport` to false if it's true
* */
if ((resolvedModule === null || resolvedModule === void 0 ? void 0 : resolvedModule.isExternalLibraryImport) && filter(resolvedModule === null || resolvedModule === void 0 ? void 0 : resolvedModule.resolvedFileName)) {
resolvedModule.isExternalLibraryImport = false;
}
return resolvedModule;
};
}
// const resolveIdAsync = (file: string, opts: AsyncOpts) =>
// new Promise<string>((fulfil, reject) =>
// resolveId(file, opts, (err, contents) =>
// err || typeof contents === 'undefined' ? reject(err) : fulfil(contents)
// )
// );
const resolveId = (file, opts) => resolve.sync(file, opts);
/**
* Returns code asynchronously for the tslib helper library.
*/
const getTsLibPath = () => {
// Note: This isn't preferable, but we've no other way to test this bit. Removing the tslib devDep
// during the test run doesn't work due to the nature of the pnpm flat node_modules, and
// other workspace dependencies that depenend upon tslib.
try {
// eslint-disable-next-line no-underscore-dangle
return resolveId(process.env.__TSLIB_TEST_PATH__ || 'tslib/tslib.es6.js', {
// @ts-ignore import.meta.url is allowed because the Rollup plugin injects the correct module format
basedir: fileURLToPath(new URL('.', import.meta.url))
});
}
catch (_) {
return null;
}
};
/**
* 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 custom).
*/
const getPluginOptions = (options) => {
const { cacheDir, exclude, include, filterRoot, noForceEmit, transformers, recreateTransformersOnRebuild, tsconfig, tslib, typescript, outputToFilesystem, compilerOptions,
// previously was compilerOptions
...extra } = options;
return {
cacheDir,
include,
exclude,
filterRoot,
noForceEmit: noForceEmit || false,
tsconfig,
compilerOptions: { ...extra, ...compilerOptions },
typescript: typescript || typescript$1,
tslib: tslib || getTsLibPath(),
transformers,
// Only enable when explicitly set to true to avoid truthy string pitfalls in JS configs
recreateTransformersOnRebuild: recreateTransformersOnRebuild === true,
outputToFilesystem
};
};
/**
* Converts a Typescript type error into an equivalent Rollup warning object.
*/
function diagnosticToWarning(ts, host, diagnostic) {
const pluginCode = `TS${diagnostic.code}`;
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
// Build a Rollup warning object from the diagnostics object.
const warning = {
pluginCode,
message: `@rollup/plugin-typescript ${pluginCode}: ${message}`
};
if (diagnostic.file) {
// Add information about the file location
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
warning.loc = {
column: character + 1,
line: line + 1,
file: diagnostic.file.fileName
};
if (host) {
// Extract a code frame from Typescript
const formatted = ts.formatDiagnosticsWithColorAndContext([diagnostic], host);
// Typescript only exposes this formatter as a string prefixed with the flattened message.
// We need to remove it here since Rollup treats the properties as separate parts.
let frame = formatted.slice(formatted.indexOf(message) + message.length);
const newLine = host.getNewLine();
if (frame.startsWith(newLine)) {
frame = frame.slice(frame.indexOf(newLine) + newLine.length);
}
warning.frame = frame;
}
}
return warning;
}
const DEFAULT_COMPILER_OPTIONS = {
module: 'esnext',
skipLibCheck: true
};
const OVERRIDABLE_EMIT_COMPILER_OPTIONS = {
noEmit: false,
emitDeclarationOnly: false
};
const FORCED_COMPILER_OPTIONS = {
// Always use tslib
noEmitHelpers: true,
importHelpers: true,
// Preventing Typescript from resolving code may break compilation
noResolve: false
};
/* eslint-disable no-param-reassign */
const DIRECTORY_PROPS = ['outDir', 'declarationDir'];
/**
* Mutates the compiler options to convert paths from relative to absolute.
* This should be used with compiler options passed through the Rollup plugin options,
* not those found from loading a tsconfig.json file.
* @param compilerOptions Compiler options to _mutate_.
* @param relativeTo Paths are resolved relative to this path.
*/
function makePathsAbsolute(compilerOptions, relativeTo) {
for (const pathProp of DIRECTORY_PROPS) {
if (compilerOptions[pathProp]) {
compilerOptions[pathProp] = resolve$1(relativeTo, compilerOptions[pathProp]);
}
}
}
/**
* Mutates the compiler options to normalize some values for Rollup.
* @param compilerOptions Compiler options to _mutate_.
* @returns True if the source map compiler option was not initially set.
*/
function normalizeCompilerOptions(ts, compilerOptions) {
let autoSetSourceMap = false;
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;
// Using inlineSources to make sure typescript generate source content
// instead of source path.
compilerOptions.inlineSources = true;
autoSetSourceMap = true;
}
switch (compilerOptions.module) {
case ts.ModuleKind.ES2015:
case ts.ModuleKind.ESNext:
case ts.ModuleKind.Node16:
case ts.ModuleKind.NodeNext:
case ts.ModuleKind.CommonJS:
// OK module type
return autoSetSourceMap;
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', 'ESNext', 'node16' or 'nodenext', found: '${moduleType}'`);
}
default:
// Unknown or unspecified module type, force ESNext
compilerOptions.module = ts.ModuleKind.ESNext;
}
return autoSetSourceMap;
}
const { ModuleKind: ModuleKind$1, ModuleResolutionKind } = typescript$1;
function makeForcedCompilerOptions(noForceEmit) {
return { ...FORCED_COMPILER_OPTIONS, ...(noForceEmit ? {} : OVERRIDABLE_EMIT_COMPILER_OPTIONS) };
}
/**
* Finds the path to the tsconfig file relative to the current working directory.
* @param ts Custom typescript implementation
* @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, relativePath) {
if (relativePath === false)
return null;
// Resolve path to file. `tsConfigOption` defaults to 'tsconfig.json'.
const tsConfigPath = resolve$1(process.cwd(), relativePath || 'tsconfig.json');
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 tsConfigPath;
}
/**
* Tries to read the tsconfig file at `tsConfigPath`.
* @param ts Custom typescript implementation
* @param tsConfigPath Absolute path to tsconfig JSON file.
*/
function readTsConfigFile(ts, tsConfigPath) {
const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
if (error) {
throw Object.assign(Error(), diagnosticToWarning(ts, null, error));
}
return config || {};
}
/**
* 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) {
const enums = [
'module',
'target',
'jsx',
'moduleResolution',
'newLine'
];
return enums.some((prop) => prop in compilerOptions && typeof compilerOptions[prop] === 'number');
}
/**
* The module resolution kind is a function of the resolved `compilerOptions.module`.
* This needs to be set explicitly for `resolveModuleName` to select the correct resolution method
*/
function setModuleResolutionKind(parsedConfig) {
const moduleKind = parsedConfig.options.module;
// Fallback if `parsedConfig.options.moduleResolution` is not set
const moduleResolution = moduleKind === ModuleKind$1.Node16
? ModuleResolutionKind.Node16
: moduleKind === ModuleKind$1.NodeNext
? ModuleResolutionKind.NodeNext
: ModuleResolutionKind.NodeJs;
return {
...parsedConfig,
options: {
moduleResolution,
...parsedConfig.options
}
};
}
const configCache = new Map();
/**
* 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.
* @param noForceEmit Whether to respect emit options from {@link tsconfig}
*
* @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.
*/
function parseTypescriptConfig(ts, tsconfig, compilerOptions, noForceEmit) {
/* eslint-disable no-undefined */
const cwd = process.cwd();
makePathsAbsolute(compilerOptions, cwd);
let parsedConfig;
// 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) : {};
const basePath = tsConfigPath ? dirname(tsConfigPath) : cwd;
// 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 = setModuleResolutionKind(ts.parseJsonConfigFileContent({
...tsConfigFile,
compilerOptions: {
...DEFAULT_COMPILER_OPTIONS,
...tsConfigFile.compilerOptions
}
}, ts.sys, basePath, { ...compilerOptions, ...makeForcedCompilerOptions(noForceEmit) }, tsConfigPath, undefined, undefined, configCache));
}
else {
parsedConfig = setModuleResolutionKind(ts.parseJsonConfigFileContent({
...tsConfigFile,
compilerOptions: {
...DEFAULT_COMPILER_OPTIONS,
...tsConfigFile.compilerOptions,
...compilerOptions
}
}, ts.sys, basePath, makeForcedCompilerOptions(noForceEmit), tsConfigPath, undefined, undefined, configCache));
}
const autoSetSourceMap = normalizeCompilerOptions(ts, parsedConfig.options);
return {
...parsedConfig,
autoSetSourceMap
};
}
/**
* If errors are detected in the parsed options,
* display all of them as warnings then emit an error.
*/
function emitParsedOptionsErrors(ts, context, parsedOptions) {
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`);
}
}
/**
* Validate that the `compilerOptions.sourceMap` option matches `outputOptions.sourcemap`.
* @param context Rollup plugin context used to emit warnings.
* @param compilerOptions Typescript compiler options.
* @param outputOptions Rollup output options.
* @param autoSetSourceMap True if the `compilerOptions.sourceMap` property was set to `true`
* by the plugin, not the user.
*/
function validateSourceMap(context, compilerOptions, outputOptions, autoSetSourceMap) {
if (compilerOptions.sourceMap && !outputOptions.sourcemap && !autoSetSourceMap) {
context.warn(`@rollup/plugin-typescript: Rollup 'sourcemap' option must be set to generate source maps.`);
}
else if (!compilerOptions.sourceMap && outputOptions.sourcemap) {
context.warn(`@rollup/plugin-typescript: Typescript 'sourceMap' compiler option must be set to generate source maps.`);
}
}
/**
* Validate that the out directory used by Typescript can be controlled by Rollup.
* @param context Rollup plugin context used to emit errors.
* @param compilerOptions Typescript compiler options.
* @param outputOptions Rollup output options.
*/
function validatePaths(context, compilerOptions, outputOptions) {
if (compilerOptions.out) {
context.error(`@rollup/plugin-typescript: Deprecated Typescript compiler option 'out' is not supported. Use 'outDir' instead.`);
}
else if (compilerOptions.outFile) {
context.error(`@rollup/plugin-typescript: Typescript compiler option 'outFile' is not supported. Use 'outDir' instead.`);
}
let outputDir = outputOptions.dir;
if (outputOptions.file) {
outputDir = dirname(outputOptions.file);
}
for (const dirProperty of DIRECTORY_PROPS) {
if (compilerOptions[dirProperty] && outputDir) {
// Checks if the given path lies within Rollup output dir
if (outputOptions.dir) {
const fromRollupDirToTs = relative(outputDir, compilerOptions[dirProperty]);
if (fromRollupDirToTs.startsWith('..')) {
context.error(`@rollup/plugin-typescript: Path of Typescript compiler option '${dirProperty}' must be located inside Rollup 'dir' option.`);
}
}
else if (dirProperty === 'outDir') {
const fromTsDirToRollup = relative(compilerOptions[dirProperty], outputDir);
if (fromTsDirToRollup.startsWith('..')) {
context.error(`@rollup/plugin-typescript: Path of Typescript compiler option '${dirProperty}' must be located inside the same directory as the Rollup 'file' option.`);
}
}
else {
const fromTsDirToRollup = relative(outputDir, compilerOptions[dirProperty]);
if (fromTsDirToRollup.startsWith('..')) {
context.error(`@rollup/plugin-typescript: Path of Typescript compiler option '${dirProperty}' must be located inside the same directory as the Rollup 'file' option.`);
}
}
}
}
if (compilerOptions.declaration || compilerOptions.declarationMap || compilerOptions.composite) {
if (DIRECTORY_PROPS.every((dirProperty) => !compilerOptions[dirProperty])) {
context.error(`@rollup/plugin-typescript: You are using one of Typescript's compiler options 'declaration', 'declarationMap' or 'composite'. ` +
`In this case 'outDir' or 'declarationDir' must be specified to generate declaration files.`);
}
}
}
/**
* Checks if the given OutputFile represents some code
*/
function isCodeOutputFile(name) {
return !isMapOutputFile(name) && !isDeclarationOutputFile(name);
}
/**
* Checks if the given OutputFile represents some source map
*/
function isMapOutputFile(name) {
return name.endsWith('.map');
}
/**
* Checks if the given OutputFile represents some TypeScript source map
*/
function isTypeScriptMapOutputFile(name) {
return name.endsWith('ts.map');
}
/**
* Checks if the given OutputFile represents some declaration
*/
function isDeclarationOutputFile(name) {
return /\.d\.[cm]?ts$/.test(name);
}
/**
* Returns the content of a filename either from the current
* typescript compiler instance or from the cached content.
* @param fileName The filename for the contents to retrieve
* @param emittedFiles The files emitted in the current typescript instance
* @param tsCache A cache to files cached by Typescript
*/
function getEmittedFile(fileName, emittedFiles, tsCache) {
let code;
if (fileName) {
if (emittedFiles.has(fileName)) {
code = emittedFiles.get(fileName);
}
else {
code = tsCache.getCached(fileName);
}
}
return code;
}
/**
* Finds the corresponding emitted Javascript files for a given Typescript file.
* @param id Path to the Typescript file.
* @param emittedFiles Map of file names to source code,
* containing files emitted by the Typescript compiler.
*/
function findTypescriptOutput(ts, parsedOptions, id, emittedFiles, tsCache) {
const emittedFileNames = ts.getOutputFileNames(parsedOptions, id, !ts.sys.useCaseSensitiveFileNames);
const codeFile = emittedFileNames.find(isCodeOutputFile);
const mapFile = emittedFileNames.find(isMapOutputFile);
return {
code: getEmittedFile(codeFile, emittedFiles, tsCache),
map: getEmittedFile(mapFile, emittedFiles, tsCache),
declarations: emittedFileNames.filter((name) => name !== codeFile && name !== mapFile)
};
}
function normalizePath(fileName) {
return fileName.split(path.win32.sep).join(path.posix.sep);
}
async function emitFile({ dir }, outputToFilesystem, context, filePath, fileSource) {
const normalizedFilePath = normalizePath(filePath);
// const normalizedPath = normalizePath(filePath);
// Note: `dir` can be a value like `dist` in which case, `path.relative` could result in a value
// of something like `'../.tsbuildinfo'. Our else-case below needs to mimic `path.relative`
// returning a dot-notated relative path, so the first if-then branch is entered into
const relativePath = dir ? path.relative(dir, normalizedFilePath) : '..';
// legal paths do not start with . nor .. : https://github.com/rollup/rollup/issues/3507#issuecomment-616495912
if (relativePath.startsWith('..')) {
if (outputToFilesystem == null) {
context.warn(`@rollup/plugin-typescript: outputToFilesystem option is defaulting to true.`);
}
if (outputToFilesystem !== false) {
await promises.mkdir(path.dirname(normalizedFilePath), { recursive: true });
await promises.writeFile(normalizedFilePath, fileSource);
}
}
else {
context.emitFile({
type: 'asset',
fileName: relativePath,
source: fileSource
});
}
}
// import { resolveIdAsync } from './tslib';
const { ModuleKind } = typescript$1;
const pluginName = '@rollup/plugin-typescript';
const moduleErrorMessage = `
${pluginName}: Rollup requires that TypeScript produces ES Modules. Unfortunately your configuration specifies a
"module" other than "esnext". Unless you know what you're doing, please change "module" to "esnext"
in the target tsconfig.json file or plugin options.`.replace(/\n/g, '');
const tsLibErrorMessage = `${pluginName}: Could not find module 'tslib', which is required by this plugin. Is it installed?`;
let undef;
const validModules = [
ModuleKind.ES2015,
ModuleKind.ES2020,
ModuleKind.ESNext,
ModuleKind.Node16,
ModuleKind.NodeNext,
undef
];
// eslint-disable-next-line import/prefer-default-export
const preflight = ({ config, context, inputPreserveModules, tslib }) => {
if (!validModules.includes(config.options.module)) {
context.warn(moduleErrorMessage);
}
if (!inputPreserveModules && tslib === null) {
context.error(tsLibErrorMessage);
}
};
// `Cannot compile modules into 'es6' when targeting 'ES5' or lower.`
const CANNOT_COMPILE_ESM = 1204;
/**
* Emit a Rollup warning or error for a Typescript type error.
*/
function emitDiagnostic(ts, context, host, diagnostic) {
if (diagnostic.code === CANNOT_COMPILE_ESM)
return;
const { noEmitOnError } = host.getCompilationSettings();
// 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);
}
}
function buildDiagnosticReporter(ts, context, host) {
return function reportDiagnostics(diagnostic) {
emitDiagnostic(ts, context, host, diagnostic);
};
}
/**
* Merges all received custom transformer definitions into a single CustomTransformers object
*/
function mergeTransformers(builder, ...input) {
// List of all transformer stages
const transformerTypes = ['after', 'afterDeclarations', 'before'];
const accumulator = {
after: [],
afterDeclarations: [],
before: []
};
let program;
let typeChecker;
input.forEach((transformers) => {
if (!transformers) {
// Skip empty arguments lists
return;
}
transformerTypes.forEach((stage) => {
getTransformers(transformers[stage]).forEach((transformer) => {
if (!transformer) {
// Skip empty
return;
}
if ('type' in transformer) {
if (typeof transformer.factory === 'function') {
let factory;
if (transformer.type === 'program') {
const currentProgram = program !== null && program !== void 0 ? program : builder.getProgram();
// Pass a getter so transformers can access the latest Program in watch mode
factory = transformer.factory(currentProgram, () => builder.getProgram());
program = currentProgram;
}
else {
const currentProgram = program !== null && program !== void 0 ? program : builder.getProgram();
const currentTypeChecker = typeChecker !== null && typeChecker !== void 0 ? typeChecker : currentProgram.getTypeChecker();
factory = transformer.factory(currentTypeChecker);
program = currentProgram;
typeChecker = currentTypeChecker;
}
// Forward the requested reference to the custom transformer factory
if (factory) {
accumulator[stage].push(factory);
}
}
}
else {
// Add normal transformer factories as is
accumulator[stage].push(transformer);
}
});
});
});
return accumulator;
}
function getTransformers(transformers) {
return transformers || [];
}
const { DiagnosticCategory } = typescript$1;
// @see https://github.com/microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json
// eslint-disable-next-line no-shadow
var DiagnosticCode;
(function (DiagnosticCode) {
DiagnosticCode[DiagnosticCode["FILE_CHANGE_DETECTED"] = 6032] = "FILE_CHANGE_DETECTED";
DiagnosticCode[DiagnosticCode["FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES"] = 6193] = "FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES";
DiagnosticCode[DiagnosticCode["FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES"] = 6194] = "FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES";
})(DiagnosticCode || (DiagnosticCode = {}));
function createDeferred(timeout) {
let promise;
let resolve = () => { };
if (timeout) {
promise = Promise.race([
new Promise((r) => setTimeout(r, timeout, true)),
new Promise((r) => (resolve = r))
]);
}
else {
promise = new Promise((r) => (resolve = r));
}
return { promise, resolve };
}
/**
* Typescript watch program helper to sync Typescript watch status with Rollup hooks.
*/
class WatchProgramHelper {
constructor() {
this._startDeferred = null;
this._finishDeferred = null;
}
watch(timeout = 1000) {
// Race watcher start promise against a timeout in case Typescript and Rollup change detection is not in sync.
this._startDeferred = createDeferred(timeout);
this._finishDeferred = createDeferred();
}
handleStatus(diagnostic) {
// Fullfil deferred promises by Typescript diagnostic message codes.
if (diagnostic.category === DiagnosticCategory.Message) {
switch (diagnostic.code) {
case DiagnosticCode.FILE_CHANGE_DETECTED:
this.resolveStart();
break;
case DiagnosticCode.FOUND_1_ERROR_WATCHING_FOR_FILE_CHANGES:
case DiagnosticCode.FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES:
this.resolveFinish();
break;
}
}
}
resolveStart() {
if (this._startDeferred) {
this._startDeferred.resolve(false);
this._startDeferred = null;
}
}
resolveFinish() {
if (this._finishDeferred) {
this._finishDeferred.resolve(false);
this._finishDeferred = null;
}
}
async wait() {
var _a;
if (this._startDeferred) {
const timeout = await this._startDeferred.promise;
// If there is no file change detected by Typescript skip deferred promises.
if (timeout) {
this._startDeferred = null;
this._finishDeferred = null;
}
await ((_a = this._finishDeferred) === null || _a === void 0 ? void 0 : _a.promise);
}
}
}
/**
* 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.
* @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
*/
function createWatchHost(ts, context, { formatHost, parsedOptions, writeFile, status, resolveModule, transformers, recreateTransformersOnRebuild }) {
const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
const baseHost = ts.createWatchCompilerHost(parsedOptions.fileNames, parsedOptions.options, ts.sys, createProgram, buildDiagnosticReporter(ts, context, formatHost), status, parsedOptions.projectReferences);
let createdTransformers;
return {
...baseHost,
/** Override the created program so an in-memory emit is used */
afterProgramCreate(program) {
// Optionally recompute custom transformers for each new builder program in watch mode
// so factories capture the current Program/TypeChecker and any provided getters can
// return the latest values. When disabled (default), legacy behavior reuses the
// same factories across rebuilds.
if (recreateTransformersOnRebuild) {
createdTransformers = void 0;
}
const origEmit = program.emit;
// eslint-disable-next-line no-param-reassign
program.emit = (targetSourceFile, _, cancellationToken, emitOnlyDtsFiles, customTransformers) => {
createdTransformers !== null && createdTransformers !== void 0 ? createdTransformers : (createdTransformers = typeof transformers === 'function'
? transformers(program.getProgram())
: mergeTransformers(program, transformers, customTransformers));
return origEmit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, createdTransformers);
};
return baseHost.afterProgramCreate(program);
},
/** Add helper to deal with module resolution */
resolveModuleNames(moduleNames, containingFile, _reusedNames, redirectedReference, _optionsOnlyWithNewerTsVersions, containingSourceFile) {
return moduleNames.map((moduleName, i) => {
var _a;
const mode = containingSourceFile
? (_a = ts.getModeForResolutionAtIndex) === null || _a === void 0 ? void 0 : _a.call(ts, containingSourceFile, i)
: undefined; // eslint-disable-line no-undefined
return resolveModule(moduleName, containingFile, redirectedReference, mode);
});
}
};
}
function createWatchProgram(ts, context, options) {
return ts.createWatchProgram(createWatchHost(ts, context, options));
}
/** Creates the folders needed given a path to a file to be saved*/
const createFileFolder = (filePath) => {
const folderPath = path__default.dirname(filePath);
fs__default.mkdirSync(folderPath, { recursive: true });
};
class TSCache {
constructor(cacheFolder = '.rollup.cache') {
this._cacheFolder = cacheFolder;
}
/** Returns the path to the cached file */
cachedFilename(fileName) {
return path__default.join(this._cacheFolder, fileName.replace(/^([a-zA-Z]+):/, '$1'));
}
/** Emits a file in the cache folder */
cacheCode(fileName, code) {
const cachedPath = this.cachedFilename(fileName);
createFileFolder(cachedPath);
fs__default.writeFileSync(cachedPath, code);
}
/** Checks if a file is in the cache */
isCached(fileName) {
return fs__default.existsSync(this.cachedFilename(fileName));
}
/** Read a file from the cache given the output name*/
getCached(fileName) {
let code;
if (this.isCached(fileName)) {
code = fs__default.readFileSync(this.cachedFilename(fileName), { encoding: 'utf-8' });
}
return code;
}
}
function typescript(options = {}) {
const { cacheDir, compilerOptions, exclude, filterRoot, include, outputToFilesystem, noForceEmit, transformers, recreateTransformersOnRebuild, tsconfig, tslib, typescript: ts } = getPluginOptions(options);
const tsCache = new TSCache(cacheDir);
const emittedFiles = new Map();
const watchProgramHelper = new WatchProgramHelper();
let autoOutDir = null;
// Centralize temp outDir cleanup to avoid duplication/drift across hooks
const cleanupAutoOutDir = () => {
if (!autoOutDir)
return;
try {
fs.rmSync(autoOutDir, { recursive: true, force: true });
}
catch {
// ignore cleanup failures
}
autoOutDir = null;
};
// Ensure the TypeScript watch program is closed and temp outDir is cleaned
// even if closing throws. Call this from lifecycle hooks that need teardown.
const closeProgramAndCleanup = () => {
try {
// ESLint doesn't understand optional chaining
// eslint-disable-next-line
program === null || program === void 0 ? void 0 : program.close();
}
finally {
cleanupAutoOutDir();
}
};
const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions, noForceEmit);
// When processing JS via allowJs, redirect emit output away from source files
// to avoid TS5055 (cannot write file because it would overwrite input file).
// We only set a temp outDir if the user did not configure one.
if (parsedOptions.options.allowJs && !parsedOptions.options.outDir) {
// Create a unique temporary outDir to avoid TS5055 when emitting JS
autoOutDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rollup-plugin-typescript-allowjs-'));
parsedOptions.options.outDir = autoOutDir;
}
// Determine default include pattern. By default we only process TS files.
// When the consumer enables `allowJs` in their tsconfig/compiler options,
// also include common JS extensions so modern JS syntax in .js files is
// downleveled by TypeScript as expected.
const defaultInclude = parsedOptions.options.allowJs
? '{,**/}*.{cts,mts,ts,tsx,js,jsx,mjs,cjs}'
: '{,**/}*.{cts,mts,ts,tsx}';
// Build filter exclusions, ensuring we never re-process TypeScript emit outputs.
// Always exclude the effective outDir (user-provided or the auto-created temp dir).
const filterExclude = Array.isArray(exclude) ? [...exclude] : exclude ? [exclude] : [];
// When auto-expanding to include JS (allowJs) and the user did not provide
// custom include/exclude patterns, avoid transforming third-party code by
// default by excluding node_modules.
if (parsedOptions.options.allowJs && !include && !exclude) {
filterExclude.push('**/node_modules/**');
}
const effectiveOutDir = parsedOptions.options.outDir
? path.resolve(parsedOptions.options.outDir)
: null;
// Determine the base used for containment checks. If pattern resolution is disabled
// (filterRoot === false), fall back to process.cwd() so we don't accidentally
// exclude sources when e.g. outDir='.'.
const willResolvePatterns = filterRoot !== false;
// Only treat string values of `filterRoot` as a base directory; booleans (e.g., true)
// should not flow into path resolution. Fallback to the tsconfig `rootDir` when not set.
const configuredBase = willResolvePatterns
? typeof filterRoot === 'string'
? filterRoot
: parsedOptions.options.rootDir
: null;
const filterBaseAbs = configuredBase ? path.resolve(configuredBase) : null;
if (effectiveOutDir) {
// Avoid excluding sources: skip when the filter base (or cwd fallback) lives inside outDir.
// Use path.relative with root equality guard for cross-platform correctness.
const baseForContainment = filterBaseAbs !== null && filterBaseAbs !== void 0 ? filterBaseAbs : process.cwd();
const outDirContainsFilterBase = (() => {
// Different roots (e.g., drive letters on Windows) cannot be in a parent/child relationship.
// Normalize Windows drive-letter case before comparison to avoid false mismatches.
const getRoot = (p) => {
const r = path.parse(p).root;
return process.platform === 'win32' ? r.toLowerCase() : r;
};
if (getRoot(effectiveOutDir) !== getRoot(baseForContainment))
return false;
const rel = path.relative(effectiveOutDir, baseForContainment);
// rel === '' -> same dir; absolute or '..' => outside
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
})();
if (!outDirContainsFilterBase) {
filterExclude.push(normalizePath(path.join(effectiveOutDir, '**')));
}
}
const filter = createFilter(include || defaultInclude, filterExclude, {
// Guard against non-string truthy values (e.g., boolean true). Only strings are valid
// for `resolve`; `false` disables resolution. Otherwise, fall back to `rootDir`.
resolve: typeof filterRoot === 'string'
? filterRoot
: filterRoot === false
? false
: parsedOptions.options.rootDir || process.cwd()
});
parsedOptions.fileNames = parsedOptions.fileNames.filter(filter);
const formatHost = createFormattingHost(ts, parsedOptions.options);
const resolveModule = createModuleResolver(ts, formatHost, filter);
let program = null;
return {
name: 'typescript',
buildStart(rollupOptions) {
emitParsedOptionsErrors(ts, this, parsedOptions);
preflight({
config: parsedOptions,
context: this,
// TODO drop rollup@3 support and remove
inputPreserveModules: rollupOptions
.preserveModules,
tslib
});
// Fixes a memory leak https://github.com/rollup/plugins/issues/322
if (this.meta.watchMode !== true) {
// eslint-disable-next-line
program === null || program === void 0 ? void 0 : program.close();
program = null;
}
if (!program) {
program = createWatchProgram(ts, this, {
formatHost,
resolveModule,
parsedOptions,
writeFile(fileName, data) {
if (parsedOptions.options.composite || parsedOptions.options.incremental) {
tsCache.cacheCode(fileName, data);
}
emittedFiles.set(fileName, data);
},
status(diagnostic) {
watchProgramHelper.handleStatus(diagnostic);
},
transformers,
recreateTransformersOnRebuild
});
}
},
watchChange(id) {
if (!filter(id))
return;
watchProgramHelper.watch();
},
buildEnd() {
if (this.meta.watchMode !== true) {
closeProgramAndCleanup();
}
},
// Ensure program is closed and temp outDir is removed exactly once when watch stops
closeWatcher() {
closeProgramAndCleanup();
},
renderStart(outputOptions) {
validateSourceMap(this, parsedOptions.options, outputOptions, parsedOptions.autoSetSourceMap);
validatePaths(this, parsedOptions.options, outputOptions);
},
resolveId(importee, importer) {
if (importee === 'tslib') {
return tslib;
}
if (!importer)
return null;
// Convert path from windows separators to posix separators
const containingFile = normalizePath(importer);
// when using node16 or nodenext module resolution, we need to tell ts if
// we are resolving to a commonjs or esnext module
const mode = typeof ts.getImpliedNodeFormatForFile === 'function'
? ts.getImpliedNodeFormatForFile(
// @ts-expect-error
containingFile, undefined, // eslint-disable-line no-undefined
{ ...ts.sys, ...formatHost }, parsedOptions.options)
: undefined; // eslint-disable-line no-undefined
// eslint-disable-next-line no-undefined
const resolved = resolveModule(importee, containingFile, undefined, mode);
if (resolved) {
if (/\.d\.[cm]?ts/.test(resolved.extension))
return null;
if (!filter(resolved.resolvedFileName))
return null;
return path.normalize(resolved.resolvedFileName);
}
return null;
},
async load(id) {
if (!filter(id))
return null;
this.addWatchFile(id);
await watchProgramHelper.wait();
const fileName = normalizePath(id);
if (!parsedOptions.fileNames.includes(fileName)) {
// Discovered new file that was not known when originally parsing the TypeScript config
parsedOptions.fileNames.push(fileName);
}
const output = findTypescriptOutput(ts, parsedOptions, id, emittedFiles, tsCache);
return output.code != null ? output : null;
},
async generateBundle(outputOptions) {
const declarationAndTypeScriptMapFiles = [...emittedFiles.keys()].filter((fileName) => isDeclarationOutputFile(fileName) || isTypeScriptMapOutputFile(fileName));
declarationAndTypeScriptMapFiles.forEach((id) => {
const code = getEmittedFile(id, emittedFiles, tsCache);
if (!code || !parsedOptions.options.declaration) {
return;
}
let baseDir;
if (outputOptions.dir) {
baseDir = outputOptions.dir;
}
else if (outputOptions.file) {
// the bundle output directory used by rollup when outputOptions.file is used instead of outputOptions.dir
baseDir = path.dirname(outputOptions.file);
}
if (!baseDir)
return;
this.emitFile({
type: 'asset',
fileName: normalizePath(path.relative(baseDir, id)),
source: code
});
});
const tsBuildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(parsedOptions.options);
if (tsBuildInfoPath) {
const tsBuildInfoSource = emittedFiles.get(tsBuildInfoPath);
// https://github.com/rollup/plugins/issues/681
if (tsBuildInfoSource) {
await emitFile(outputOptions, outputToFilesystem, this, tsBuildInfoPath, tsBuildInfoSource);
}
}
}
};
}
export { typescript as default };
//# sourceMappingURL=index.js.map