|
45 | 45 | // |
46 | 46 | // This lever only works where api-documenter honours overloadIndex, which is |
47 | 47 | // ApiParameterListMixin — methods, functions, constructors and signatures. A |
48 | | -// TypeAlias colliding with a Variable (@imqueue/pg-prisma's AuditAction) has NO |
49 | | -// filename lever at all, so it is reported and left for api-pages.js to fail on: |
50 | | -// the remedy there is a source change or an @internal, and pretending otherwise |
| 48 | +// TypeAlias colliding with a Variable has NO filename lever at all. Where the two |
| 49 | +// are one thing declared twice, (4) folds them; where they are genuinely unrelated |
| 50 | +// there is nothing to be done here, so it is reported and left for api-pages.js to |
| 51 | +// fail on: the remedy is a source change or an @internal, and pretending otherwise |
51 | 52 | // would ship a lost page. |
52 | 53 | // |
53 | 54 | // --- 3. ambient-collision renames (`Response_2`) ---------------------------- |
|
80 | 81 | // api-documenter resolves signature links through those references, so changing |
81 | 82 | // `name` alone turns a working cross-reference into plain text with no link and |
82 | 83 | // no warning. |
| 84 | +// |
| 85 | +// --- 4. const object + its derived union type (`AuditAction`) ---------------- |
| 86 | +// |
| 87 | +// The standard TypeScript way to get an enum without `enum` is to declare a frozen |
| 88 | +// object and derive a union from its values: |
| 89 | +// |
| 90 | +// export const AuditAction = { INSERT: 'INSERT', … } as const; |
| 91 | +// export type AuditAction = (typeof AuditAction)[keyof typeof AuditAction]; |
| 92 | +// |
| 93 | +// That is ONE concept the language requires you to declare twice, once in value |
| 94 | +// space and once in type space — which is why both declarations are public and why |
| 95 | +// neither can be renamed or marked @internal without breaking consumers. |
| 96 | +// @imqueue/pg-prisma does this at src/audit.ts:27+32 and it is the last thing |
| 97 | +// standing between that package and a published reference. |
| 98 | +// |
| 99 | +// It reaches api-documenter as a TypeAlias and a Variable of the same name, both |
| 100 | +// wanting pg-prisma.auditaction.md, and (2) has no lever for either kind. So this |
| 101 | +// folds them onto one page instead: the Variable survives and the alias's |
| 102 | +// declaration is appended to its signature, giving a page that shows both lines |
| 103 | +// exactly as the source writes them. |
| 104 | +// |
| 105 | +// The discriminator is exact rather than a guess — the alias must carry a Reference |
| 106 | +// token pointing at the variable's own canonicalReference, which is what |
| 107 | +// `typeof AuditAction` compiles to in the model. A TypeAlias and a Variable that |
| 108 | +// merely happen to share a name are two different things and are NOT folded; they |
| 109 | +// keep the (2) report and the api-pages.js failure, because putting two unrelated |
| 110 | +// symbols on one page would be a worse lie than failing. |
| 111 | +// |
| 112 | +// The Variable is the survivor, not the alias, for a mechanical reason: the alias's |
| 113 | +// excerpt reads `export type X = (typeof X)[keyof typeof X];`, a complete statement |
| 114 | +// that appends cleanly, whereas the variable's reads `X: { … }`, which is not. It |
| 115 | +// also keeps the alias's Reference tokens resolvable — they point at the variable, |
| 116 | +// which is now the page itself. |
| 117 | +// |
| 118 | +// One cost, stated because it is invisible in the output: the package page lists |
| 119 | +// the symbol under "Variables" only, since the row under "Type Aliases" came from |
| 120 | +// the node that was folded away. The page it links to documents both declarations. |
83 | 121 |
|
84 | 122 | 'use strict'; |
85 | 123 |
|
@@ -183,6 +221,101 @@ function mergeDeclarationMerges(model, notes) { |
183 | 221 | } |
184 | 222 | } |
185 | 223 |
|
| 224 | +// Is this TypeAlias derived from that Variable? True only when the alias names the |
| 225 | +// variable itself, which is what `typeof <name>` leaves in the model — so a |
| 226 | +// same-named pair that has nothing to do with each other is not matched. |
| 227 | +function aliasDerivesFrom(alias, variable) { |
| 228 | + return (alias.excerptTokens || []).some( |
| 229 | + token => token.kind === 'Reference' |
| 230 | + && token.canonicalReference === variable.canonicalReference, |
| 231 | + ); |
| 232 | +} |
| 233 | + |
| 234 | +// Point every canonicalReference at `toRef` that currently names `fromRef`, |
| 235 | +// including references to its members (`…!X:type#member`). Used when a node is |
| 236 | +// folded away: a reference left naming the removed node resolves to nothing, and |
| 237 | +// api-documenter renders that as unlinked plain text without warning. |
| 238 | +function retargetReferences(node, fromRef, toRef) { |
| 239 | + let rewritten = 0; |
| 240 | + |
| 241 | + const visit = (item) => { |
| 242 | + if (Array.isArray(item)) { |
| 243 | + item.forEach(visit); |
| 244 | + |
| 245 | + return; |
| 246 | + } |
| 247 | + if (!item || typeof item !== 'object') return; |
| 248 | + |
| 249 | + const ref = item.canonicalReference; |
| 250 | + |
| 251 | + if (typeof ref === 'string' && (ref === fromRef || ref.startsWith(`${fromRef}#`))) { |
| 252 | + item.canonicalReference = toRef + ref.slice(fromRef.length); |
| 253 | + rewritten++; |
| 254 | + } |
| 255 | + for (const value of Object.values(item)) { |
| 256 | + if (value && typeof value === 'object') visit(value); |
| 257 | + } |
| 258 | + }; |
| 259 | + |
| 260 | + visit(node); |
| 261 | + |
| 262 | + return rewritten; |
| 263 | +} |
| 264 | + |
| 265 | +// Fold `type X = (typeof X)[keyof typeof X]` into `const X`, so the pair the |
| 266 | +// language forces you to declare twice occupies one page instead of losing one. |
| 267 | +function foldDerivedUnionTypes(model, notes) { |
| 268 | + for (const parent of [...containers(model)]) { |
| 269 | + const byName = new Map(); |
| 270 | + |
| 271 | + for (const member of parent.members) { |
| 272 | + if (member.name === undefined) continue; |
| 273 | + if (!['TypeAlias', 'Variable'].includes(member.kind)) continue; |
| 274 | + if (!byName.has(member.name)) byName.set(member.name, []); |
| 275 | + byName.get(member.name).push(member); |
| 276 | + } |
| 277 | + |
| 278 | + for (const [name, decls] of byName) { |
| 279 | + if (decls.length !== 2) continue; |
| 280 | + |
| 281 | + const alias = decls.find(d => d.kind === 'TypeAlias'); |
| 282 | + const variable = decls.find(d => d.kind === 'Variable'); |
| 283 | + |
| 284 | + if (!alias || !variable) continue; |
| 285 | + if (!aliasDerivesFrom(alias, variable)) { |
| 286 | + continue; // same name, unrelated declarations — not ours to merge |
| 287 | + } |
| 288 | + |
| 289 | + // Append the alias's own tokens rather than a reconstructed type: the page |
| 290 | + // then shows the two declarations the source actually has, references and all. |
| 291 | + variable.excerptTokens = [ |
| 292 | + ...(variable.excerptTokens || []), |
| 293 | + { kind: 'Content', text: '\n\n' }, |
| 294 | + ...(alias.excerptTokens || []).map(token => ({ ...token })), |
| 295 | + ]; |
| 296 | + |
| 297 | + // Same rule as (1): keep the survivor's prose, inherit the folded node's only |
| 298 | + // when the survivor has none, so a documented alias is not thrown away. |
| 299 | + if (!String(variable.docComment || '').trim() && String(alias.docComment || '').trim()) { |
| 300 | + variable.docComment = alias.docComment; |
| 301 | + } |
| 302 | + |
| 303 | + parent.members = parent.members.filter(m => m !== alias); |
| 304 | + |
| 305 | + const refs = retargetReferences( |
| 306 | + model, alias.canonicalReference, variable.canonicalReference, |
| 307 | + ); |
| 308 | + |
| 309 | + notes.push( |
| 310 | + `folded type ${name} into const ${name} and retargeted ${refs} canonical ` + |
| 311 | + 'reference(s): the alias is derived from the const, so the two are one ' + |
| 312 | + 'concept TypeScript makes you declare twice — both declarations are now on ' + |
| 313 | + `the ${safeName(name)} page, which the package page lists under Variables`, |
| 314 | + ); |
| 315 | + } |
| 316 | + } |
| 317 | +} |
| 318 | + |
186 | 319 | // Give same-filename siblings distinct overloadIndex values where the filename |
187 | 320 | // derivation honours them. |
188 | 321 | function disambiguateSiblings(model, notes) { |
@@ -357,6 +490,8 @@ function normalizeModel(model) { |
357 | 490 | const renames = []; |
358 | 491 |
|
359 | 492 | mergeDeclarationMerges(model, notes); |
| 493 | + // Before (2), so a pair this fixes never also draws (2)'s "no lever here" report. |
| 494 | + foldDerivedUnionTypes(model, notes); |
360 | 495 | disambiguateSiblings(model, notes); |
361 | 496 | // After (2), so a suffix this run assigned via overloadIndex is never mistaken |
362 | 497 | // for one api-extractor baked into a name. |
|
0 commit comments