rollup-plugin-dts
Version:
A rollup plugin that will bundle up your .d.ts definition files.
1,266 lines (1,252 loc) • 87.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var path = require('node:path');
var ts = require('typescript');
var node_module = require('node:module');
var MagicString = require('magic-string');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
function resolveDefaultOptions(options) {
return {
...options,
compilerOptions: options.compilerOptions ?? {},
respectExternal: options.respectExternal ?? false,
};
}
const DTS_EXTENSIONS = /\.d\.(c|m)?tsx?$/;
const JSON_EXTENSIONS = /\.json$/;
const SUPPORTED_EXTENSIONS = /((\.d)?\.(c|m)?(t|j)sx?|\.json)$/;
function trimExtension(path) {
return path.replace(SUPPORTED_EXTENSIONS, "");
}
function getDeclarationId(path) {
return path.replace(SUPPORTED_EXTENSIONS, ".d.ts");
}
function parse(fileName, code) {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
}
const formatHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? (f) => f : (f) => f.toLowerCase(),
};
const DEFAULT_OPTIONS = {
// Ensure ".d.ts" modules are generated
declaration: true,
// Skip ".js" generation
noEmit: false,
emitDeclarationOnly: true,
// Skip code generation when error occurs
noEmitOnError: true,
// Avoid extra work
checkJs: false,
declarationMap: false,
skipLibCheck: true,
// Ensure TS2742 errors are visible
preserveSymlinks: true,
// Ensure we can parse the latest code
target: ts.ScriptTarget.ESNext,
// Allows importing `*.json`
resolveJsonModule: true,
};
const configByPath = new Map();
const logCache = (...args) => (process.env.DTS_LOG_CACHE ? console.log("[cache]", ...args) : null);
/**
* Caches the config for every path between two given paths.
*
* It starts from the first path and walks up the directory tree until it reaches the second path.
*/
function cacheConfig([fromPath, toPath], config) {
logCache(fromPath);
configByPath.set(fromPath, config);
while (fromPath !== toPath &&
// make sure we're not stuck in an infinite loop
fromPath !== path__namespace.dirname(fromPath)) {
fromPath = path__namespace.dirname(fromPath);
logCache("up", fromPath);
if (configByPath.has(fromPath))
return logCache("has", fromPath);
configByPath.set(fromPath, config);
}
}
function getCompilerOptions(input, overrideOptions, overrideConfigPath) {
const compilerOptions = { ...DEFAULT_OPTIONS, ...overrideOptions };
let dirName = path__namespace.dirname(input);
let dtsFiles = [];
// if a custom config is provided we'll use that as the cache key since it will always be used
const cacheKey = overrideConfigPath || dirName;
if (!configByPath.has(cacheKey)) {
logCache("miss", cacheKey);
const configPath = overrideConfigPath
? path__namespace.resolve(process.cwd(), overrideConfigPath)
: ts.findConfigFile(dirName, ts.sys.fileExists);
if (!configPath) {
return { dtsFiles, dirName, compilerOptions };
}
const inputDirName = dirName;
dirName = path__namespace.dirname(configPath);
const { config, error } = ts.readConfigFile(configPath, ts.sys.readFile);
if (error) {
console.error(ts.formatDiagnostic(error, formatHost));
return { dtsFiles, dirName, compilerOptions };
}
logCache("tsconfig", config);
const configContents = ts.parseJsonConfigFileContent(config, ts.sys, dirName);
if (overrideConfigPath) {
// if a custom config is provided, we always only use that one
cacheConfig([overrideConfigPath, overrideConfigPath], configContents);
}
else {
// cache the config for all directories between input and resolved config path
cacheConfig([inputDirName, dirName], configContents);
}
}
else {
logCache("HIT", cacheKey);
}
const { fileNames, options, errors } = configByPath.get(cacheKey);
dtsFiles = fileNames.filter((name) => DTS_EXTENSIONS.test(name));
if (errors.length) {
console.error(ts.formatDiagnostics(errors, formatHost));
return { dtsFiles, dirName, compilerOptions };
}
return {
dtsFiles,
dirName,
compilerOptions: {
...options,
...compilerOptions,
},
};
}
function createProgram$1(fileName, overrideOptions, tsconfig) {
const { dtsFiles, compilerOptions } = getCompilerOptions(fileName, overrideOptions, tsconfig);
return ts.createProgram([fileName].concat(Array.from(dtsFiles)), compilerOptions, ts.createCompilerHost(compilerOptions, true));
}
function createPrograms(input, overrideOptions, tsconfig) {
const programs = [];
const dtsFiles = new Set();
let inputs = [];
let dirName = "";
let compilerOptions = {};
for (let main of input) {
if (DTS_EXTENSIONS.test(main)) {
continue;
}
main = path__namespace.resolve(main);
const options = getCompilerOptions(main, overrideOptions, tsconfig);
options.dtsFiles.forEach(dtsFiles.add, dtsFiles);
if (!inputs.length) {
inputs.push(main);
({ dirName, compilerOptions } = options);
continue;
}
if (options.dirName === dirName) {
inputs.push(main);
}
else {
const host = ts.createCompilerHost(compilerOptions, true);
const program = ts.createProgram(inputs.concat(Array.from(dtsFiles)), compilerOptions, host);
programs.push(program);
inputs = [main];
({ dirName, compilerOptions } = options);
}
}
if (inputs.length) {
const host = ts.createCompilerHost(compilerOptions, true);
const program = ts.createProgram(inputs.concat(Array.from(dtsFiles)), compilerOptions, host);
programs.push(program);
}
return programs;
}
function getCodeFrame() {
let codeFrameColumns = undefined;
try {
({ codeFrameColumns } = require("@babel/code-frame"));
return codeFrameColumns;
}
catch {
try {
const esmRequire = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('rollup-plugin-dts.cjs', document.baseURI).href)));
({ codeFrameColumns } = esmRequire("@babel/code-frame"));
return codeFrameColumns;
}
catch { }
}
return undefined;
}
function getLocation(node) {
const sourceFile = node.getSourceFile();
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
return {
start: { line: start.line + 1, column: start.character + 1 },
end: { line: end.line + 1, column: end.character + 1 },
};
}
function frameNode(node) {
const codeFrame = getCodeFrame();
const sourceFile = node.getSourceFile();
const code = sourceFile.getFullText();
const location = getLocation(node);
if (codeFrame) {
return ("\n" +
codeFrame(code, location, {
highlightCode: true,
}));
}
else {
return `\n${location.start.line}:${location.start.column}: \`${node.getFullText().trim()}\``;
}
}
class UnsupportedSyntaxError extends Error {
constructor(node, message = "Syntax not yet supported") {
super(`${message}\n${frameNode(node)}`);
}
}
class NamespaceFixer {
constructor(sourceFile) {
this.sourceFile = sourceFile;
}
findNamespaces() {
const namespaces = [];
const items = {};
for (const node of this.sourceFile.statements) {
const location = {
start: node.getStart(),
end: node.getEnd(),
};
// Well, this is a big hack:
// For some global `namespace` and `module` declarations, we generate
// some fake IIFE code, so rollup can correctly scan its scope.
// However, rollup will then insert bogus semicolons,
// these `EmptyStatement`s, which are a syntax error and we want to
// remove them. Well, we do that here…
if (ts.isEmptyStatement(node)) {
namespaces.unshift({
name: "",
exports: [],
location,
});
continue;
}
// When generating multiple chunks, rollup links those via import
// statements, obviously. But rollup uses full filenames with typescript extension,
// which typescript does not like. So make sure to change those to javascript extension here.
// `.d.ts` -> `.js`
// `.d.cts` -> `.cjs`
// `.d.mts` -> `.mjs`
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)) {
const { text } = node.moduleSpecifier;
if (text.startsWith(".") && (text.endsWith(".d.ts") || text.endsWith(".d.cts") || text.endsWith(".d.mts"))) {
const start = node.moduleSpecifier.getStart() + 1; // +1 to account for the quote
const end = node.moduleSpecifier.getEnd() - 1; // -1 to account for the quote
namespaces.unshift({
name: "",
exports: [],
location: {
start,
end,
},
textBeforeCodeAfter: text
.replace(/\.d\.ts$/, ".js")
.replace(/\.d\.cts$/, ".cjs")
.replace(/\.d\.mts$/, ".mjs"),
});
}
}
// Remove redundant `{ Foo as Foo }` exports from a namespace which we
// added in pre-processing to fix up broken renaming
if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body)) {
for (const stmt of node.body.statements) {
if (ts.isExportDeclaration(stmt) && stmt.exportClause) {
if (ts.isNamespaceExport(stmt.exportClause)) {
continue;
}
for (const decl of stmt.exportClause.elements) {
if (decl.propertyName && decl.propertyName.getText() == decl.name.getText()) {
namespaces.unshift({
name: "",
exports: [],
location: {
start: decl.propertyName.getEnd(),
end: decl.name.getEnd(),
},
});
}
}
}
}
}
if (ts.isClassDeclaration(node)) {
items[node.name.getText()] = { type: "class", generics: node.typeParameters };
}
else if (ts.isFunctionDeclaration(node)) {
// a function has generics, but these don’t need to be specified explicitly,
// since functions are treated as values.
items[node.name.getText()] = { type: "function" };
}
else if (ts.isInterfaceDeclaration(node)) {
items[node.name.getText()] = { type: "interface", generics: node.typeParameters };
}
else if (ts.isTypeAliasDeclaration(node)) {
items[node.name.getText()] = { type: "type", generics: node.typeParameters };
}
else if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
items[node.name.getText()] = { type: "namespace" };
}
else if (ts.isEnumDeclaration(node)) {
items[node.name.getText()] = { type: "enum" };
}
if (!ts.isVariableStatement(node)) {
continue;
}
const { declarations } = node.declarationList;
if (declarations.length !== 1) {
continue;
}
const decl = declarations[0];
const name = decl.name.getText();
if (!decl.initializer || !ts.isCallExpression(decl.initializer)) {
items[name] = { type: "var" };
continue;
}
const obj = decl.initializer.arguments[0];
if (!decl.initializer.expression.getFullText().includes("/*#__PURE__*/Object.freeze") ||
!ts.isObjectLiteralExpression(obj)) {
continue;
}
const exports = [];
for (const prop of obj.properties) {
if (!ts.isPropertyAssignment(prop) ||
!(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) ||
(prop.name.text !== "__proto__" && !ts.isIdentifier(prop.initializer))) {
throw new UnsupportedSyntaxError(prop, "Expected a property assignment");
}
if (prop.name.text === "__proto__") {
continue;
}
exports.push({
exportedName: prop.name.text,
localName: prop.initializer.getText(),
});
}
// sort in reverse order, since we will do string manipulation
namespaces.unshift({
name,
exports,
location,
});
}
return { namespaces, itemTypes: items };
}
fix() {
let code = this.sourceFile.getFullText();
const { namespaces, itemTypes } = this.findNamespaces();
for (const ns of namespaces) {
const codeAfter = code.slice(ns.location.end);
code = code.slice(0, ns.location.start);
for (const { exportedName, localName } of ns.exports) {
if (exportedName === localName) {
const { type, generics } = itemTypes[localName] || {};
if (type === "interface" || type === "type") {
// an interface is just a type
const typeParams = renderTypeParams(generics);
code += `type ${ns.name}_${exportedName}${typeParams.in} = ${localName}${typeParams.out};\n`;
}
else if (type === "enum" || type === "class") {
// enums and classes are both types and values
const typeParams = renderTypeParams(generics);
code += `type ${ns.name}_${exportedName}${typeParams.in} = ${localName}${typeParams.out};\n`;
code += `declare const ${ns.name}_${exportedName}: typeof ${localName};\n`;
}
else if (type === "namespace") {
// namespaces may contain both types and values
code += `import ${ns.name}_${exportedName} = ${localName};\n`;
}
else {
// functions and vars are just values
code += `declare const ${ns.name}_${exportedName}: typeof ${localName};\n`;
}
}
}
if (ns.name) {
code += `declare namespace ${ns.name} {\n`;
code += ` export {\n`;
for (const { exportedName, localName } of ns.exports) {
if (exportedName === localName) {
code += ` ${ns.name}_${exportedName} as ${exportedName},\n`;
}
else {
code += ` ${localName} as ${exportedName},\n`;
}
}
code += ` };\n`;
code += `}`;
}
code += ns.textBeforeCodeAfter ?? "";
code += codeAfter;
}
return code;
}
}
function renderTypeParams(typeParameters) {
if (!typeParameters || !typeParameters.length) {
return { in: "", out: "" };
}
return {
in: `<${typeParameters.map((param) => param.getText()).join(", ")}>`,
out: `<${typeParameters.map((param) => param.name.getText()).join(", ")}>`,
};
}
let IDs = 1;
/**
* Create a new `Program` for the given `node`:
*/
function createProgram(node) {
return withStartEnd({
type: "Program",
sourceType: "module",
body: [],
}, { start: node.getFullStart(), end: node.getEnd() });
}
/**
* Creates a reference to `id`:
* `_ = ${id}`
*/
function createReference(id) {
const ident = {
type: "Identifier",
name: String(IDs++),
};
return {
ident,
expr: {
type: "AssignmentPattern",
left: ident,
right: id,
},
};
}
function createIdentifier(node) {
return withStartEnd({
type: "Identifier",
name: node.getText(),
}, node);
}
/**
* Create a new Scope which is always included
* `(function (_ = MARKER) {})()`
*/
function createIIFE(range) {
const fn = withStartEnd({
type: "FunctionExpression",
id: null,
params: [],
body: { type: "BlockStatement", body: [] },
}, range);
const iife = withStartEnd({
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: { type: "Identifier", name: String(IDs++) },
arguments: [fn],
optional: false,
},
}, range);
return { fn, iife };
}
/**
* Create a dummy ReturnStatement with an ArrayExpression:
* `return [];`
*/
function createReturn() {
const expr = {
type: "ArrayExpression",
elements: [],
};
return {
expr,
stmt: {
type: "ReturnStatement",
argument: expr,
},
};
}
/**
* Create a new Declaration and Scope for `id`:
* `function ${id}(_ = MARKER) {}`
*/
function createDeclaration(id, range) {
return withStartEnd({
type: "FunctionDeclaration",
id: withStartEnd({
type: "Identifier",
name: ts.idText(id),
}, id),
params: [],
body: { type: "BlockStatement", body: [] },
}, range);
}
function convertExpression(node) {
if (ts.isLiteralExpression(node)) {
return { type: "Literal", value: node.text };
}
if (ts.isPropertyAccessExpression(node)) {
if (ts.isPrivateIdentifier(node.name)) {
throw new UnsupportedSyntaxError(node.name);
}
return withStartEnd({
type: "MemberExpression",
computed: false,
optional: false,
object: convertExpression(node.expression),
property: convertExpression(node.name),
}, {
start: node.expression.getStart(),
end: node.name.getEnd(),
});
}
if (ts.isIdentifier(node)) {
return createIdentifier(node);
}
else if (node.kind == ts.SyntaxKind.NullKeyword) {
return { type: "Literal", value: null };
}
else {
throw new UnsupportedSyntaxError(node);
}
}
function withStartEnd(esNode, nodeOrRange) {
const range = "start" in nodeOrRange ? nodeOrRange : { start: nodeOrRange.getStart(), end: nodeOrRange.getEnd() };
return Object.assign(esNode, range);
}
function matchesModifier(node, flags) {
const nodeFlags = ts.getCombinedModifierFlags(node);
return (nodeFlags & flags) === flags;
}
class LanguageService {
constructor(code) {
this.fileName = "index.d.ts";
const serviceHost = {
getCompilationSettings: () => ({
noEmit: true,
noResolve: true,
skipLibCheck: true,
declaration: false,
checkJs: false,
declarationMap: false,
target: ts.ScriptTarget.ESNext,
}),
getScriptFileNames: () => [this.fileName],
getScriptVersion: () => "1",
getScriptSnapshot: (fileName) => fileName === this.fileName
? ts.ScriptSnapshot.fromString(code)
: undefined,
getCurrentDirectory: () => "",
getDefaultLibFileName: () => "",
fileExists: (fileName) => fileName === this.fileName,
readFile: (fileName) => fileName === this.fileName ? code : undefined,
};
this.service = ts.createLanguageService(serviceHost, ts.createDocumentRegistry(undefined, ""), ts.LanguageServiceMode.PartialSemantic);
}
findReferenceCount(node) {
const referencedSymbols = this.service.findReferences(this.fileName, node.getStart());
if (!referencedSymbols?.length) {
return 0;
}
return referencedSymbols.reduce((total, symbol) => total + symbol.references.length, 0);
}
}
class TypeOnlyFixer {
constructor(fileName, rawCode, sourcemap) {
this.sourcemap = sourcemap;
this.DEBUG = !!process.env.DTS_EXPORTS_FIXER_DEBUG;
this.types = new Set();
this.values = new Set();
this.typeHints = new Map();
this.reExportTypeHints = new Map();
this.importNodes = [];
this.exportNodes = [];
this.rawCode = rawCode;
this.source = parse(fileName, rawCode);
this.code = new MagicString(rawCode);
}
fix() {
this.analyze(this.source.statements);
if (this.typeHints.size || this.reExportTypeHints.size) {
this.service = new LanguageService(this.rawCode);
this.importNodes.forEach((node) => this.fixTypeOnlyImport(node));
}
if (this.types.size) {
this.exportNodes.forEach((node) => this.fixTypeOnlyExport(node));
}
return this.types.size
? {
code: this.code.toString(),
map: this.sourcemap ? this.code.generateMap() : null,
}
: {
code: this.rawCode,
map: null,
};
}
fixTypeOnlyImport(node) {
let hasRemoved = false;
const typeImports = [];
const valueImports = [];
const specifier = node.moduleSpecifier.getText();
const nameNode = node.importClause.name;
const namedBindings = node.importClause.namedBindings;
if (nameNode) {
const name = nameNode.text;
if (this.isTypeOnly(name)) {
if (this.isUselessImport(nameNode)) {
hasRemoved = true;
}
else {
// import A from 'a'; -> import type A from 'a';
typeImports.push(`import type ${name} from ${specifier};`);
}
}
else {
valueImports.push(`import ${name} from ${specifier};`);
}
}
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
const name = namedBindings.name.text;
if (this.isTypeOnly(name)) {
if (this.isUselessImport(namedBindings.name)) {
hasRemoved = true;
}
else {
// import * as A from 'a'; -> import type * as A from 'a';
typeImports.push(`import type * as ${name} from ${specifier};`);
}
}
else {
valueImports.push(`import * as ${name} from ${specifier};`);
}
}
if (namedBindings && ts.isNamedImports(namedBindings)) {
const typeNames = [];
const valueNames = [];
for (const element of namedBindings.elements) {
if (this.isTypeOnly(element.name.text)) {
if (this.isUselessImport(element.name)) {
hasRemoved = true;
}
else {
// import { A as B } from 'a'; -> import type { A as B } from 'a';
typeNames.push(element.getText());
}
}
else {
valueNames.push(element.getText());
}
}
if (typeNames.length) {
typeImports.push(`import type { ${typeNames.join(', ')} } from ${specifier};`);
}
if (valueNames.length) {
valueImports.push(`import { ${valueNames.join(', ')} } from ${specifier};`);
}
}
if (typeImports.length || hasRemoved) {
this.code.overwrite(node.getStart(), node.getEnd(), [...valueImports, ...typeImports].join(`\n${getNodeIndent(node)}`));
}
}
fixTypeOnlyExport(node) {
const typeExports = [];
const valueExports = [];
const specifier = node.moduleSpecifier?.getText();
if (ts.isNamespaceExport(node.exportClause)) {
const name = node.exportClause.name.text;
if (this.isReExportTypeOnly(name)) {
// export * as A from 'a'; -> export type * as A from 'a';
typeExports.push(`export type * as ${name} from ${specifier};`);
}
else {
valueExports.push(`export * as ${name} from ${specifier};`);
}
}
if (ts.isNamedExports(node.exportClause)) {
const typeNames = [];
const valueNames = [];
for (const element of node.exportClause.elements) {
const name = element.propertyName?.text || element.name.text;
const isType = node.moduleSpecifier
? this.isReExportTypeOnly(element.name.text)
: this.isTypeOnly(name);
if (isType) {
// export { A as B } from 'a'; -> export type { A as B } from 'a';
typeNames.push(element.getText());
}
else {
// export { A as B }; -> export { A as B };
valueNames.push(element.getText());
}
}
if (typeNames.length) {
typeExports.push(`export type { ${typeNames.join(', ')} }${specifier ? ` from ${specifier}` : ''};`);
}
if (valueNames.length) {
valueExports.push(`export { ${valueNames.join(', ')} }${specifier ? ` from ${specifier}` : ''};`);
}
}
if (typeExports.length) {
this.code.overwrite(node.getStart(), node.getEnd(), [...valueExports, ...typeExports].join(`\n${getNodeIndent(node)}`));
}
}
analyze(nodes) {
for (const node of nodes) {
this.DEBUG && console.log(node.getText(), node.kind);
if (ts.isImportDeclaration(node) && node.importClause) {
this.importNodes.push(node);
continue;
}
if (ts.isExportDeclaration(node) && node.exportClause) {
this.exportNodes.push(node);
continue;
}
if (ts.isInterfaceDeclaration(node)) {
this.DEBUG && console.log(`${node.name.getFullText()} is a type`);
this.types.add(node.name.text);
continue;
}
if (ts.isTypeAliasDeclaration(node)) {
const alias = node.name.text;
this.DEBUG && console.log(`${node.name.getFullText()} is a type`);
this.types.add(alias);
/**
* TODO: type-only import/export fixer.
* Temporarily disable the type-only import/export transformation,
* because the current implementation is unsafe.
*
* Issue: https://github.com/Swatinem/rollup-plugin-dts/issues/340
*/
// if (ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName)) {
// const reference = node.type.typeName.text;
// const aliasHint = parseTypeOnlyName(alias);
// if(aliasHint.isTypeOnly) {
// this.DEBUG && console.log(`${reference} is a type (from type-only hint)`);
// this.types.add(reference);
// this.typeHints.set(reference, (this.typeHints.get(reference) || 0) + 1);
// if(aliasHint.isReExport) {
// const reExportName = alias.split(TYPE_ONLY_RE_EXPORT)[0]!
// this.DEBUG && console.log(`${reExportName} is a type (from type-only re-export hint)`);
// this.reExportTypeHints.set(reExportName, (this.reExportTypeHints.get(reExportName) || 0) + 1);
// }
// this.code.remove(node.getStart(), node.getEnd());
// }
// }
continue;
}
if (ts.isEnumDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isVariableStatement(node)) {
if (ts.isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) {
this.DEBUG && console.log(`${declaration.name.getFullText()} is a value (from var statement)`);
this.values.add(declaration.name.text);
}
}
}
else {
if (node.name) {
this.DEBUG && console.log(`${node.name.getFullText()} is a value (from declaration)`);
this.values.add(node.name.text);
}
}
continue;
}
if (ts.isModuleBlock(node)) {
this.analyze(node.statements);
continue;
}
if (ts.isModuleDeclaration(node)) {
if (node.name && ts.isIdentifier(node.name)) {
this.DEBUG && console.log(`${node.name.getFullText()} is a value (from module declaration)`);
this.values.add(node.name.text);
}
this.analyze(node.getChildren());
continue;
}
this.DEBUG && console.log("unhandled statement", node.getFullText(), node.kind);
}
}
// The type-hint statements may lead to redundant import statements.
// After type-hint statements been removed,
// it is better to also remove these redundant import statements as well.
// Of course, this is not necessary since it won't cause issues,
// but it can make the output bundles cleaner :)
isUselessImport(node) {
// `referenceCount` contains it self.
const referenceCount = this.service.findReferenceCount(node);
const typeHintCount = this.typeHints.get(node.text);
return (typeHintCount && typeHintCount + 1 >= referenceCount);
}
isTypeOnly(name) {
return this.typeHints.has(name)
|| (this.types.has(name) && !this.values.has(name));
}
isReExportTypeOnly(name) {
return this.reExportTypeHints.has(name);
}
}
function getNodeIndent(node) {
const match = node.getFullText().match(/^(?:\n*)([ ]*)/);
return ' '.repeat(match?.[1]?.length || 0);
}
/**
* The pre-process step has the following goals:
* - [x] Fixes the "modifiers", removing any `export` modifier and adding any
* missing `declare` modifier.
* - [x] Splitting compound `VariableStatement` into its parts.
* - [x] Moving declarations for the same "name" to be next to each other.
* - [x] Removing any triple-slash directives and recording them.
* - [x] Create a synthetic name for any nameless "export default".
* - [x] Resolve inline `import()` statements and generate top-level imports for
* them.
* - [x] Generate a separate `export {}` statement for any item which had its
* modifiers rewritten.
* - [ ] Duplicate the identifiers of a namespace `export`, so that renaming does
* not break it
*/
function preProcess({ sourceFile, isEntry, isJSON }) {
const code = new MagicString(sourceFile.getFullText());
// Only treat as global module if it's not an entry point,
// otherwise the final output will be mismatched with the entry.
const treatAsGlobalModule = !isEntry && isGlobalModule(sourceFile);
/** All the names that are declared in the `SourceFile`. */
const declaredNames = new Set();
/** All the names that are exported. */
const exportedNames = new Set();
/** The name of the default export. */
let defaultExport = "";
/** Inlined exports from `fileId` -> <synthetic name>. */
const inlineImports = new Map();
/** The ranges that each name covers, for re-ordering. */
const nameRanges = new Map();
/**
* Pass 1:
*
* - Remove statements that we can’t handle.
* - Collect a `Set` of all the declared names.
* - Collect a `Set` of all the exported names.
* - Maybe collect the name of the default export if present.
* - Fix the modifiers of all the items.
* - Collect the ranges of each named statement.
* - Duplicate the identifiers of a namespace `export`, so that renaming does
* not break it
*/
for (const node of sourceFile.statements) {
if (ts.isEmptyStatement(node)) {
code.remove(node.getStart(), node.getEnd());
continue;
}
if (ts.isImportDeclaration(node)) {
if (!node.importClause) {
continue;
}
if (node.importClause.name) {
declaredNames.add(node.importClause.name.text);
}
if (node.importClause.namedBindings) {
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
declaredNames.add(node.importClause.namedBindings.name.text);
}
else {
node.importClause.namedBindings.elements
.forEach((element) => declaredNames.add(element.name.text));
}
}
}
else if (ts.isEnumDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isModuleDeclaration(node)) {
// collect the declared name
if (node.name) {
const name = node.name.getText();
declaredNames.add(name);
// collect the exported name, maybe as `default`.
if (matchesModifier(node, ts.ModifierFlags.ExportDefault)) {
defaultExport = name;
}
else if ((treatAsGlobalModule && ts.isIdentifier(node.name))
|| matchesModifier(node, ts.ModifierFlags.Export)) {
exportedNames.add(name);
}
if (!(node.flags & ts.NodeFlags.GlobalAugmentation)) {
pushNamedNode(name, [getStart(node), getEnd(node)]);
}
}
// duplicate exports of namespaces
if (ts.isModuleDeclaration(node)) {
duplicateExports(code, node);
}
fixModifiers(code, node);
}
else if (ts.isVariableStatement(node)) {
const { declarations } = node.declarationList;
// collect all the names, also check if they are exported
const isExport = matchesModifier(node, ts.ModifierFlags.Export);
for (const decl of node.declarationList.declarations) {
if (ts.isIdentifier(decl.name)) {
const name = decl.name.getText();
declaredNames.add(name);
if (treatAsGlobalModule || isExport) {
exportedNames.add(name);
}
}
}
fixModifiers(code, node);
// collect the ranges for re-ordering
if (declarations.length === 1) {
const decl = declarations[0];
if (ts.isIdentifier(decl.name)) {
pushNamedNode(decl.name.getText(), [getStart(node), getEnd(node)]);
}
}
else {
// we do reordering after splitting
const decls = declarations.slice();
const first = decls.shift();
pushNamedNode(first.name.getText(), [getStart(node), first.getEnd()]);
for (const decl of decls) {
if (ts.isIdentifier(decl.name)) {
pushNamedNode(decl.name.getText(), [decl.getFullStart(), decl.getEnd()]);
}
}
}
// split the variable declaration into different statements
const { flags } = node.declarationList;
const type = flags & ts.NodeFlags.Let ? "let" : flags & ts.NodeFlags.Const ? "const" : "var";
const prefix = `declare ${type} `;
const list = node.declarationList
.getChildren()
.find((c) => c.kind === ts.SyntaxKind.SyntaxList)
.getChildren();
let commaPos = 0;
for (const node of list) {
if (node.kind === ts.SyntaxKind.CommaToken) {
commaPos = node.getStart();
code.remove(commaPos, node.getEnd());
}
else if (commaPos) {
code.appendLeft(commaPos, ";\n");
const start = node.getFullStart();
const slice = code.slice(start, node.getStart());
const whitespace = slice.length - slice.trimStart().length;
if (whitespace) {
code.overwrite(start, start + whitespace, prefix);
}
else {
code.appendLeft(start, prefix);
}
}
}
}
}
/**
* Pass 2:
*
* Now that we have a Set of all the declared names, we can use that to
* generate and de-conflict names for the following steps:
*
* - Resolve all the inline imports.
* - Give any name-less `default export` a name.
*/
for (const node of sourceFile.statements) {
// recursively check inline imports
checkInlineImport(node);
/**
* TODO: type-only import/export fixer.
* Temporarily disable the type-only import/export transformation,
* because the current implementation is unsafe.
*
* Issue: https://github.com/Swatinem/rollup-plugin-dts/issues/340
*/
// transformTypeOnlyImport(node);
// transformTypeOnlyExport(node);
if (!matchesModifier(node, ts.ModifierFlags.ExportDefault)) {
continue;
}
// only function and class can be default exported, and be missing a name
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
if (node.name) {
continue;
}
if (!defaultExport) {
defaultExport = uniqName("export_default");
}
const children = node.getChildren();
const idx = children.findIndex((node) => node.kind === ts.SyntaxKind.ClassKeyword || node.kind === ts.SyntaxKind.FunctionKeyword);
const token = children[idx];
const nextToken = children[idx + 1];
const isPunctuation = nextToken.kind >= ts.SyntaxKind.FirstPunctuation && nextToken.kind <= ts.SyntaxKind.LastPunctuation;
if (isPunctuation) {
const addSpace = code.slice(token.getEnd(), nextToken.getStart()) != " ";
code.appendLeft(nextToken.getStart(), `${addSpace ? " " : ""}${defaultExport}`);
}
else {
code.appendRight(token.getEnd(), ` ${defaultExport}`);
}
}
}
// and re-order all the name ranges to be contiguous
for (const ranges of nameRanges.values()) {
// we have to move all the nodes in front of the *last* one, which is a bit
// unintuitive but is a workaround for:
// https://github.com/Rich-Harris/magic-string/issues/180
const last = ranges.pop();
const start = last[0];
for (const node of ranges) {
code.move(node[0], node[1], start);
}
}
// render all the inline imports, and all the exports
if (defaultExport) {
code.append(`\nexport default ${defaultExport};\n`);
}
if (exportedNames.size) {
code.append(`\nexport { ${[...exportedNames].join(", ")} };\n`);
}
if (isJSON && exportedNames.size) {
/**
* Add default export for JSON modules.
*
* The typescript compiler only generate named exports for each top-level key,
* but we also need a default export for JSON modules in most cases.
* This also aligns with the behavior of `@rollup/plugin-json`.
*/
defaultExport = uniqName("export_default");
code.append([
`\ndeclare const ${defaultExport}: {`,
[...exportedNames].map(name => ` ${name}: typeof ${name};`).join("\n"),
`};`,
`export default ${defaultExport};\n`
].join('\n'));
}
for (const [fileId, importName] of inlineImports.entries()) {
code.prepend(`import * as ${importName} from "${fileId}";\n`);
}
const lineStarts = sourceFile.getLineStarts();
// and collect/remove all the typeReferenceDirectives
const typeReferences = new Set();
for (const ref of sourceFile.typeReferenceDirectives) {
typeReferences.add(ref.fileName);
const { line } = sourceFile.getLineAndCharacterOfPosition(ref.pos);
const start = lineStarts[line];
let end = sourceFile.getLineEndOfPosition(ref.pos);
if (code.slice(end, end + 1) === "\n") {
end += 1;
}
code.remove(start, end);
}
// and collect/remove all the fileReferenceDirectives
const fileReferences = new Set();
for (const ref of sourceFile.referencedFiles) {
fileReferences.add(ref.fileName);
const { line } = sourceFile.getLineAndCharacterOfPosition(ref.pos);
const start = lineStarts[line];
let end = sourceFile.getLineEndOfPosition(ref.pos);
if (code.slice(end, end + 1) === "\n") {
end += 1;
}
code.remove(start, end);
}
return {
code,
typeReferences,
fileReferences,
};
function checkInlineImport(node) {
ts.forEachChild(node, checkInlineImport);
if (ts.isImportTypeNode(node)) {
if (!ts.isLiteralTypeNode(node.argument) || !ts.isStringLiteral(node.argument.literal)) {
throw new UnsupportedSyntaxError(node, "inline imports should have a literal argument");
}
const fileId = node.argument.literal.text;
const children = node.getChildren();
const start = children.find((t) => t.kind === ts.SyntaxKind.ImportKeyword).getStart();
let end = node.getEnd();
const token = children.find((t) => t.kind === ts.SyntaxKind.DotToken || t.kind === ts.SyntaxKind.LessThanToken);
if (token) {
end = token.getStart();
}
const importName = createNamespaceImport(fileId);
code.overwrite(start, end, importName);
}
}
function createNamespaceImport(fileId) {
let importName = inlineImports.get(fileId);
if (!importName) {
importName = uniqName(getSafeName(fileId));
inlineImports.set(fileId, importName);
}
return importName;
}
function uniqName(hint) {
let name = hint;
while (declaredNames.has(name)) {
name = `_${name}`;
}
declaredNames.add(name);
return name;
}
function pushNamedNode(name, range) {
let nodes = nameRanges.get(name);
if (!nodes) {
nodes = [range];
nameRanges.set(name, nodes);
}
else {
const last = nodes[nodes.length - 1];
if (last[1] === range[0]) {
last[1] = range[1];
}
else {
nodes.push(range);
}
}
}
}
/**
* If the `SourceFile` is a "global module":
*
* 1. Doesn't have any top-level `export {}` or `export default` statements,
* otherwise it's a "scoped module".
*
* 2. Should have at least one top-level `import` or `export` statement,
* otherwise it's not a module.
*
* Issue: https://github.com/Swatinem/rollup-plugin-dts/issues/334
*/
function isGlobalModule(sourceFile) {
let isModule = false;
for (const node of sourceFile.statements) {
if (ts.isExportDeclaration(node) || ts.isExportAssignment(node)) {
return false;
}
if (isModule || ts.isImportDeclaration(node) || matchesModifier(node, ts.ModifierFlags.Export)) {
isModule = true;
}
}
return isModule;
}
function fixModifiers(code, node) {
// remove the `export` and `default` modifier, add a `declare` if its missing.
if (!ts.canHaveModifiers(node)) {
return;
}
let hasDeclare = false;
const needsDeclare = ts.isEnumDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
ts.isModuleDeclaration(node) ||
ts.isVariableStatement(node);
for (const mod of node.modifiers ?? []) {
switch (mod.kind) {
case ts.SyntaxKind.ExportKeyword: // fall through
case ts.SyntaxKind.DefaultKeyword:
// TODO: be careful about that `+ 1`
code.remove(mod.getStart(), mod.getEnd() + 1);
break;
case ts.SyntaxKind.DeclareKeyword:
hasDeclare = true;
}
}
if (needsDeclare && !hasDeclare) {
code.appendRight(node.getStart(), "declare ");
}
}
function duplicateExports(code, module) {
if (!module.body || !ts.isModuleBlock(module.body)) {
return;
}
for (const node of module.body.statements) {
if (ts.isExportDeclaration(node) && node.exportClause) {
if (ts.isNamespaceExport(node.exportClause)) {
continue;
}
for (const decl of node.exportClause.elements) {
if (!decl.propertyName) {
code.appendLeft(decl.name.getEnd(), ` as ${decl.name.getText()}`);
}
}
}
}
}
function getSafeName(fileId) {
return fileId.replace(/[^a-zA-Z0-9_$]/g, () => "_");
}
function getStart(node) {
const start = node.getFullStart();
return start + (newlineAt(node, start) ? 1 : 0);
}
function getEnd(node) {
const end = node.getEnd();
return end + (newlineAt(node, end) ? 1 : 0);
}
function newlineAt(node, idx) {
return node.getSourceFile().getFullText()[idx] === "\n";
}
const IGNORE_TYPENODES = new Set([
ts.SyntaxKind.LiteralType,
ts.SyntaxKind.VoidKeyword,
ts.SyntaxKind.UnknownKeyword,
ts.SyntaxKind.AnyKeyword,
ts.SyntaxKind.BooleanKeyword,
ts.SyntaxKind.NumberKeyword,
ts.SyntaxKind.StringKeyword,
ts.SyntaxKind.ObjectKeyword,
ts.SyntaxKind.NullKeyword,
ts.SyntaxKind.UndefinedKeyword,
ts.SyntaxKind.SymbolKeyword,
ts.SyntaxKind.NeverKeyword,
ts.SyntaxKind.ThisKeyword,
ts.SyntaxKind.ThisType,
ts.SyntaxKind.BigIntKeyword,
]);
class DeclarationScope {
constructor({ id, range }) {
/**
* As we walk the AST, we need to keep track of type variable bindings that
* shadow the outer identifiers. To achieve this, we keep a stack of scopes,
* represented as Sets of variable names.
*/
this.scopes = [];
if (id) {
this.declaration = createDeclaration(id, range);
}
else {
const { iife, fn } = createIIFE(range);
this.iife = iife;
this.declaration = fn;
}
const ret = createReturn();
this.declaration.body.body.push(ret.stmt)