mirror of
https://github.com/hanzoai/jose.git
synced 2026-09-24 17:00:01 +00:00
227 lines
8.3 KiB
JavaScript
227 lines
8.3 KiB
JavaScript
// Prepares the JSDoc in the published declarations for where it is actually read - an editor hover
|
|
// - by dropping what is written for the generated documentation and rewriting what only renders
|
|
// correctly on a docs page. docs/ is generated from src, so it keeps all of it.
|
|
//
|
|
// @example blocks - 22% of dist/types, and typedoc renders them on the page the reader is
|
|
// already on. Stripping them is not new: tools/postbump.cjs did it at
|
|
// release time, but its stripper treated any fence that was not ```js as
|
|
// the end of the example, so a ```ts one ended removal at the opening
|
|
// fence and left the body behind. jose 6.2.3 and 6.2.4 ship five orphaned
|
|
// code blocks and three stray `// @ts-ignore` lines in
|
|
// dist/types/jwks/remote.d.ts because of it. Doing it here instead puts
|
|
// the result under the type and packaging gates, and makes a local build
|
|
// match what is published.
|
|
//
|
|
// the subpath note - "This function is exported (as a named export) from the main 'jose'
|
|
// module entry point as well as from its subpath export
|
|
// 'jose/jwt/verify'." and its variants. Load-bearing on a docs page,
|
|
// since typedoc names its folders after source paths and that sentence is
|
|
// the only place the real specifier appears; noise in an editor hover.
|
|
//
|
|
// module documentation - TypeScript attaches it to the first import but does not expose it as
|
|
// useful module documentation in an editor.
|
|
//
|
|
// extended prose - only the first summary paragraph, alerts, signature-help tags, and
|
|
// @deprecated are useful in editor hovers. The rest remains in src for
|
|
// generated documentation.
|
|
//
|
|
// @ignore and friends - typedoc acts on them, TypeScript does not, so they reach the reader as
|
|
// a bare tag that means nothing to them.
|
|
//
|
|
// GitHub alerts - `> [!NOTE]` and the rest of the GFM set render as an admonition on
|
|
// GitHub and in the docs, and as a literal `[!NOTE]\` in a hover. The
|
|
// blockquote itself does carry over, so keep it and label it in words.
|
|
//
|
|
// Set JOSE_DEBUG_TYPES=1 to print the documentation stripped from publishable declarations.
|
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import { join, sep } from 'node:path'
|
|
|
|
const SUBPATH_NOTE = /^(?:This|These)\b[^.]*\bexported\b/
|
|
const BLOCK_TAG = /^@(\w+)/
|
|
const MARKER_TAG = /^@(?:ignore|internal|private|hidden)\s*$/
|
|
const PUBLISHED_TAGS = new Set(['param', 'returns', 'throws', 'deprecated'])
|
|
const REPORT_STRIPPED = process.env.JOSE_DEBUG_TYPES === '1'
|
|
const PRIVATE_DECLARATIONS = `${join('dist', 'types', 'lib')}${sep}`
|
|
/** The five alert types GitHub Flavored Markdown defines. */
|
|
const ALERT = /^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\\?\s*$/
|
|
const RELABELLED_ALERT = /^>\s*(?:Note|Tip|Important|Warning|Caution):\s/
|
|
|
|
/**
|
|
* Splits the text lines of a JSDoc body into paragraphs. Fenced code blocks stay whole, so neither
|
|
* a blank line nor a leading `@` inside one splits a paragraph or reads as a tag.
|
|
*/
|
|
function paragraphs(lines) {
|
|
const out = []
|
|
let current = []
|
|
let fenced = false
|
|
|
|
for (const line of lines) {
|
|
if (line.trimStart().startsWith('```')) fenced = !fenced
|
|
if (!fenced && line.trim() === '') {
|
|
if (current.length) out.push(current)
|
|
current = []
|
|
continue
|
|
}
|
|
current.push(line)
|
|
}
|
|
if (current.length) out.push(current)
|
|
return out
|
|
}
|
|
|
|
/** Splits adjacent block tags so filtering one tag cannot remove a neighboring retained tag. */
|
|
function blockTagSections(paragraph) {
|
|
const out = []
|
|
let current = []
|
|
let fenced = false
|
|
|
|
for (const line of paragraph) {
|
|
if (!fenced && BLOCK_TAG.test(line) && current.length) {
|
|
out.push(current)
|
|
current = []
|
|
}
|
|
current.push(line)
|
|
if (line.trimStart().startsWith('```')) fenced = !fenced
|
|
}
|
|
if (current.length) out.push(current)
|
|
return out
|
|
}
|
|
|
|
/** Turns `> [!NOTE]\` followed by the quote into a quote that opens with `Note:`. */
|
|
function relabelAlert(paragraph) {
|
|
const [marker, ...rest] = paragraph
|
|
const type = marker.trim().match(ALERT)?.[1]
|
|
if (!type || !rest.length) return null
|
|
|
|
const label = type.charAt(0) + type.slice(1).toLowerCase()
|
|
return [rest[0].replace(/^(\s*>\s*)/, `$1${label}: `), ...rest.slice(1)]
|
|
}
|
|
|
|
function* declarations(directory) {
|
|
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
a.name.localeCompare(b.name),
|
|
)
|
|
for (const entry of entries) {
|
|
const path = join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
yield* declarations(path)
|
|
} else if (entry.name.endsWith('.d.ts')) {
|
|
yield path
|
|
}
|
|
}
|
|
}
|
|
|
|
function transform(lines) {
|
|
const kept = []
|
|
const removed = []
|
|
let changed = false
|
|
let summary = false
|
|
// an @example runs until the next block tag that is not itself an @example - its description and
|
|
// its code fence are separate paragraphs, and there may be several in a row
|
|
let inExample = false
|
|
const sections = paragraphs(lines).flatMap(blockTagSections)
|
|
|
|
if (sections.some((paragraph) => paragraph[0].match(BLOCK_TAG)?.[1] === 'module')) {
|
|
return { kept, removed: paragraphs(lines), changed: true }
|
|
}
|
|
|
|
for (const paragraph of sections) {
|
|
const tag = paragraph[0].match(BLOCK_TAG)?.[1]
|
|
|
|
if (tag === 'example') {
|
|
inExample = true
|
|
changed = true
|
|
removed.push(paragraph)
|
|
continue
|
|
}
|
|
if (inExample) {
|
|
if (!tag) {
|
|
changed = true
|
|
removed.push(paragraph)
|
|
continue
|
|
}
|
|
inExample = false
|
|
}
|
|
if (SUBPATH_NOTE.test(paragraph[0]) || MARKER_TAG.test(paragraph[0])) {
|
|
changed = true
|
|
removed.push(paragraph)
|
|
continue
|
|
}
|
|
|
|
const relabelled = relabelAlert(paragraph)
|
|
if (relabelled) {
|
|
kept.push(relabelled)
|
|
changed = true
|
|
continue
|
|
}
|
|
if (RELABELLED_ALERT.test(paragraph[0])) {
|
|
kept.push(paragraph)
|
|
continue
|
|
}
|
|
if (tag) {
|
|
if (PUBLISHED_TAGS.has(tag)) {
|
|
const previous = kept.at(-1)
|
|
if (previous && BLOCK_TAG.test(previous[0])) {
|
|
previous.push(...paragraph)
|
|
} else {
|
|
kept.push(paragraph)
|
|
}
|
|
} else {
|
|
changed = true
|
|
removed.push(paragraph)
|
|
}
|
|
continue
|
|
}
|
|
if (!summary) {
|
|
kept.push(paragraph)
|
|
summary = true
|
|
} else {
|
|
changed = true
|
|
removed.push(paragraph)
|
|
}
|
|
}
|
|
|
|
return { kept, removed, changed }
|
|
}
|
|
|
|
let touched = 0
|
|
const reports = []
|
|
|
|
for (const file of declarations('dist/types')) {
|
|
const source = readFileSync(file, 'utf8')
|
|
|
|
// prettier-plugin-jsdoc puts the summary on the `/**` line when it fits, so both shapes occur
|
|
const updated = source.replace(
|
|
/^([ \t]*)\/\*\*[ \t]?([\s\S]*?)[ \t]*\*\/[ \t]*\n/gm,
|
|
(block, indent, body, offset) => {
|
|
const lines = body.split('\n').map((line) => line.replace(/^[ \t]*\*[ \t]?/, ''))
|
|
while (lines.length && lines.at(-1).trim() === '') lines.pop()
|
|
|
|
const { kept, removed, changed } = transform(lines)
|
|
if (!changed) return block
|
|
|
|
touched++
|
|
if (REPORT_STRIPPED && removed.length && !file.startsWith(PRIVATE_DECLARATIONS)) {
|
|
const line = source.slice(0, offset).split('\n').length
|
|
const stripped = removed.flatMap((paragraph, index) =>
|
|
index ? ['', ...paragraph] : paragraph,
|
|
)
|
|
reports.push(`${file}:${line}\n${stripped.map((line) => `- ${line}`).join('\n')}`)
|
|
}
|
|
if (!kept.length) return ''
|
|
|
|
const flat = kept.flatMap((paragraph, i) => (i ? ['', ...paragraph] : paragraph))
|
|
if (flat.length === 1) return `${indent}/** ${flat[0]} */\n`
|
|
|
|
const rendered = flat.map((line) => (line ? `${indent} * ${line}` : `${indent} *`)).join('\n')
|
|
return `${indent}/**\n${rendered}\n${indent} */\n`
|
|
},
|
|
)
|
|
|
|
if (updated !== source) writeFileSync(file, updated, 'utf8')
|
|
}
|
|
|
|
if (reports.length) {
|
|
console.log(`stripped published declaration documentation\n\n${reports.join('\n\n')}\n`)
|
|
}
|
|
console.log(`rewrote ${touched} declaration comment(s) for editor hovers`)
|