UNPKG

rollup-plugin-dts

Version:

A rollup plugin that will bundle up your .d.ts definition files.

1,278 lines (1,268 loc) 86.7 kB
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, }; } 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 = []; 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); 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; } } } // `thi