🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2363 lines
94 KiB
JavaScript
Executable File
2363 lines
94 KiB
JavaScript
Executable File
import * as path from 'node:path';
|
||
import ts from 'typescript';
|
||
import { createRequire } from 'node:module';
|
||
import MagicString from 'magic-string';
|
||
|
||
function resolveDefaultOptions(options) {
|
||
return {
|
||
...options,
|
||
compilerOptions: options.compilerOptions ?? {},
|
||
respectExternal: options.respectExternal ?? false,
|
||
includeExternal: options.includeExternal ?? [],
|
||
};
|
||
}
|
||
|
||
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.dirname(fromPath)) {
|
||
fromPath = path.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.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.resolve(process.cwd(), overrideConfigPath)
|
||
: ts.findConfigFile(dirName, ts.sys.fileExists);
|
||
if (!configPath) {
|
||
return { dtsFiles, dirName, compilerOptions };
|
||
}
|
||
const inputDirName = dirName;
|
||
dirName = path.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.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 = createRequire(import.meta.url);
|
||
({ 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$1 = [];
|
||
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$1.push({
|
||
exportedName: prop.name.text,
|
||
localName: prop.initializer.getText(),
|
||
});
|
||
}
|
||
// sort in reverse order, since we will do string manipulation
|
||
namespaces.unshift({
|
||
name,
|
||
exports: exports$1,
|
||
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.isObjectLiteralExpression(node)) {
|
||
return withStartEnd({
|
||
type: "ObjectExpression",
|
||
properties: node.properties.map((prop) => {
|
||
if (ts.isPropertyAssignment(prop)) {
|
||
return withStartEnd({
|
||
type: "Property",
|
||
key: ts.isIdentifier(prop.name) ? createIdentifier(prop.name) : convertExpression(prop.name),
|
||
value: convertExpression(prop.initializer),
|
||
kind: "init",
|
||
method: false,
|
||
shorthand: false,
|
||
computed: ts.isComputedPropertyName(prop.name),
|
||
}, prop);
|
||
}
|
||
else if (ts.isShorthandPropertyAssignment(prop)) {
|
||
return withStartEnd({
|
||
type: "Property",
|
||
key: createIdentifier(prop.name),
|
||
value: createIdentifier(prop.name),
|
||
kind: "init",
|
||
method: false,
|
||
shorthand: true,
|
||
computed: false,
|
||
}, prop);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(prop, "Unsupported property type in object literal");
|
||
}
|
||
}),
|
||
}, node);
|
||
}
|
||
if (ts.isArrayLiteralExpression(node)) {
|
||
return withStartEnd({
|
||
type: "ArrayExpression",
|
||
elements: node.elements.map((elem) => {
|
||
if (ts.isExpression(elem)) {
|
||
return convertExpression(elem);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(elem, "Unsupported element type in array literal");
|
||
}
|
||
}),
|
||
}, node);
|
||
}
|
||
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) {
|
||
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
|
||
? {
|
||
magicCode: this.code,
|
||
}
|
||
: {
|
||
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);
|
||
}
|
||
|
||
function preProcessNamespaceBody(body, code, sourceFile) {
|
||
for (const stmt of body.statements) {
|
||
// Safely call the new context-aware function on all children
|
||
fixModifiers(code, stmt);
|
||
// Recurse for nested namespaces
|
||
if (ts.isModuleDeclaration(stmt) && stmt.body && ts.isModuleBlock(stmt.body)) {
|
||
preProcessNamespaceBody(stmt.body, code);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 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)) {
|
||
if (node.body && ts.isModuleBlock(node.body)) {
|
||
preProcessNamespaceBody(node.body, code);
|
||
}
|
||
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);
|
||
// Handle export default with object/array literals
|
||
// These need to be converted to named declarations so Rollup can track references within them
|
||
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
||
if (ts.isObjectLiteralExpression(node.expression) || ts.isArrayLiteralExpression(node.expression)) {
|
||
if (!defaultExport) {
|
||
defaultExport = uniqName("export_default");
|
||
}
|
||
// Replace "export default" with "declare var export_default ="
|
||
code.overwrite(node.getStart(), node.expression.getStart(), `declare var ${defaultExport} = `);
|
||
continue;
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
const isTopLevel = node.parent.kind === ts.SyntaxKind.SourceFile;
|
||
if (isTopLevel) {
|
||
// For top-level statements, remove `export`/`default` and ensure `declare` exists
|
||
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 ");
|
||
}
|
||
}
|
||
// For statements inside namespaces, preserve all modifiers (including export)
|
||
}
|
||
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);
|
||
this.returnExpr = ret.expr;
|
||
}
|
||
pushScope() {
|
||
this.scopes.push(new Set());
|
||
}
|
||
popScope(n = 1) {
|
||
for (let i = 0; i < n; i++) {
|
||
this.scopes.pop();
|
||
}
|
||
}
|
||
pushTypeVariable(id) {
|
||
const name = id.getText();
|
||
this.scopes[this.scopes.length - 1]?.add(name);
|
||
}
|
||
pushReference(id) {
|
||
let name;
|
||
// We convert references from TS AST to ESTree
|
||
// to hand them off to rollup.
|
||
// This means we have to check the left-most identifier inside our scope
|
||
// tree and avoid to create the reference in that case
|
||
if (id.type === "Identifier") {
|
||
name = id.name;
|
||
}
|
||
else if (id.type === "MemberExpression") {
|
||
if (id.object.type === "Identifier") {
|
||
name = id.object.name;
|
||
}
|
||
}
|
||
if (name) {
|
||
for (const scope of this.scopes) {
|
||
if (scope.has(name)) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
// `this` is a reserved keyword that retrains meaning in certain Type-only contexts, including classes
|
||
if (name === "this")
|
||
return;
|
||
const { ident, expr } = createReference(id);
|
||
this.declaration.params.push(expr);
|
||
this.returnExpr.elements.push(ident);
|
||
}
|
||
pushIdentifierReference(id) {
|
||
this.pushReference(createIdentifier(id));
|
||
}
|
||
convertEntityName(node) {
|
||
if (ts.isIdentifier(node)) {
|
||
return createIdentifier(node);
|
||
}
|
||
return withStartEnd({
|
||
type: "MemberExpression",
|
||
computed: false,
|
||
optional: false,
|
||
object: this.convertEntityName(node.left),
|
||
property: createIdentifier(node.right),
|
||
}, node);
|
||
}
|
||
convertPropertyAccess(node) {
|
||
// hm, we only care about property access expressions here…
|
||
if (!ts.isIdentifier(node.expression) && !ts.isPropertyAccessExpression(node.expression)) {
|
||
throw new UnsupportedSyntaxError(node.expression);
|
||
}
|
||
if (ts.isPrivateIdentifier(node.name)) {
|
||
throw new UnsupportedSyntaxError(node.name);
|
||
}
|
||
const object = ts.isIdentifier(node.expression)
|
||
? createIdentifier(node.expression)
|
||
: this.convertPropertyAccess(node.expression);
|
||
return withStartEnd({
|
||
type: "MemberExpression",
|
||
computed: false,
|
||
optional: false,
|
||
object,
|
||
property: createIdentifier(node.name),
|
||
}, node);
|
||
}
|
||
convertComputedPropertyName(node) {
|
||
if (!node.name || !ts.isComputedPropertyName(node.name)) {
|
||
return;
|
||
}
|
||
const { expression } = node.name;
|
||
if (ts.isLiteralExpression(expression) || ts.isPrefixUnaryExpression(expression)) {
|
||
return;
|
||
}
|
||
if (ts.isIdentifier(expression)) {
|
||
return this.pushReference(createIdentifier(expression));
|
||
}
|
||
if (ts.isPropertyAccessExpression(expression)) {
|
||
return this.pushReference(this.convertPropertyAccess(expression));
|
||
}
|
||
throw new UnsupportedSyntaxError(expression);
|
||
}
|
||
convertParametersAndType(node) {
|
||
this.convertComputedPropertyName(node);
|
||
const typeVariables = this.convertTypeParameters(node.typeParameters);
|
||
for (const param of node.parameters) {
|
||
this.convertTypeNode(param.type);
|
||
}
|
||
this.convertTypeNode(node.type);
|
||
this.popScope(typeVariables);
|
||
}
|
||
convertHeritageClauses(node) {
|
||
for (const heritage of node.heritageClauses || []) {
|
||
for (const type of heritage.types) {
|
||
this.pushReference(convertExpression(type.expression));
|
||
this.convertTypeArguments(type);
|
||
}
|
||
}
|
||
}
|
||
convertTypeArguments(node) {
|
||
if (!node.typeArguments) {
|
||
return;
|
||
}
|
||
for (const arg of node.typeArguments) {
|
||
this.convertTypeNode(arg);
|
||
}
|
||
}
|
||
convertMembers(members) {
|
||
for (const node of members) {
|
||
if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isIndexSignatureDeclaration(node)) {
|
||
if (ts.isPropertyDeclaration(node) && node.initializer && ts.isPropertyAccessExpression(node.initializer)) {
|
||
this.pushReference(this.convertPropertyAccess(node.initializer));
|
||
}
|
||
this.convertComputedPropertyName(node);
|
||
this.convertTypeNode(node.type);
|
||
continue;
|
||
}
|
||
if (ts.isMethodDeclaration(node) ||
|
||
ts.isMethodSignature(node) ||
|
||
ts.isConstructorDeclaration(node) ||
|
||
ts.isConstructSignatureDeclaration(node) ||
|
||
ts.isCallSignatureDeclaration(node) ||
|
||
ts.isGetAccessorDeclaration(node) ||
|
||
ts.isSetAccessorDeclaration(node)) {
|
||
this.convertParametersAndType(node);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(node);
|
||
}
|
||
}
|
||
}
|
||
convertTypeParameters(params) {
|
||
if (!params) {
|
||
return 0;
|
||
}
|
||
for (const node of params) {
|
||
this.convertTypeNode(node.constraint);
|
||
this.convertTypeNode(node.default);
|
||
this.pushScope();
|
||
this.pushTypeVariable(node.name);
|
||
}
|
||
return params.length;
|
||
}
|
||
convertTypeNode(node) {
|
||
if (!node) {
|
||
return;
|
||
}
|
||
if (IGNORE_TYPENODES.has(node.kind)) {
|
||
return;
|
||
}
|
||
if (ts.isTypeReferenceNode(node)) {
|
||
this.pushReference(this.convertEntityName(node.typeName));
|
||
this.convertTypeArguments(node);
|
||
return;
|
||
}
|
||
if (ts.isTypeLiteralNode(node)) {
|
||
this.convertMembers(node.members);
|
||
return;
|
||
}
|
||
if (ts.isArrayTypeNode(node)) {
|
||
this.convertTypeNode(node.elementType);
|
||
return;
|
||
}
|
||
if (ts.isTupleTypeNode(node)) {
|
||
for (const type of node.elements) {
|
||
this.convertTypeNode(type);
|
||
}
|
||
return;
|
||
}
|
||
if (ts.isNamedTupleMember(node) ||
|
||
ts.isParenthesizedTypeNode(node) ||
|
||
ts.isTypeOperatorNode(node) ||
|
||
ts.isTypePredicateNode(node)) {
|
||
this.convertTypeNode(node.type);
|
||
return;
|
||
}
|
||
if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
|
||
for (const type of node.types) {
|
||
this.convertTypeNode(type);
|
||
}
|
||
return;
|
||
}
|
||
if (ts.isMappedTypeNode(node)) {
|
||
const { typeParameter, type, nameType } = node;
|
||
this.convertTypeNode(typeParameter.constraint);
|
||
this.pushScope();
|
||
this.pushTypeVariable(typeParameter.name);
|
||
this.convertTypeNode(type);
|
||
if (nameType) {
|
||
this.convertTypeNode(nameType);
|
||
}
|
||
this.popScope();
|
||
return;
|
||
}
|
||
if (ts.isConditionalTypeNode(node)) {
|
||
this.convertTypeNode(node.checkType);
|
||
this.pushScope();
|
||
this.convertTypeNode(node.extendsType);
|
||
this.convertTypeNode(node.trueType);
|
||
this.convertTypeNode(node.falseType);
|
||
this.popScope();
|
||
return;
|
||
}
|
||
if (ts.isIndexedAccessTypeNode(node)) {
|
||
this.convertTypeNode(node.objectType);
|
||
this.convertTypeNode(node.indexType);
|
||
return;
|
||
}
|
||
if (ts.isFunctionOrConstructorTypeNode(node)) {
|
||
this.convertParametersAndType(node);
|
||
return;
|
||
}
|
||
if (ts.isTypeQueryNode(node)) {
|
||
const reference = this.convertEntityName(node.exprName);
|
||
this.pushReference(reference);
|
||
this.convertTypeArguments(node);
|
||
return;
|
||
}
|
||
if (ts.isRestTypeNode(node)) {
|
||
this.convertTypeNode(node.type);
|
||
return;
|
||
}
|
||
if (ts.isOptionalTypeNode(node)) {
|
||
this.convertTypeNode(node.type);
|
||
return;
|
||
}
|
||
if (ts.isTemplateLiteralTypeNode(node)) {
|
||
for (const span of node.templateSpans) {
|
||
this.convertTypeNode(span.type);
|
||
}
|
||
return;
|
||
}
|
||
if (ts.isInferTypeNode(node)) {
|
||
const { typeParameter } = node;
|
||
this.convertTypeNode(typeParameter.constraint);
|
||
this.pushTypeVariable(typeParameter.name);
|
||
return;
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(node);
|
||
}
|
||
}
|
||
convertNamespace(node, relaxedModuleBlock = false) {
|
||
this.pushScope();
|
||
if (relaxedModuleBlock && node.body && ts.isModuleDeclaration(node.body)) {
|
||
this.convertNamespace(node.body, true);
|
||
return;
|
||
}
|
||
if (!node.body || !ts.isModuleBlock(node.body)) {
|
||
throw new UnsupportedSyntaxError(node, `namespace must have a "ModuleBlock" body.`);
|
||
}
|
||
const { statements } = node.body;
|
||
// first, hoist all the declarations for correct shadowing
|
||
for (const stmt of statements) {
|
||
if (ts.isEnumDeclaration(stmt) ||
|
||
ts.isFunctionDeclaration(stmt) ||
|
||
ts.isClassDeclaration(stmt) ||
|
||
ts.isInterfaceDeclaration(stmt) ||
|
||
ts.isTypeAliasDeclaration(stmt) ||
|
||
ts.isModuleDeclaration(stmt)) {
|
||
if (stmt.name && ts.isIdentifier(stmt.name)) {
|
||
this.pushTypeVariable(stmt.name);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(stmt, "non-Identifier name not supported");
|
||
}
|
||
continue;
|
||
}
|
||
if (ts.isVariableStatement(stmt)) {
|
||
for (const decl of stmt.declarationList.declarations) {
|
||
if (ts.isIdentifier(decl.name)) {
|
||
this.pushTypeVariable(decl.name);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(decl, "non-Identifier name not supported");
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
if (ts.isImportDeclaration(stmt)) {
|
||
if (stmt.importClause) {
|
||
if (stmt.importClause.name) {
|
||
this.pushTypeVariable(stmt.importClause.name);
|
||
}
|
||
if (stmt.importClause.namedBindings) {
|
||
if (ts.isNamespaceImport(stmt.importClause.namedBindings)) {
|
||
this.pushTypeVariable(stmt.importClause.namedBindings.name);
|
||
}
|
||
else {
|
||
for (const el of stmt.importClause.namedBindings.elements) {
|
||
this.pushTypeVariable(el.name);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
if (ts.isImportEqualsDeclaration(stmt)) {
|
||
this.pushTypeVariable(stmt.name);
|
||
continue;
|
||
}
|
||
if (ts.isExportDeclaration(stmt)) ;
|
||
else {
|
||
throw new UnsupportedSyntaxError(stmt, "namespace child (hoisting) not supported yet");
|
||
}
|
||
}
|
||
// and then walk all the children like normal…
|
||
for (const stmt of statements) {
|
||
if (ts.isVariableStatement(stmt)) {
|
||
for (const decl of stmt.declarationList.declarations) {
|
||
if (decl.type) {
|
||
this.convertTypeNode(decl.type);
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
if (ts.isFunctionDeclaration(stmt)) {
|
||
this.convertParametersAndType(stmt);
|
||
continue;
|
||
}
|
||
if (ts.isInterfaceDeclaration(stmt) || ts.isClassDeclaration(stmt)) {
|
||
const typeVariables = this.convertTypeParameters(stmt.typeParameters);
|
||
this.convertHeritageClauses(stmt);
|
||
this.convertMembers(stmt.members);
|
||
this.popScope(typeVariables);
|
||
continue;
|
||
}
|
||
if (ts.isTypeAliasDeclaration(stmt)) {
|
||
const typeVariables = this.convertTypeParameters(stmt.typeParameters);
|
||
this.convertTypeNode(stmt.type);
|
||
this.popScope(typeVariables);
|
||
continue;
|
||
}
|
||
if (ts.isModuleDeclaration(stmt)) {
|
||
this.convertNamespace(stmt, relaxedModuleBlock);
|
||
continue;
|
||
}
|
||
if (ts.isEnumDeclaration(stmt)) {
|
||
// noop
|
||
continue;
|
||
}
|
||
// handle imports in the walking pass
|
||
if (ts.isImportDeclaration(stmt)) {
|
||
// noop, already handled by hoisting
|
||
continue;
|
||
}
|
||
if (ts.isImportEqualsDeclaration(stmt)) {
|
||
if (ts.isEntityName(stmt.moduleReference)) {
|
||
this.pushReference(this.convertEntityName(stmt.moduleReference));
|
||
}
|
||
// we ignore `import foo = require(...)` as that is a module import
|
||
continue;
|
||
}
|
||
if (ts.isExportDeclaration(stmt)) {
|
||
if (stmt.exportClause) {
|
||
if (ts.isNamespaceExport(stmt.exportClause)) {
|
||
throw new UnsupportedSyntaxError(stmt.exportClause);
|
||
}
|
||
for (const decl of stmt.exportClause.elements) {
|
||
const id = decl.propertyName || decl.name;
|
||
this.pushIdentifierReference(id);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(stmt, "namespace child (walking) not supported yet");
|
||
}
|
||
}
|
||
this.popScope();
|
||
}
|
||
}
|
||
|
||
function convert({ sourceFile }) {
|
||
const transformer = new Transformer(sourceFile);
|
||
return transformer.transform();
|
||
}
|
||
class Transformer {
|
||
constructor(sourceFile) {
|
||
this.sourceFile = sourceFile;
|
||
this.declarations = new Map();
|
||
this.ast = createProgram(sourceFile);
|
||
for (const stmt of sourceFile.statements) {
|
||
this.convertStatement(stmt);
|
||
}
|
||
}
|
||
transform() {
|
||
return {
|
||
ast: this.ast,
|
||
};
|
||
}
|
||
pushStatement(node) {
|
||
this.ast.body.push(node);
|
||
}
|
||
createDeclaration(node, id) {
|
||
const range = { start: node.getFullStart(), end: node.getEnd() };
|
||
if (!id) {
|
||
const scope = new DeclarationScope({ range });
|
||
this.pushStatement(scope.iife);
|
||
return scope;
|
||
}
|
||
const name = id.getText();
|
||
// We have re-ordered and grouped declarations in `reorderStatements`,
|
||
// so we can assume same-name statements are next to each other, so we just
|
||
// bump the `end` range.
|
||
const scope = new DeclarationScope({ id, range });
|
||
const existingScope = this.declarations.get(name);
|
||
if (existingScope) {
|
||
existingScope.pushIdentifierReference(id);
|
||
existingScope.declaration.end = range.end;
|
||
// we possibly have other declarations, such as an ExportDeclaration in
|
||
// between, which should also be updated to the correct start/end.
|
||
const selfIdx = this.ast.body.findIndex((node) => node == existingScope.declaration);
|
||
for (let i = selfIdx + 1; i < this.ast.body.length; i++) {
|
||
const decl = this.ast.body[i];
|
||
decl.start = decl.end = range.end;
|
||
}
|
||
}
|
||
else {
|
||
this.pushStatement(scope.declaration);
|
||
this.declarations.set(name, scope);
|
||
}
|
||
return existingScope || scope;
|
||
}
|
||
convertStatement(node) {
|
||
if (ts.isEnumDeclaration(node)) {
|
||
return this.convertEnumDeclaration(node);
|
||
}
|
||
if (ts.isFunctionDeclaration(node)) {
|
||
return this.convertFunctionDeclaration(node);
|
||
}
|
||
if (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) {
|
||
return this.convertClassOrInterfaceDeclaration(node);
|
||
}
|
||
if (ts.isTypeAliasDeclaration(node)) {
|
||
return this.convertTypeAliasDeclaration(node);
|
||
}
|
||
if (ts.isVariableStatement(node)) {
|
||
return this.convertVariableStatement(node);
|
||
}
|
||
if (ts.isExportDeclaration(node) || ts.isExportAssignment(node)) {
|
||
return this.convertExportDeclaration(node);
|
||
}
|
||
if (ts.isModuleDeclaration(node)) {
|
||
return this.convertNamespaceDeclaration(node);
|
||
}
|
||
if (node.kind === ts.SyntaxKind.NamespaceExportDeclaration) {
|
||
// just ignore `export as namespace FOO` statements…
|
||
return this.removeStatement(node);
|
||
}
|
||
if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node)) {
|
||
return this.convertImportDeclaration(node);
|
||
}
|
||
else {
|
||
throw new UnsupportedSyntaxError(node);
|
||
}
|
||
}
|
||
removeStatement(node) {
|
||
this.pushStatement(withStartEnd({
|
||
type: "ExpressionStatement",
|
||
expression: { type: "Literal", value: "pls remove me" },
|
||
}, node));
|
||
}
|
||
convertNamespaceDeclaration(node) {
|
||
// we want to keep `declare global` augmentations, and we want to
|
||
// pull in all the things referenced inside.
|
||
// so for this case, we need to figure out some way so that rollup does
|
||
// the right thing and not rename these…
|
||
const isGlobalAugmentation = node.flags & ts.NodeFlags.GlobalAugmentation;
|
||
if (isGlobalAugmentation || !ts.isIdentifier(node.name)) {
|
||
const scope = this.createDeclaration(node);
|
||
scope.convertNamespace(node, true);
|
||
return;
|
||
}
|
||
const scope = this.createDeclaration(node, node.name);
|
||
scope.pushIdentifierReference(node.name);
|
||
scope.convertNamespace(node);
|
||
}
|
||
convertEnumDeclaration(node) {
|
||
const scope = this.createDeclaration(node, node.name);
|
||
scope.pushIdentifierReference(node.name);
|
||
}
|
||
convertFunctionDeclaration(node) {
|
||
if (!node.name) {
|
||
throw new UnsupportedSyntaxError(node, "FunctionDeclaration should have a name");
|
||
}
|
||
const scope = this.createDeclaration(node, node.name);
|
||
scope.pushIdentifierReference(node.name);
|
||
scope.convertParametersAndType(node);
|
||
}
|
||
convertClassOrInterfaceDeclaration(node) {
|
||
if (!node.name) {
|
||
throw new UnsupportedSyntaxError(node, "ClassDeclaration / InterfaceDeclaration should have a name");
|
||
}
|
||
const scope = this.createDeclaration(node, node.name);
|
||
const typeVariables = scope.convertTypeParameters(node.typeParameters);
|
||
scope.convertHeritageClauses(node);
|
||
scope.convertMembers(node.members);
|
||
scope.popScope(typeVariables);
|
||
}
|
||
convertTypeAliasDeclaration(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
|
||
*/
|
||
// if(parseTypeOnlyName(node.name.text).isTypeOnly) {
|
||
// this.pushStatement(convertTypeOnlyHintStatement(node))
|
||
// return
|
||
// }
|
||
const scope = this.createDeclaration(node, node.name);
|
||
const typeVariables = scope.convertTypeParameters(node.typeParameters);
|
||
scope.convertTypeNode(node.type);
|
||
scope.popScope(typeVariables);
|
||
}
|
||
convertVariableStatement(node) {
|
||
const { declarations } = node.declarationList;
|
||
if (declarations.length !== 1) {
|
||
throw new UnsupportedSyntaxError(node, "VariableStatement with more than one declaration not yet supported");
|
||
}
|
||
for (const decl of declarations) {
|
||
if (!ts.isIdentifier(decl.name)) {
|
||
throw new UnsupportedSyntaxError(node, "VariableDeclaration must have a name");
|
||
}
|
||
const scope = this.createDeclaration(node, decl.name);
|
||
scope.convertTypeNode(decl.type);
|
||
// Track references in the initializer (e.g., for object literals)
|
||
if (decl.initializer) {
|
||
this.trackExpressionReferences(decl.initializer, scope);
|
||
}
|
||
}
|
||
}
|
||
// Helper to track identifier references in expressions
|
||
trackExpressionReferences(expr, scope) {
|
||
if (ts.isIdentifier(expr)) {
|
||
scope.pushIdentifierReference(expr);
|
||
}
|
||
else if (ts.isObjectLiteralExpression(expr)) {
|
||
for (const prop of expr.properties) {
|
||
if (ts.isShorthandPropertyAssignment(prop)) {
|
||
scope.pushIdentifierReference(prop.name);
|
||
}
|
||
else if (ts.isPropertyAssignment(prop)) {
|
||
this.trackExpressionReferences(prop.initializer, scope);
|
||
}
|
||
}
|
||
}
|
||
else if (ts.isArrayLiteralExpression(expr)) {
|
||
for (const elem of expr.elements) {
|
||
if (ts.isExpression(elem)) {
|
||
this.trackExpressionReferences(elem, scope);
|
||
}
|
||
}
|
||
}
|
||
else if (ts.isPropertyAccessExpression(expr)) {
|
||
this.trackExpressionReferences(expr.expression, scope);
|
||
}
|
||
}
|
||
convertExportDeclaration(node) {
|
||
if (ts.isExportAssignment(node)) {
|
||
this.pushStatement(withStartEnd({
|
||
type: "ExportDefaultDeclaration",
|
||
declaration: convertExpression(node.expression),
|
||
}, node));
|
||
return;
|
||
}
|
||
const source = node.moduleSpecifier ? convertExpression(node.moduleSpecifier) : undefined;
|
||
if (!node.exportClause) {
|
||
// export * from './other'
|
||
this.pushStatement(withStartEnd({
|
||
type: "ExportAllDeclaration",
|
||
source,
|
||
exported: null,
|
||
attributes: [],
|
||
}, node));
|
||
}
|
||
else if (ts.isNamespaceExport(node.exportClause)) {
|
||
// export * as name from './other'
|
||
this.pushStatement(withStartEnd({
|
||
type: "ExportAllDeclaration",
|
||
source,
|
||
exported: createIdentifier(node.exportClause.name),
|
||
attributes: [],
|
||
}, node));
|
||
}
|
||
else {
|
||
// export { name } from './other'
|
||
const specifiers = [];
|
||
for (const elem of node.exportClause.elements) {
|
||
specifiers.push(this.convertExportSpecifier(elem));
|
||
}
|
||
this.pushStatement(withStartEnd({
|
||
type: "ExportNamedDeclaration",
|
||
declaration: null,
|
||
specifiers,
|
||
source,
|
||
attributes: [],
|
||
}, node));
|
||
}
|
||
}
|
||
convertImportDeclaration(node) {
|
||
if (ts.isImportEqualsDeclaration(node)) {
|
||
if (ts.isEntityName(node.moduleReference)) {
|
||
const scope = this.createDeclaration(node, node.name);
|
||
scope.pushReference(scope.convertEntityName(node.moduleReference));
|
||
return;
|
||
}
|
||
// assume its like `import default`
|
||
if (!ts.isExternalModuleReference(node.moduleReference)) {
|
||
throw new UnsupportedSyntaxError(node, "ImportEquals should have a literal source.");
|
||
}
|
||
this.pushStatement(withStartEnd({
|
||
type: "ImportDeclaration",
|
||
specifiers: [
|
||
{
|
||
type: "ImportDefaultSpecifier",
|
||
local: createIdentifier(node.name),
|
||
},
|
||
],
|
||
source: convertExpression(node.moduleReference.expression),
|
||
attributes: [],
|
||
}, node));
|
||
return;
|
||
}
|
||
const source = convertExpression(node.moduleSpecifier);
|
||
const specifiers = node.importClause && node.importClause.namedBindings
|
||
? this.convertNamedImportBindings(node.importClause.namedBindings)
|
||
: [];
|
||
if (node.importClause && node.importClause.name) {
|
||
specifiers.push({
|
||
type: "ImportDefaultSpecifier",
|
||
local: createIdentifier(node.importClause.name),
|
||
});
|
||
}
|
||
this.pushStatement(withStartEnd({
|
||
type: "ImportDeclaration",
|
||
specifiers,
|
||
source,
|
||
attributes: [],
|
||
}, node));
|
||
}
|
||
convertNamedImportBindings(node) {
|
||
if (ts.isNamedImports(node)) {
|
||
return node.elements.map((el) => {
|
||
const local = createIdentifier(el.name);
|
||
const imported = el.propertyName ? createIdentifier(el.propertyName) : local;
|
||
return {
|
||
type: "ImportSpecifier",
|
||
local,
|
||
imported,
|
||
};
|
||
});
|
||
}
|
||
return [
|
||
{
|
||
type: "ImportNamespaceSpecifier",
|
||
local: createIdentifier(node.name),
|
||
},
|
||
];
|
||
}
|
||
convertExportSpecifier(node) {
|
||
const exported = createIdentifier(node.name);
|
||
return {
|
||
type: "ExportSpecifier",
|
||
exported: exported,
|
||
local: node.propertyName ? createIdentifier(node.propertyName) : exported,
|
||
};
|
||
}
|
||
}
|
||
|
||
class RelativeModuleDeclarationFixer {
|
||
constructor(fileName, code, sourcemap, name) {
|
||
this.sourcemap = sourcemap;
|
||
this.DEBUG = !!process.env.DTS_EXPORTS_FIXER_DEBUG;
|
||
this.relativeModuleDeclarations = [];
|
||
this.source = parse(fileName, code.toString());
|
||
this.code = code;
|
||
this.name = name || "./index";
|
||
}
|
||
fix() {
|
||
this.analyze(this.source.statements);
|
||
for (const node of this.relativeModuleDeclarations) {
|
||
const start = node.getStart();
|
||
const end = node.getEnd();
|
||
const quote = node.name.kind === ts.SyntaxKind.StringLiteral && "singleQuote" in node.name && node.name.singleQuote
|
||
? "'"
|
||
: '"';
|
||
const code = `declare module ${quote}${this.name}${quote} ${node.body.getText()}`;
|
||
this.code.overwrite(start, end, code);
|
||
}
|
||
return {
|
||
code: this.code.toString(),
|
||
map: this.relativeModuleDeclarations.length && this.sourcemap ? this.code.generateMap() : null,
|
||
};
|
||
}
|
||
analyze(nodes) {
|
||
for (const node of nodes) {
|
||
if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body) && /^\.\.?\//.test(node.name.text)) {
|
||
if (this.DEBUG) {
|
||
console.log(`Found relative module declaration: ${node.name.text} in ${this.source.fileName}`);
|
||
}
|
||
this.relativeModuleDeclarations.push(node);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This is the *transform* part of `rollup-plugin-dts`.
|
||
*
|
||
* It sets a few input and output options, and otherwise is the core part of the
|
||
* plugin responsible for bundling `.d.ts` files.
|
||
*
|
||
* That itself is a multi-step process:
|
||
*
|
||
* 1. The plugin has a preprocessing step that moves code around and cleans it
|
||
* up a bit, so that later steps can work with it easier. See `preprocess.ts`.
|
||
* 2. It then converts the TypeScript AST into a ESTree-like AST that rollup
|
||
* understands. See `Transformer.ts`.
|
||
* 3. After rollup is finished, the plugin will postprocess the output in a
|
||
* `renderChunk` hook. As rollup usually outputs javascript, it can output
|
||
* some code that is invalid in the context of a `.d.ts` file. In particular,
|
||
* the postprocess convert any javascript code that was created for namespace
|
||
* exports into TypeScript namespaces. See `NamespaceFixer.ts`.
|
||
*/
|
||
const transform = () => {
|
||
const allTypeReferences = new Map();
|
||
const allFileReferences = new Map();
|
||
return {
|
||
name: "dts-transform",
|
||
options({ onLog, ...options }) {
|
||
return {
|
||
...options,
|
||
onLog(level, log, defaultHandler) {
|
||
if (level === "warn" && log.code === "CIRCULAR_DEPENDENCY") {
|
||
return;
|
||
}
|
||
if (onLog) {
|
||
onLog(level, log, defaultHandler);
|
||
}
|
||
else {
|
||
defaultHandler(level, log);
|
||
}
|
||
},
|
||
treeshake: {
|
||
moduleSideEffects: "no-external",
|
||
propertyReadSideEffects: true,
|
||
unknownGlobalSideEffects: false,
|
||
},
|
||
};
|
||
},
|
||
outputOptions(options) {
|
||
return {
|
||
...options,
|
||
chunkFileNames: options.chunkFileNames || "[name]-[hash].d.ts",
|
||
entryFileNames: options.entryFileNames || "[name].d.ts",
|
||
format: "es",
|
||
exports: "named",
|
||
compact: false,
|
||
freeze: true,
|
||
interop: "esModule",
|
||
generatedCode: Object.assign({ symbols: false }, options.generatedCode),
|
||
strict: false,
|
||
};
|
||
},
|
||
transform(code, fileName) {
|
||
// `fileName` may not match the name in the moduleIds,
|
||
// as we generate the `fileName` manually in the previews step,
|
||
// so we need to find the correct moduleId.
|
||
const name = trimExtension(fileName);
|
||
const moduleIds = this.getModuleIds();
|
||
const moduleId = Array.from(moduleIds).find((id) => trimExtension(id) === name);
|
||
const isEntry = Boolean(moduleId && this.getModuleInfo(moduleId)?.isEntry);
|
||
const isJSON = Boolean(moduleId && JSON_EXTENSIONS.test(moduleId));
|
||
let sourceFile = parse(fileName, code);
|
||
const preprocessed = preProcess({ sourceFile, isEntry, isJSON });
|
||
// `sourceFile.fileName` here uses forward slashes
|
||
allTypeReferences.set(sourceFile.fileName, preprocessed.typeReferences);
|
||
allFileReferences.set(sourceFile.fileName, preprocessed.fileReferences);
|
||
code = preprocessed.code.toString();
|
||
sourceFile = parse(fileName, code);
|
||
const converted = convert({ sourceFile });
|
||
if (process.env.DTS_DUMP_AST) {
|
||
console.log(fileName);
|
||
console.log(code);
|
||
console.log(JSON.stringify(converted.ast.body, undefined, 2));
|
||
}
|
||
return { code, ast: converted.ast, map: preprocessed.code.generateMap() };
|
||
},
|
||
renderChunk(inputCode, chunk, options) {
|
||
const source = parse(chunk.fileName, inputCode);
|
||
const fixer = new NamespaceFixer(source);
|
||
const typeReferences = new Set();
|
||
const fileReferences = new Set();
|
||
for (const fileName of Object.keys(chunk.modules)) {
|
||
for (const ref of allTypeReferences.get(fileName.split("\\").join("/")) || []) {
|
||
typeReferences.add(ref);
|
||
}
|
||
for (const ref of allFileReferences.get(fileName.split("\\").join("/")) || []) {
|
||
if (ref.startsWith(".")) {
|
||
// Need absolute path of the target file here
|
||
const absolutePathToOriginal = path.join(path.dirname(fileName), ref);
|
||
const chunkFolder = (options.file && path.dirname(options.file)) ||
|
||
(chunk.facadeModuleId && path.dirname(chunk.facadeModuleId)) ||
|
||
".";
|
||
let targetRelPath = path.relative(chunkFolder, absolutePathToOriginal).split("\\").join("/");
|
||
if (targetRelPath[0] !== ".") {
|
||
targetRelPath = "./" + targetRelPath;
|
||
}
|
||
fileReferences.add(targetRelPath);
|
||
}
|
||
else {
|
||
fileReferences.add(ref);
|
||
}
|
||
}
|
||
}
|
||
let code = writeBlock(Array.from(fileReferences, (ref) => `/// <reference path="${ref}" />`));
|
||
code += writeBlock(Array.from(typeReferences, (ref) => `/// <reference types="${ref}" />`));
|
||
code += fixer.fix();
|
||
if (!code) {
|
||
code += "\nexport { };";
|
||
}
|
||
const typeOnlyFixer = new TypeOnlyFixer(chunk.fileName, code);
|
||
const typesFixed = typeOnlyFixer.fix();
|
||
const relativeModuleDeclarationFixed = new RelativeModuleDeclarationFixer(chunk.fileName, "magicCode" in typesFixed && typesFixed.magicCode ? typesFixed.magicCode : new MagicString(code), !!options.sourcemap, "./" + (options.file && options.file !== "-" ? path.basename(options.file, ".d.ts") : "index"));
|
||
return relativeModuleDeclarationFixed.fix();
|
||
},
|
||
};
|
||
};
|
||
function writeBlock(codes) {
|
||
if (codes.length) {
|
||
return codes.join("\n") + "\n";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
const TS_EXTENSIONS = /\.([cm]ts|[tj]sx?)$/;
|
||
function getModule({ entries, programs, resolvedOptions }, fileName, code) {
|
||
const { compilerOptions, tsconfig } = resolvedOptions;
|
||
// Create any `ts.SourceFile` objects on-demand for ".d.ts" modules,
|
||
// but only when there are zero ".ts" entry points.
|
||
if (!programs.length && DTS_EXTENSIONS.test(fileName)) {
|
||
return { code };
|
||
}
|
||
const isEntry = entries.includes(fileName);
|
||
// Rollup doesn't tell you the entry point of each module in the bundle,
|
||
// so we need to ask every TypeScript program for the given filename.
|
||
const existingProgram = programs.find((p) => {
|
||
// Entry points may be in the other entry source files, but it can't emit from them.
|
||
// So we should find the program about the entry point which is the root files.
|
||
if (isEntry) {
|
||
return p.getRootFileNames().includes(fileName);
|
||
}
|
||
else {
|
||
const sourceFile = p.getSourceFile(fileName);
|
||
if (sourceFile && p.isSourceFileFromExternalLibrary(sourceFile)) {
|
||
return false;
|
||
}
|
||
return !!sourceFile;
|
||
}
|
||
});
|
||
if (existingProgram) {
|
||
// we know this exists b/c of the .filter above, so this non-null assertion is safe
|
||
const source = existingProgram.getSourceFile(fileName);
|
||
return {
|
||
code: source?.getFullText(),
|
||
source,
|
||
program: existingProgram,
|
||
};
|
||
}
|
||
else if (ts.sys.fileExists(fileName)) {
|
||
// For .d.ts files from external libraries (node_modules), return just the code without creating a program.
|
||
// The programs.find() above returns false for external library files (line 52-54), causing
|
||
// existingProgram to be undefined even though the file exists in a program. Without this check,
|
||
// we would create a new program for each external .d.ts file, causing memory exhaustion
|
||
// with large packages like type-fest (170+ files → 170+ programs → OOM).
|
||
if (programs.length > 0 && DTS_EXTENSIONS.test(fileName)) {
|
||
// Apply this optimization when bundling external packages via includeExternal or respectExternal
|
||
const shouldBundleExternal = resolvedOptions.includeExternal.length > 0 || resolvedOptions.respectExternal;
|
||
if (shouldBundleExternal) {
|
||
return { code };
|
||
}
|
||
}
|
||
const newProgram = createProgram$1(fileName, compilerOptions, tsconfig);
|
||
programs.push(newProgram);
|
||
// we created hte program from this fileName, so the source file must exist :P
|
||
const source = newProgram.getSourceFile(fileName);
|
||
return {
|
||
code: source?.getFullText(),
|
||
source,
|
||
program: newProgram,
|
||
};
|
||
}
|
||
else {
|
||
// the file isn't part of an existing program and doesn't exist on disk
|
||
return null;
|
||
}
|
||
}
|
||
const plugin = (options = {}) => {
|
||
const transformPlugin = transform();
|
||
const ctx = { entries: [], programs: [], resolvedOptions: resolveDefaultOptions(options) };
|
||
return {
|
||
name: "dts",
|
||
// pass outputOptions & renderChunk hooks to the inner transform plugin
|
||
outputOptions: transformPlugin.outputOptions,
|
||
renderChunk: transformPlugin.renderChunk,
|
||
options(options) {
|
||
let { input = [] } = options;
|
||
if (!Array.isArray(input)) {
|
||
input = typeof input === "string" ? [input] : Object.values(input);
|
||
}
|
||
else if (input.length > 1) {
|
||
// when dealing with multiple unnamed inputs, transform the inputs into
|
||
// an explicit object, which strips the file extension
|
||
options.input = {};
|
||
for (const filename of input) {
|
||
let name = trimExtension(filename);
|
||
if (path.isAbsolute(filename)) {
|
||
name = path.basename(name);
|
||
}
|
||
else {
|
||
name = path.normalize(name);
|
||
}
|
||
options.input[name] = filename;
|
||
}
|
||
}
|
||
ctx.programs = createPrograms(Object.values(input), ctx.resolvedOptions.compilerOptions, ctx.resolvedOptions.tsconfig);
|
||
return transformPlugin.options.call(this, options);
|
||
},
|
||
transform(code, id) {
|
||
if (!TS_EXTENSIONS.test(id) && !JSON_EXTENSIONS.test(id)) {
|
||
return null;
|
||
}
|
||
const watchFiles = (module) => {
|
||
if (module.program) {
|
||
const sourceDirectory = path.dirname(id);
|
||
const sourceFilesInProgram = module.program
|
||
.getSourceFiles()
|
||
.map((sourceFile) => sourceFile.fileName)
|
||
.filter((fileName) => fileName.startsWith(sourceDirectory));
|
||
sourceFilesInProgram.forEach(this.addWatchFile);
|
||
}
|
||
};
|
||
const handleDtsFile = () => {
|
||
const module = getModule(ctx, id, code);
|
||
if (module) {
|
||
watchFiles(module);
|
||
return transformPlugin.transform.call(this, module.code, id);
|
||
}
|
||
return null;
|
||
};
|
||
const treatTsAsDts = () => {
|
||
const declarationId = getDeclarationId(id);
|
||
const module = getModule(ctx, declarationId, code);
|
||
if (module) {
|
||
watchFiles(module);
|
||
return transformPlugin.transform.call(this, module.code, declarationId);
|
||
}
|
||
return null;
|
||
};
|
||
const generateDts = () => {
|
||
const module = getModule(ctx, id, code);
|
||
if (!module || !module.source || !module.program)
|
||
return null;
|
||
watchFiles(module);
|
||
const declarationId = getDeclarationId(id);
|
||
let generated;
|
||
const { emitSkipped, diagnostics } = module.program.emit(module.source, (_, declarationText) => {
|
||
generated = transformPlugin.transform.call(this, declarationText, declarationId);
|
||
}, undefined, // cancellationToken
|
||
true, // emitOnlyDtsFiles
|
||
undefined, // customTransformers
|
||
// @ts-ignore This is a private API for workers, should be safe to use as TypeScript Playground has used it for a long time.
|
||
true);
|
||
if (emitSkipped) {
|
||
const errors = diagnostics.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
|
||
if (errors.length) {
|
||
console.error(ts.formatDiagnostics(errors, formatHost));
|
||
this.error("Failed to compile. Check the logs above.");
|
||
}
|
||
}
|
||
return generated;
|
||
};
|
||
// if it's a .d.ts file, handle it as-is
|
||
if (DTS_EXTENSIONS.test(id))
|
||
return handleDtsFile();
|
||
// if it's a json file, use the typescript compiler to generate the declarations,
|
||
// requires `compilerOptions.resolveJsonModule: true`.
|
||
// This is also commonly used with `@rollup/plugin-json` to import JSON files.
|
||
if (JSON_EXTENSIONS.test(id))
|
||
return generateDts();
|
||
// first attempt to treat .ts files as .d.ts files, and otherwise use the typescript compiler to generate the declarations
|
||
return treatTsAsDts() ?? generateDts();
|
||
},
|
||
resolveId(source, importer) {
|
||
if (!importer) {
|
||
// store the entry point, because we need to know which program to add the file
|
||
ctx.entries.push(path.resolve(source));
|
||
return;
|
||
}
|
||
// normalize directory separators to forward slashes, as apparently typescript expects that?
|
||
importer = importer.split("\\").join("/");
|
||
let resolvedCompilerOptions = ctx.resolvedOptions.compilerOptions;
|
||
if (ctx.resolvedOptions.tsconfig) {
|
||
// Here we have a chicken and egg problem.
|
||
// `source` would be resolved by `ts.nodeModuleNameResolver` a few lines below, but
|
||
// `ts.nodeModuleNameResolver` requires `compilerOptions` which we have to resolve here,
|
||
// since we have a custom `tsconfig.json`.
|
||
// So, we use Node's resolver algorithm so we can see where the request is coming from so we
|
||
// can load the custom `tsconfig.json` from the correct path.
|
||
const resolvedSource = source.startsWith(".") ? path.resolve(path.dirname(importer), source) : source;
|
||
resolvedCompilerOptions = getCompilerOptions(resolvedSource, ctx.resolvedOptions.compilerOptions, ctx.resolvedOptions.tsconfig).compilerOptions;
|
||
}
|
||
// resolve this via typescript
|
||
const { resolvedModule } = ts.resolveModuleName(source, importer, resolvedCompilerOptions, ts.sys);
|
||
if (!resolvedModule) {
|
||
return;
|
||
}
|
||
if (resolvedModule.isExternalLibraryImport && resolvedModule.packageId && ctx.resolvedOptions.includeExternal.includes(resolvedModule.packageId.name)) {
|
||
// include types from specified external modules
|
||
return { id: path.resolve(resolvedModule.resolvedFileName) };
|
||
}
|
||
else if (!ctx.resolvedOptions.respectExternal && resolvedModule.isExternalLibraryImport) {
|
||
// here, we define everything else that comes from `node_modules` as `external`.
|
||
return { id: source, external: true };
|
||
}
|
||
else {
|
||
// using `path.resolve` here converts paths back to the system specific separators
|
||
return { id: path.resolve(resolvedModule.resolvedFileName) };
|
||
}
|
||
},
|
||
};
|
||
};
|
||
|
||
export { plugin as default, plugin as dts };
|