@webpieces/api-doc-model 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * The PROSE half of the model: the JSDoc body, the `@format` tag and the `@mcp` override.
4
+ *
5
+ * `{@link Foo.bar}` is flattened to `Foo.bar` HERE, at this boundary, and that placement is the
6
+ * point: a link is a TypeScript editor affordance, and every downstream renderer — OpenAPI
7
+ * `description`, an MCP tool description, an HTML page — would otherwise each have to know the inline
8
+ * tag grammar and each get it slightly differently wrong. One flattening, at extraction.
9
+ */
10
+ export declare class JsDoc {
11
+ /** The body text, links flattened, trimmed. Empty string when undocumented. */
12
+ readonly description: string;
13
+ /** The `@format` block tag's text, e.g. `email` / `date-time`. */
14
+ readonly format: string | undefined;
15
+ /**
16
+ * The `@mcp` block tag's text — the OPTIONAL agent-facing override. Undefined means the
17
+ * author wrote none, which a renderer answers by falling back to {@link description}. The
18
+ * fallback is NOT applied here: "the author wrote an agent-facing sentence" and "we reused
19
+ * the human one" are different facts, and only the first is worth trusting.
20
+ */
21
+ readonly mcp: string | undefined;
22
+ private constructor();
23
+ /** Read the JSDoc attached to one declaration. */
24
+ static read(node: ts.Node): JsDoc;
25
+ /**
26
+ * A comment's text with every inline tag replaced by its own text: `{@link Foo.bar}` -> `Foo.bar`,
27
+ * `{@link Foo.bar|the widget}` -> `the widget`.
28
+ *
29
+ * TypeScript hands a commented node either a plain string or an array of parts, and the link
30
+ * parts are the ones with structure. Both shapes are handled here so no caller has to.
31
+ */
32
+ private static flatten;
33
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JsDoc = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const ts = tslib_1.__importStar(require("typescript"));
6
+ /**
7
+ * The PROSE half of the model: the JSDoc body, the `@format` tag and the `@mcp` override.
8
+ *
9
+ * `{@link Foo.bar}` is flattened to `Foo.bar` HERE, at this boundary, and that placement is the
10
+ * point: a link is a TypeScript editor affordance, and every downstream renderer — OpenAPI
11
+ * `description`, an MCP tool description, an HTML page — would otherwise each have to know the inline
12
+ * tag grammar and each get it slightly differently wrong. One flattening, at extraction.
13
+ */
14
+ class JsDoc {
15
+ description;
16
+ format;
17
+ mcp;
18
+ constructor(
19
+ /** The body text, links flattened, trimmed. Empty string when undocumented. */
20
+ description,
21
+ /** The `@format` block tag's text, e.g. `email` / `date-time`. */
22
+ format,
23
+ /**
24
+ * The `@mcp` block tag's text — the OPTIONAL agent-facing override. Undefined means the
25
+ * author wrote none, which a renderer answers by falling back to {@link description}. The
26
+ * fallback is NOT applied here: "the author wrote an agent-facing sentence" and "we reused
27
+ * the human one" are different facts, and only the first is worth trusting.
28
+ */
29
+ mcp) {
30
+ this.description = description;
31
+ this.format = format;
32
+ this.mcp = mcp;
33
+ }
34
+ /** Read the JSDoc attached to one declaration. */
35
+ // webpieces-disable no-function-outside-class -- static factory; JsDoc has a private constructor so a caller cannot invent prose
36
+ static read(node) {
37
+ const symbolLike = node;
38
+ const blocks = symbolLike.jsDoc ?? [];
39
+ const bodies = [];
40
+ let format;
41
+ let mcp;
42
+ for (const block of blocks) {
43
+ bodies.push(JsDoc.flatten(block.comment));
44
+ for (const tag of block.tags ?? []) {
45
+ const name = tag.tagName.text;
46
+ const text = JsDoc.flatten(tag.comment);
47
+ if (name === 'format' && text !== '') {
48
+ format = text;
49
+ }
50
+ else if (name === 'mcp' && text !== '') {
51
+ mcp = text;
52
+ }
53
+ }
54
+ }
55
+ return new JsDoc(bodies.join('\n').trim(), format, mcp);
56
+ }
57
+ /**
58
+ * A comment's text with every inline tag replaced by its own text: `{@link Foo.bar}` -> `Foo.bar`,
59
+ * `{@link Foo.bar|the widget}` -> `the widget`.
60
+ *
61
+ * TypeScript hands a commented node either a plain string or an array of parts, and the link
62
+ * parts are the ones with structure. Both shapes are handled here so no caller has to.
63
+ */
64
+ // webpieces-disable no-function-outside-class -- private static helper of this class
65
+ static flatten(comment) {
66
+ if (comment === undefined) {
67
+ return '';
68
+ }
69
+ if (typeof comment === 'string') {
70
+ return comment.trim();
71
+ }
72
+ const parts = [];
73
+ for (const part of comment) {
74
+ if (ts.isJSDocLink(part) || ts.isJSDocLinkCode(part) || ts.isJSDocLinkPlain(part)) {
75
+ // `text` is whatever followed the target ('|the widget'); the NAME is the target.
76
+ const label = part.text.replace(/^[|\s]+/, '').trim();
77
+ const target = part.name ? part.name.getText() : '';
78
+ parts.push(label !== '' ? label : target);
79
+ }
80
+ else {
81
+ parts.push(part.text);
82
+ }
83
+ }
84
+ return parts.join('').trim();
85
+ }
86
+ }
87
+ exports.JsDoc = JsDoc;
88
+ //# sourceMappingURL=JsDoc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsDoc.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/extract/JsDoc.ts"],"names":[],"mappings":";;;;AAAA,uDAAiC;AAQjC;;;;;;;GAOG;AACH,MAAa,KAAK;IAGD;IAEA;IAOA;IAXb;IACI,+EAA+E;IACtE,WAAmB;IAC5B,kEAAkE;IACzD,MAA0B;IACnC;;;;;OAKG;IACM,GAAuB;QATvB,gBAAW,GAAX,WAAW,CAAQ;QAEnB,WAAM,GAAN,MAAM,CAAoB;QAO1B,QAAG,GAAH,GAAG,CAAoB;IACjC,CAAC;IAEJ,kDAAkD;IAClD,iIAAiI;IACjI,MAAM,CAAC,IAAI,CAAC,IAAa;QACrB,MAAM,UAAU,GAAG,IAAoB,CAAC;QACxC,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE,CAAC;QACtC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,MAA0B,CAAC;QAC/B,IAAI,GAAuB,CAAC;QAE5B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAC1C,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;gBACjC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;gBAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBACxC,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;oBACnC,MAAM,GAAG,IAAI,CAAC;gBAClB,CAAC;qBAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;oBACvC,GAAG,GAAG,IAAI,CAAC;gBACf,CAAC;YACL,CAAC;QACL,CAAC;QAED,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;OAMG;IACH,qFAAqF;IAC7E,MAAM,CAAC,OAAO,CAAC,OAA2D;QAC9E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;QACD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YACzB,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChF,kFAAkF;gBAClF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACtD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpD,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC9C,CAAC;iBAAM,CAAC;gBACJ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;QACL,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC;CACJ;AApED,sBAoEC","sourcesContent":["import * as ts from 'typescript';\n\n/**\n * A node the compiler has attached JSDoc blocks to. `jsDoc` is internal to the TypeScript API and is\n * therefore not on `ts.Node`, so it is named HERE, once, instead of being cast at the use site.\n */\ntype JsDocCarrier = ts.Node & { jsDoc?: ts.JSDoc[] };\n\n/**\n * The PROSE half of the model: the JSDoc body, the `@format` tag and the `@mcp` override.\n *\n * `{@link Foo.bar}` is flattened to `Foo.bar` HERE, at this boundary, and that placement is the\n * point: a link is a TypeScript editor affordance, and every downstream renderer — OpenAPI\n * `description`, an MCP tool description, an HTML page — would otherwise each have to know the inline\n * tag grammar and each get it slightly differently wrong. One flattening, at extraction.\n */\nexport class JsDoc {\n private constructor(\n /** The body text, links flattened, trimmed. Empty string when undocumented. */\n readonly description: string,\n /** The `@format` block tag's text, e.g. `email` / `date-time`. */\n readonly format: string | undefined,\n /**\n * The `@mcp` block tag's text — the OPTIONAL agent-facing override. Undefined means the\n * author wrote none, which a renderer answers by falling back to {@link description}. The\n * fallback is NOT applied here: \"the author wrote an agent-facing sentence\" and \"we reused\n * the human one\" are different facts, and only the first is worth trusting.\n */\n readonly mcp: string | undefined,\n ) {}\n\n /** Read the JSDoc attached to one declaration. */\n // webpieces-disable no-function-outside-class -- static factory; JsDoc has a private constructor so a caller cannot invent prose\n static read(node: ts.Node): JsDoc {\n const symbolLike = node as JsDocCarrier;\n const blocks = symbolLike.jsDoc ?? [];\n const bodies: string[] = [];\n let format: string | undefined;\n let mcp: string | undefined;\n\n for (const block of blocks) {\n bodies.push(JsDoc.flatten(block.comment));\n for (const tag of block.tags ?? []) {\n const name = tag.tagName.text;\n const text = JsDoc.flatten(tag.comment);\n if (name === 'format' && text !== '') {\n format = text;\n } else if (name === 'mcp' && text !== '') {\n mcp = text;\n }\n }\n }\n\n return new JsDoc(bodies.join('\\n').trim(), format, mcp);\n }\n\n /**\n * A comment's text with every inline tag replaced by its own text: `{@link Foo.bar}` -> `Foo.bar`,\n * `{@link Foo.bar|the widget}` -> `the widget`.\n *\n * TypeScript hands a commented node either a plain string or an array of parts, and the link\n * parts are the ones with structure. Both shapes are handled here so no caller has to.\n */\n // webpieces-disable no-function-outside-class -- private static helper of this class\n private static flatten(comment: string | ts.NodeArray<ts.JSDocComment> | undefined): string {\n if (comment === undefined) {\n return '';\n }\n if (typeof comment === 'string') {\n return comment.trim();\n }\n const parts: string[] = [];\n for (const part of comment) {\n if (ts.isJSDocLink(part) || ts.isJSDocLinkCode(part) || ts.isJSDocLinkPlain(part)) {\n // `text` is whatever followed the target ('|the widget'); the NAME is the target.\n const label = part.text.replace(/^[|\\s]+/, '').trim();\n const target = part.name ? part.name.getText() : '';\n parts.push(label !== '' ? label : target);\n } else {\n parts.push(part.text);\n }\n }\n return parts.join('').trim();\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Pointer-style locations, in ONE place.
4
+ *
5
+ * Every `UnmappedType` and every {@link ApiDocExtractionError} carries one, and they have to agree:
6
+ * an agent that is handed two different spellings of "where" for the same file cannot tell whether
7
+ * it is looking at one problem or two.
8
+ */
9
+ export declare class SourceLocation {
10
+ static of(node: ts.Node): string;
11
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SourceLocation = void 0;
4
+ /**
5
+ * Pointer-style locations, in ONE place.
6
+ *
7
+ * Every `UnmappedType` and every {@link ApiDocExtractionError} carries one, and they have to agree:
8
+ * an agent that is handed two different spellings of "where" for the same file cannot tell whether
9
+ * it is looking at one problem or two.
10
+ */
11
+ class SourceLocation {
12
+ // webpieces-disable no-function-outside-class -- static helper on the class that defines the format
13
+ static of(node) {
14
+ const file = node.getSourceFile();
15
+ if (!file) {
16
+ return '<unknown>';
17
+ }
18
+ const position = file.getLineAndCharacterOfPosition(node.getStart(file));
19
+ return `${file.fileName}:${position.line + 1}:${position.character + 1}`;
20
+ }
21
+ }
22
+ exports.SourceLocation = SourceLocation;
23
+ //# sourceMappingURL=SourceLocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SourceLocation.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/extract/SourceLocation.ts"],"names":[],"mappings":";;;AAEA;;;;;;GAMG;AACH,MAAa,cAAc;IACvB,oGAAoG;IACpG,MAAM,CAAC,EAAE,CAAC,IAAa;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO,WAAW,CAAC;QACvB,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACzE,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC,IAAI,QAAQ,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;IAC7E,CAAC;CACJ;AAVD,wCAUC","sourcesContent":["import * as ts from 'typescript';\n\n/**\n * Pointer-style locations, in ONE place.\n *\n * Every `UnmappedType` and every {@link ApiDocExtractionError} carries one, and they have to agree:\n * an agent that is handed two different spellings of \"where\" for the same file cannot tell whether\n * it is looking at one problem or two.\n */\nexport class SourceLocation {\n // webpieces-disable no-function-outside-class -- static helper on the class that defines the format\n static of(node: ts.Node): string {\n const file = node.getSourceFile();\n if (!file) {\n return '<unknown>';\n }\n const position = file.getLineAndCharacterOfPosition(node.getStart(file));\n return `${file.fileName}:${position.line + 1}:${position.character + 1}`;\n }\n}\n"]}
@@ -0,0 +1,111 @@
1
+ import * as ts from 'typescript';
2
+ import { DocumentedType, UnmappedType } from '../model/ApiDocModel';
3
+ import { TypeRef } from '../model/TypeRef';
4
+ /**
5
+ * Resolve declared TypeScript types into {@link TypeRef}s, registering every NAMED type it meets as
6
+ * a {@link DocumentedType} a renderer can `$ref`.
7
+ *
8
+ * ## It walks TYPE NODES, not checker types, and that is load-bearing
9
+ *
10
+ * `type Integer = number` resolves, in the checker, to `number` — the alias is gone. Integer-ness is
11
+ * therefore invisible to a checker-driven walk, and the whole reason `Integer` is the PREFERRED
12
+ * spelling is that it COMPOSES: `Integer[]` and `Record<string, Integer>` say exactly which thing is
13
+ * an integer, where a decorator on `counts?: number[]` cannot (the same ambiguity the deleted
14
+ * `arrayItems` argument had). Walking the written syntax is what keeps that composition readable.
15
+ *
16
+ * ## Cycles terminate BY CONSTRUCTION
17
+ *
18
+ * Every named type is ONE entry in the model, registered before its fields are walked, so a type
19
+ * that refers back to itself resolves to a `$ref` at the second visit and stops. There is NO depth
20
+ * counter anywhere — a deep-but-finite graph of 7, 20 or 200 named hops is fully expanded, because
21
+ * truncating one would silently publish an incomplete document. The ONLY thing that is cut is a
22
+ * self-referential ANONYMOUS type, and it is cut by NODE IDENTITY (it has no name to `$ref`), with an
23
+ * {@link UnmappedType} recorded so #982's guard has something to name.
24
+ */
25
+ export declare class TypeResolver {
26
+ private readonly checker;
27
+ private readonly types;
28
+ private readonly unmapped;
29
+ /** Named types already registered (or mid-registration) — the cycle stop for the NAMED case. */
30
+ private readonly registered;
31
+ /** Anonymous type literals currently being expanded — the cycle stop for the ANONYMOUS case. */
32
+ private readonly expandingAnonymous;
33
+ constructor(checker: ts.TypeChecker);
34
+ /** Every named type reached so far, by name. */
35
+ collectedTypes(): ReadonlyMap<string, DocumentedType>;
36
+ /** Everything that could not be represented — recorded, never dropped. */
37
+ collectedUnmapped(): readonly UnmappedType[];
38
+ /** Resolve one written type, registering whatever named types it reaches. */
39
+ resolve(node: ts.TypeNode, ownerName: string): TypeRef;
40
+ /** `string` / `number` / `boolean` / `null` / `unknown`, or undefined for anything else. */
41
+ private static keywordOf;
42
+ private resolveLiteral;
43
+ /**
44
+ * A union, after `null` / `undefined` have been dropped (the FIELD records those as nullable /
45
+ * optional — see {@link fieldOf} — because `{}` and `{x: null}` are different wire documents).
46
+ *
47
+ * Three outcomes, in this order:
48
+ * - every branch a string literal -> an ENUM
49
+ * - one branch left -> that branch
50
+ * - every branch a named object -> a UNION, with a DERIVED discriminator when every branch
51
+ * carries the same property typed as ONE string literal
52
+ * - anything else -> an {@link UnmappedType}. No invented discriminator, ever:
53
+ * a union TypeScript itself cannot narrow is not one a
54
+ * renderer may claim to.
55
+ */
56
+ private resolveUnion;
57
+ /** `null` and `undefined` branches — recorded as nullable/optional on the FIELD, not in the union. */
58
+ private static isNullish;
59
+ /**
60
+ * The DERIVED discriminator: the property every branch declares as exactly ONE string literal,
61
+ * with a value no two branches share. Derived, never invented — if the source does not narrow,
62
+ * neither does the document.
63
+ */
64
+ private deriveDiscriminator;
65
+ /** A union written as a named `type X = A | B` becomes its own model entry, so #982 can `$ref` it. */
66
+ private registerUnionAlias;
67
+ /**
68
+ * `Integer`, `Array<T>`, `Record<string, V>`, `Promise<T>`, and everything named — an interface,
69
+ * a class, or a type alias. Anything else (a `Map`, a `Date`, a generic parameter) is recorded
70
+ * rather than guessed at.
71
+ */
72
+ private resolveReference;
73
+ /** `type X = ...` — registered under X when it has a shape of its own, else transparent. */
74
+ private resolveAlias;
75
+ /** A TS `enum` of string members — the one non-union enum shape a document can carry. */
76
+ private registerStringEnum;
77
+ /**
78
+ * Register a named object shape, RESERVING THE NAME BEFORE walking its fields. That order is the
79
+ * whole cycle story: a type that refers back to itself meets `registered.has(name)` on the second
80
+ * visit and resolves to a `$ref`, so the walk terminates with no counter and no truncation.
81
+ */
82
+ private registerNamedObject;
83
+ /**
84
+ * An inline `{ ... }`. It has no name to `$ref`, so it is registered under an OWNER-DERIVED one
85
+ * (`Parent.field`) — a renderer needs a target, and a name derived from where the shape is
86
+ * written is the only honest one available.
87
+ *
88
+ * A self-referential anonymous type is cut HERE, by NODE IDENTITY. It cannot be a `$ref` (there
89
+ * is no declared name to point at) and it cannot be expanded (it would never end), so it is
90
+ * recorded as unmapped and the walk returns.
91
+ */
92
+ private resolveAnonymousObject;
93
+ /** ONE property of a DTO — its type, its optionality, its prose and its numeric constraints. */
94
+ private fieldOf;
95
+ /**
96
+ * `@WpMin` / `@WpMax` on a non-numeric field is a BUILD FAILURE, not a warning. A minimum on a
97
+ * string is not something a renderer can emit sensibly, and a document that silently dropped it
98
+ * would publish a contract weaker than the one its author wrote down.
99
+ */
100
+ private assertNumericConstraintsFit;
101
+ /** `@WpInt()` decorates the FIELD, so the integer flag is pushed onto the right leaf. */
102
+ private static markInteger;
103
+ private static declaresUndefined;
104
+ private static declaresNull;
105
+ private static hasDecorator;
106
+ private static decoratorCall;
107
+ private numericArgument;
108
+ /** The declaration a type name points at, through imports and aliases. */
109
+ private declarationOf;
110
+ private recordUnmapped;
111
+ }