Skip to content

Instantly share code, notes, and snippets.

@coreyward
Created January 20, 2023 21:38
Show Gist options
  • Save coreyward/e70642a41e2181641a817a632a82cd2d to your computer and use it in GitHub Desktop.
Save coreyward/e70642a41e2181641a817a632a82cd2d to your computer and use it in GitHub Desktop.
Inject JSDoc comments into generated TypeScript Type definition files
/**
* This script injects JSDoc comments from the JS source files into the type
* definition files. This is necessary because the type definition files
* generated by TypeScript do not include JSDoc comments.
*
* @see https://github.com/microsoft/TypeScript/issues/14619
*
* The strategy is a bit hacky, but straightforward:
*
* 1. Recursively walk the output folder looking for .d.ts files
* 2. For each .d.ts file, find the corresponding .js file
* 3. Read the type definition file and identify function declarations that do
* not have JSDoc comments
* 4. Read the .js file and find the corresponding function declarations that
* have JSDoc comments
* 5. Extract matched comments and strip redundant information about types
* 6. Inject the comments into the type definition file
*
* This has some shortcomings. There is no actual parsing or static analysis
* going on, so it's possible that some functions or comments will be missed.
* It's also plausible that something matches unexpectedly and breaks the type
* file. Since the output folder is generated anyways, these are hopefully easy
* to remedy issues.
*
* NOTES:
* - You may need to alter the source-file identification. For my purposes,
* substituting `.js` in place of `.d.ts` was all that was needed. If you
* have `.jsx` source files, you will need to change that.
* - This will NOT work with TypeScript source files in a majority of cases.
*
*/
const fs = require("fs")
const path = require("path")
const srcFolder = path.join(__dirname, "src")
const outputFolder = path.join(__dirname, "dist")
const getFiles = (dir, done) => {
let results = []
fs.readdirSync(dir).forEach((file) => {
file = path.join(dir, file)
const stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
results = results.concat(getFiles(file, done))
} else {
results.push(file)
}
})
return results
}
getFiles(outputFolder).forEach((file) => {
if (!file.endsWith(".d.ts")) return
const relativePath = path.relative(outputFolder, file)
const srcFile = path.join(srcFolder, relativePath).replace(".d.ts", ".js")
if (fs.existsSync(srcFile)) {
injectComments(srcFile, file)
}
})
console.log("Done.")
function injectComments(srcFile, typeFile) {
const fileData = fs.readFileSync(srcFile, "utf8")
const typeData = fs.readFileSync(typeFile, "utf8")
const functionDeclarationMatches = Array.from(
typeData.matchAll(
/(?<!\*\/\n)(?:export|declare) function ([a-zA-Z0-9]+)\(/g
)
)
const output = functionDeclarationMatches.reduce(
(output, { 0: matchedText, 1: functionName }) => {
const functionDefinitionLinePattern = `(?:export (?:default )?)?(?:const|let|var|function) ${functionName}[^a-zA-Z0-9.]`
const pattern = new RegExp(
`(\\/\\*\\*\\s*\n([^*]|(\\*(?!\\/)))*\\*\\/)\n${functionDefinitionLinePattern}`
)
const match = fileData.match(pattern)
const jsDocComment = match?.[1]
if (jsDocComment) {
output = output.replace(
matchedText,
[stripTypeComments(jsDocComment), matchedText].join("\n")
)
}
return output
},
typeData
)
if (output !== typeData) {
console.log(`Updating ${typeFile}`)
fs.writeFileSync(typeFile, output)
}
}
function stripTypeComments(comment) {
return comment
.replaceAll(/^.*?@(param|property|typedef|returns).*$\n/gm, "")
.replaceAll(/^[ *]+$\n/gm, "")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment