@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.
- package/README.md +21 -0
- package/package.json +28 -0
- package/src/extract/ApiDocExtractionError.d.ts +30 -0
- package/src/extract/ApiDocExtractionError.js +39 -0
- package/src/extract/ApiDocExtractionError.js.map +1 -0
- package/src/extract/ApiDocExtractor.d.ts +59 -0
- package/src/extract/ApiDocExtractor.js +220 -0
- package/src/extract/ApiDocExtractor.js.map +1 -0
- package/src/extract/ConstantFolder.d.ts +30 -0
- package/src/extract/ConstantFolder.js +103 -0
- package/src/extract/ConstantFolder.js.map +1 -0
- package/src/extract/JsDoc.d.ts +33 -0
- package/src/extract/JsDoc.js +88 -0
- package/src/extract/JsDoc.js.map +1 -0
- package/src/extract/SourceLocation.d.ts +11 -0
- package/src/extract/SourceLocation.js +23 -0
- package/src/extract/SourceLocation.js.map +1 -0
- package/src/extract/TypeResolver.d.ts +111 -0
- package/src/extract/TypeResolver.js +445 -0
- package/src/extract/TypeResolver.js.map +1 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/model/ApiDocModel.d.ts +202 -0
- package/src/model/ApiDocModel.js +231 -0
- package/src/model/ApiDocModel.js.map +1 -0
- package/src/model/TypeRef.d.ts +48 -0
- package/src/model/TypeRef.js +83 -0
- package/src/model/TypeRef.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @webpieces/api-doc-model
|
|
2
|
+
|
|
3
|
+
Read a webpieces API contract with the TypeScript compiler API and produce one in-memory `ApiDocModel`.
|
|
4
|
+
|
|
5
|
+
No renderer, no runtime behaviour, no file output. This is the single extraction pass that OpenAPI documents and MCP tool lists are both rendered from, so the two can never disagree about what the contract says.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { ApiDocExtractor } from '@webpieces/api-doc-model';
|
|
9
|
+
|
|
10
|
+
const model = new ApiDocExtractor().extractFile('/abs/path/to/SaveApi.ts', {
|
|
11
|
+
experimentalDecorators: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
model.contractName; // 'SaveApi'
|
|
15
|
+
model.basePath; // '/api/save'
|
|
16
|
+
model.endpoints; // DocumentedEndpoint[]
|
|
17
|
+
model.types; // ReadonlyMap<string, DocumentedType> — a renderer's $ref targets
|
|
18
|
+
model.unmapped; // UnmappedType[] — recorded, never dropped
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It depends on `typescript` and nothing else, so it can be pointed at any project's contract. See `responsibilities.md` for what is in and out of scope, why that dependency constraint is the product rather than tidiness, and why both `Integer` and `@WpInt()` are accepted spellings of integer-ness.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webpieces/api-doc-model",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript-compiler-API extractor producing one in-memory ApiDocModel from a webpieces API contract. No renderer, no runtime, no app-specific import.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"author": "Dean Hiller",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/deanhiller/webpieces-ts.git",
|
|
13
|
+
"directory": "packages/docs/api-doc-model"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"webpieces",
|
|
17
|
+
"openapi",
|
|
18
|
+
"mcp",
|
|
19
|
+
"docs",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"typescript": "5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE failure type this package throws. Extraction either produces a complete
|
|
3
|
+
* {@link ApiDocModel} or throws this — it never prints, never warns, and never guesses.
|
|
4
|
+
*
|
|
5
|
+
* It is a structured throw to the caller's single top-level handler, per
|
|
6
|
+
* `.claude/review/error-output.md`: `location` and `cure` are carried as FIELDS rather than baked
|
|
7
|
+
* into `message`, so a renderer (#982) can print them per audience, and it hand-numbers nothing.
|
|
8
|
+
*
|
|
9
|
+
* It is NOT `RuleFailError`. That type lives in `@webpieces/rules-config`, and this package depends
|
|
10
|
+
* on `typescript` and nothing else so it can be pointed at any upstream project's contract — see
|
|
11
|
+
* `responsibilities.md`. A dependency added here to reuse an error class would end that guarantee
|
|
12
|
+
* for every consumer, which is a far larger cost than one more error type.
|
|
13
|
+
*
|
|
14
|
+
* WHY a throw and not a recorded warning: the two cases that reach it are a path that cannot be
|
|
15
|
+
* constant-folded and a numeric constraint on a non-numeric field. Both mean the DOCUMENT would be
|
|
16
|
+
* wrong — a guessed path, or a range on a string — and a partner-grade document that is quietly
|
|
17
|
+
* wrong is worse than one that failed to build. Something the extractor merely cannot REPRESENT is a
|
|
18
|
+
* different thing and is recorded as an {@link UnmappedType} instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ApiDocExtractionError extends Error {
|
|
21
|
+
/** Pointer-style `path/to/File.ts:12:5`, so an agent can open the exact line. */
|
|
22
|
+
readonly location: string;
|
|
23
|
+
/** What to do instead, in one sentence. No numbering — the caller's renderer owns that. */
|
|
24
|
+
readonly cure: string;
|
|
25
|
+
constructor(message: string,
|
|
26
|
+
/** Pointer-style `path/to/File.ts:12:5`, so an agent can open the exact line. */
|
|
27
|
+
location: string,
|
|
28
|
+
/** What to do instead, in one sentence. No numbering — the caller's renderer owns that. */
|
|
29
|
+
cure: string);
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiDocExtractionError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The ONE failure type this package throws. Extraction either produces a complete
|
|
6
|
+
* {@link ApiDocModel} or throws this — it never prints, never warns, and never guesses.
|
|
7
|
+
*
|
|
8
|
+
* It is a structured throw to the caller's single top-level handler, per
|
|
9
|
+
* `.claude/review/error-output.md`: `location` and `cure` are carried as FIELDS rather than baked
|
|
10
|
+
* into `message`, so a renderer (#982) can print them per audience, and it hand-numbers nothing.
|
|
11
|
+
*
|
|
12
|
+
* It is NOT `RuleFailError`. That type lives in `@webpieces/rules-config`, and this package depends
|
|
13
|
+
* on `typescript` and nothing else so it can be pointed at any upstream project's contract — see
|
|
14
|
+
* `responsibilities.md`. A dependency added here to reuse an error class would end that guarantee
|
|
15
|
+
* for every consumer, which is a far larger cost than one more error type.
|
|
16
|
+
*
|
|
17
|
+
* WHY a throw and not a recorded warning: the two cases that reach it are a path that cannot be
|
|
18
|
+
* constant-folded and a numeric constraint on a non-numeric field. Both mean the DOCUMENT would be
|
|
19
|
+
* wrong — a guessed path, or a range on a string — and a partner-grade document that is quietly
|
|
20
|
+
* wrong is worse than one that failed to build. Something the extractor merely cannot REPRESENT is a
|
|
21
|
+
* different thing and is recorded as an {@link UnmappedType} instead.
|
|
22
|
+
*/
|
|
23
|
+
class ApiDocExtractionError extends Error {
|
|
24
|
+
location;
|
|
25
|
+
cure;
|
|
26
|
+
constructor(message,
|
|
27
|
+
/** Pointer-style `path/to/File.ts:12:5`, so an agent can open the exact line. */
|
|
28
|
+
location,
|
|
29
|
+
/** What to do instead, in one sentence. No numbering — the caller's renderer owns that. */
|
|
30
|
+
cure) {
|
|
31
|
+
super(`${message} (${location})`);
|
|
32
|
+
this.location = location;
|
|
33
|
+
this.cure = cure;
|
|
34
|
+
this.name = 'ApiDocExtractionError';
|
|
35
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ApiDocExtractionError = ApiDocExtractionError;
|
|
39
|
+
//# sourceMappingURL=ApiDocExtractionError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiDocExtractionError.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/extract/ApiDocExtractionError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,qBAAsB,SAAQ,KAAK;IAI/B;IAEA;IALb,YACI,OAAe;IACf,iFAAiF;IACxE,QAAgB;IACzB,2FAA2F;IAClF,IAAY;QAErB,KAAK,CAAC,GAAG,OAAO,KAAK,QAAQ,GAAG,CAAC,CAAC;QAJzB,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,SAAI,GAAJ,IAAI,CAAQ;QAGrB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;CACJ;AAZD,sDAYC","sourcesContent":["/**\n * The ONE failure type this package throws. Extraction either produces a complete\n * {@link ApiDocModel} or throws this — it never prints, never warns, and never guesses.\n *\n * It is a structured throw to the caller's single top-level handler, per\n * `.claude/review/error-output.md`: `location` and `cure` are carried as FIELDS rather than baked\n * into `message`, so a renderer (#982) can print them per audience, and it hand-numbers nothing.\n *\n * It is NOT `RuleFailError`. That type lives in `@webpieces/rules-config`, and this package depends\n * on `typescript` and nothing else so it can be pointed at any upstream project's contract — see\n * `responsibilities.md`. A dependency added here to reuse an error class would end that guarantee\n * for every consumer, which is a far larger cost than one more error type.\n *\n * WHY a throw and not a recorded warning: the two cases that reach it are a path that cannot be\n * constant-folded and a numeric constraint on a non-numeric field. Both mean the DOCUMENT would be\n * wrong — a guessed path, or a range on a string — and a partner-grade document that is quietly\n * wrong is worse than one that failed to build. Something the extractor merely cannot REPRESENT is a\n * different thing and is recorded as an {@link UnmappedType} instead.\n */\nexport class ApiDocExtractionError extends Error {\n constructor(\n message: string,\n /** Pointer-style `path/to/File.ts:12:5`, so an agent can open the exact line. */\n readonly location: string,\n /** What to do instead, in one sentence. No numbering — the caller's renderer owns that. */\n readonly cure: string,\n ) {\n super(`${message} (${location})`);\n this.name = 'ApiDocExtractionError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { ApiDocModel } from '../model/ApiDocModel';
|
|
3
|
+
/**
|
|
4
|
+
* ONE contract file -> ONE {@link ApiDocModel}. The single extraction pass both the OpenAPI documents
|
|
5
|
+
* and the MCP tool list (#982) are rendered from.
|
|
6
|
+
*
|
|
7
|
+
* ## Why the compiler API at all
|
|
8
|
+
*
|
|
9
|
+
* A DTO field's TYPE IS ERASED AT RUNTIME. Reflection can see that `save` takes one argument; it
|
|
10
|
+
* cannot see that the argument has a `deliveryWindow` that is a discriminated union of two shapes,
|
|
11
|
+
* one of which carries an ISO timestamp. Those are precisely the shapes a partner-grade document is
|
|
12
|
+
* made of, so the only place they exist is the source, and the only honest way to read the source is
|
|
13
|
+
* the compiler.
|
|
14
|
+
*
|
|
15
|
+
* ## Why it imports NO webpieces package
|
|
16
|
+
*
|
|
17
|
+
* It depends on `typescript` and nothing else, so it can be pointed at ANY upstream project's
|
|
18
|
+
* contract — which is the whole point of shipping it as a package rather than as a script in this
|
|
19
|
+
* repo. Decorators are therefore matched BY NAME on the syntax, never by importing the real
|
|
20
|
+
* decorator. One `@webpieces/core-util` import here would make the package unusable by anybody whose
|
|
21
|
+
* webpieces version differs from ours, which is everybody.
|
|
22
|
+
*
|
|
23
|
+
* ## What it does NOT do
|
|
24
|
+
*
|
|
25
|
+
* It emits nothing. No OpenAPI, no MCP tool definitions, no file — that is #982, and keeping the
|
|
26
|
+
* render out means the two renderers cannot drift apart about what the contract SAYS.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ApiDocExtractor {
|
|
29
|
+
/**
|
|
30
|
+
* Extract the contract in `entryFile`.
|
|
31
|
+
*
|
|
32
|
+
* @param entryFile absolute path to the `.ts` file holding the `@ApiPath` class.
|
|
33
|
+
* @param compilerOptions handed straight to `ts.createProgram`; the caller owns them because
|
|
34
|
+
* only the caller knows its own `paths` / `lib` setup.
|
|
35
|
+
* @throws ApiDocExtractionError when the file holds no `@ApiPath` class, or when something that
|
|
36
|
+
* must be exact (a path constant, a numeric bound) cannot be established.
|
|
37
|
+
*/
|
|
38
|
+
extractFile(entryFile: string, compilerOptions?: ts.CompilerOptions): ApiDocModel;
|
|
39
|
+
/** The same extraction against a program the caller already built. */
|
|
40
|
+
extract(program: ts.Program, source: ts.SourceFile): ApiDocModel;
|
|
41
|
+
/** The one `@ApiPath` class in the file. Zero is a hard failure; the FIRST wins if there are two. */
|
|
42
|
+
private findContract;
|
|
43
|
+
/** One `@Endpoint` method. Members without the decorator are not part of the contract. */
|
|
44
|
+
private endpointOf;
|
|
45
|
+
/** The FIRST parameter's declared type. An endpoint with no parameter has no request document. */
|
|
46
|
+
private requestOf;
|
|
47
|
+
/** The declared return type, unwrapped from `Promise<...>` by the resolver. */
|
|
48
|
+
private responseOf;
|
|
49
|
+
/** `@WpAuthPublic()`, `@WpAuthJwt({...})`, … — recorded verbatim; this package rules on nothing. */
|
|
50
|
+
private static authOf;
|
|
51
|
+
/** `@WpMcpTool({ name, readOnly, ... })` — the name plus whichever hints were declared. */
|
|
52
|
+
private static mcpToolOf;
|
|
53
|
+
/** `@MaskLog({ refreshToken: 'full' })` -> field name -> mask mode. */
|
|
54
|
+
private static maskLogOf;
|
|
55
|
+
private static booleanProperty;
|
|
56
|
+
private static stringProperty;
|
|
57
|
+
private static findProperty;
|
|
58
|
+
private static decoratorCall;
|
|
59
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiDocExtractor = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
6
|
+
const ApiDocModel_1 = require("../model/ApiDocModel");
|
|
7
|
+
const ApiDocExtractionError_1 = require("./ApiDocExtractionError");
|
|
8
|
+
const ConstantFolder_1 = require("./ConstantFolder");
|
|
9
|
+
const JsDoc_1 = require("./JsDoc");
|
|
10
|
+
const SourceLocation_1 = require("./SourceLocation");
|
|
11
|
+
const TypeResolver_1 = require("./TypeResolver");
|
|
12
|
+
/** The decorator names this extractor reads, by NAME. It imports none of them — see the class doc. */
|
|
13
|
+
const API_PATH = 'ApiPath';
|
|
14
|
+
const ENDPOINT = 'Endpoint';
|
|
15
|
+
const MASK_LOG = 'MaskLog';
|
|
16
|
+
const MCP_TOOL = 'WpMcpTool';
|
|
17
|
+
const MCP_AUTH = 'WpMcpAuthJwt';
|
|
18
|
+
const AUTH_PREFIX = 'WpAuth';
|
|
19
|
+
/** The MCP tool hints a renderer surfaces to an agent. */
|
|
20
|
+
const TOOL_HINTS = ['readOnly', 'destructive', 'idempotent', 'openWorld'];
|
|
21
|
+
/**
|
|
22
|
+
* ONE contract file -> ONE {@link ApiDocModel}. The single extraction pass both the OpenAPI documents
|
|
23
|
+
* and the MCP tool list (#982) are rendered from.
|
|
24
|
+
*
|
|
25
|
+
* ## Why the compiler API at all
|
|
26
|
+
*
|
|
27
|
+
* A DTO field's TYPE IS ERASED AT RUNTIME. Reflection can see that `save` takes one argument; it
|
|
28
|
+
* cannot see that the argument has a `deliveryWindow` that is a discriminated union of two shapes,
|
|
29
|
+
* one of which carries an ISO timestamp. Those are precisely the shapes a partner-grade document is
|
|
30
|
+
* made of, so the only place they exist is the source, and the only honest way to read the source is
|
|
31
|
+
* the compiler.
|
|
32
|
+
*
|
|
33
|
+
* ## Why it imports NO webpieces package
|
|
34
|
+
*
|
|
35
|
+
* It depends on `typescript` and nothing else, so it can be pointed at ANY upstream project's
|
|
36
|
+
* contract — which is the whole point of shipping it as a package rather than as a script in this
|
|
37
|
+
* repo. Decorators are therefore matched BY NAME on the syntax, never by importing the real
|
|
38
|
+
* decorator. One `@webpieces/core-util` import here would make the package unusable by anybody whose
|
|
39
|
+
* webpieces version differs from ours, which is everybody.
|
|
40
|
+
*
|
|
41
|
+
* ## What it does NOT do
|
|
42
|
+
*
|
|
43
|
+
* It emits nothing. No OpenAPI, no MCP tool definitions, no file — that is #982, and keeping the
|
|
44
|
+
* render out means the two renderers cannot drift apart about what the contract SAYS.
|
|
45
|
+
*/
|
|
46
|
+
class ApiDocExtractor {
|
|
47
|
+
/**
|
|
48
|
+
* Extract the contract in `entryFile`.
|
|
49
|
+
*
|
|
50
|
+
* @param entryFile absolute path to the `.ts` file holding the `@ApiPath` class.
|
|
51
|
+
* @param compilerOptions handed straight to `ts.createProgram`; the caller owns them because
|
|
52
|
+
* only the caller knows its own `paths` / `lib` setup.
|
|
53
|
+
* @throws ApiDocExtractionError when the file holds no `@ApiPath` class, or when something that
|
|
54
|
+
* must be exact (a path constant, a numeric bound) cannot be established.
|
|
55
|
+
*/
|
|
56
|
+
extractFile(entryFile, compilerOptions = {}) {
|
|
57
|
+
const program = ts.createProgram([entryFile], compilerOptions);
|
|
58
|
+
const source = program.getSourceFile(entryFile);
|
|
59
|
+
if (source === undefined) {
|
|
60
|
+
throw new ApiDocExtractionError_1.ApiDocExtractionError('entry file is not part of the program', entryFile, 'Pass an absolute path to a .ts file that exists.');
|
|
61
|
+
}
|
|
62
|
+
return this.extract(program, source);
|
|
63
|
+
}
|
|
64
|
+
/** The same extraction against a program the caller already built. */
|
|
65
|
+
extract(program, source) {
|
|
66
|
+
const checker = program.getTypeChecker();
|
|
67
|
+
const folder = new ConstantFolder_1.ConstantFolder(checker);
|
|
68
|
+
const resolver = new TypeResolver_1.TypeResolver(checker);
|
|
69
|
+
const contract = this.findContract(source);
|
|
70
|
+
const pathDecorator = ApiDocExtractor.decoratorCall(contract, API_PATH);
|
|
71
|
+
const basePathArgument = pathDecorator.arguments[0];
|
|
72
|
+
const basePath = basePathArgument === undefined
|
|
73
|
+
? ''
|
|
74
|
+
: folder.foldString(basePathArgument, '@ApiPath argument');
|
|
75
|
+
const endpoints = [];
|
|
76
|
+
for (const member of contract.members) {
|
|
77
|
+
const endpoint = this.endpointOf(member, folder, resolver);
|
|
78
|
+
if (endpoint !== undefined) {
|
|
79
|
+
endpoints.push(endpoint);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return new ApiDocModel_1.ApiDocModel(contract.name?.text ?? '<anonymous>', basePath, JsDoc_1.JsDoc.read(contract).description, endpoints, resolver.collectedTypes(), resolver.collectedUnmapped());
|
|
83
|
+
}
|
|
84
|
+
/** The one `@ApiPath` class in the file. Zero is a hard failure; the FIRST wins if there are two. */
|
|
85
|
+
findContract(source) {
|
|
86
|
+
for (const statement of source.statements) {
|
|
87
|
+
if (ts.isClassDeclaration(statement) &&
|
|
88
|
+
ApiDocExtractor.decoratorCall(statement, API_PATH) !== undefined) {
|
|
89
|
+
return statement;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new ApiDocExtractionError_1.ApiDocExtractionError('no @ApiPath class in this file', source.fileName, 'Point the extractor at the contract file — the one whose class carries @ApiPath.');
|
|
93
|
+
}
|
|
94
|
+
/** One `@Endpoint` method. Members without the decorator are not part of the contract. */
|
|
95
|
+
endpointOf(member, folder, resolver) {
|
|
96
|
+
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const call = ApiDocExtractor.decoratorCall(member, ENDPOINT);
|
|
100
|
+
if (call === undefined) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const methodName = member.name.text;
|
|
104
|
+
const pathArgument = call.arguments[0];
|
|
105
|
+
if (pathArgument === undefined) {
|
|
106
|
+
throw new ApiDocExtractionError_1.ApiDocExtractionError(`@Endpoint on '${methodName}' declares no path`, SourceLocation_1.SourceLocation.of(call), "Give it a path: @Endpoint('/thing', 'rpc').");
|
|
107
|
+
}
|
|
108
|
+
const path = folder.foldString(pathArgument, `@Endpoint path on '${methodName}'`);
|
|
109
|
+
const kindArgument = call.arguments[1];
|
|
110
|
+
if (kindArgument === undefined) {
|
|
111
|
+
throw new ApiDocExtractionError_1.ApiDocExtractionError(`@Endpoint on '${methodName}' declares no kind`, SourceLocation_1.SourceLocation.of(call), "State what triggers it: 'rpc', 'cloudtasks', 'cron' or 'external'.");
|
|
112
|
+
}
|
|
113
|
+
const kind = folder.foldString(kindArgument, `@Endpoint kind on '${methodName}'`);
|
|
114
|
+
const options = call.arguments[2];
|
|
115
|
+
const literal = options !== undefined && ts.isObjectLiteralExpression(options) ? options : undefined;
|
|
116
|
+
const doc = JsDoc_1.JsDoc.read(member);
|
|
117
|
+
return new ApiDocModel_1.DocumentedEndpoint(methodName, path, kind, ApiDocExtractor.booleanProperty(literal, 'hidden'), new ApiDocModel_1.DocumentedEndpointOptions(ApiDocExtractor.booleanProperty(literal, 'formPost'), ApiDocExtractor.stringProperty(literal, 'calledBy', folder), ApiDocExtractor.stringProperty(literal, 'callerKind', folder)), ApiDocExtractor.authOf(member), ApiDocExtractor.mcpToolOf(member, folder), ApiDocExtractor.decoratorCall(member, MCP_AUTH)?.arguments[0]?.getText(), ApiDocExtractor.maskLogOf(member), doc.description, doc.mcp, this.requestOf(member, methodName, resolver), this.responseOf(member, methodName, resolver));
|
|
118
|
+
}
|
|
119
|
+
/** The FIRST parameter's declared type. An endpoint with no parameter has no request document. */
|
|
120
|
+
requestOf(member, methodName, resolver) {
|
|
121
|
+
const parameter = member.parameters[0];
|
|
122
|
+
if (parameter?.type === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return resolver.resolve(parameter.type, `${methodName}.request`);
|
|
126
|
+
}
|
|
127
|
+
/** The declared return type, unwrapped from `Promise<...>` by the resolver. */
|
|
128
|
+
responseOf(member, methodName, resolver) {
|
|
129
|
+
if (member.type === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return resolver.resolve(member.type, `${methodName}.response`);
|
|
133
|
+
}
|
|
134
|
+
/** `@WpAuthPublic()`, `@WpAuthJwt({...})`, … — recorded verbatim; this package rules on nothing. */
|
|
135
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
136
|
+
static authOf(member) {
|
|
137
|
+
const decorators = ts.canHaveDecorators(member) ? (ts.getDecorators(member) ?? []) : [];
|
|
138
|
+
for (const decorator of decorators) {
|
|
139
|
+
const call = decorator.expression;
|
|
140
|
+
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const name = call.expression.text;
|
|
144
|
+
if (name.startsWith(AUTH_PREFIX) && name !== MCP_AUTH) {
|
|
145
|
+
return new ApiDocModel_1.DocumentedAuth(name, call.arguments[0]?.getText());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/** `@WpMcpTool({ name, readOnly, ... })` — the name plus whichever hints were declared. */
|
|
151
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
152
|
+
static mcpToolOf(member, folder) {
|
|
153
|
+
const call = ApiDocExtractor.decoratorCall(member, MCP_TOOL);
|
|
154
|
+
const argument = call?.arguments[0];
|
|
155
|
+
if (argument === undefined || !ts.isObjectLiteralExpression(argument)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const hints = new Map();
|
|
159
|
+
for (const hint of TOOL_HINTS) {
|
|
160
|
+
const value = ApiDocExtractor.findProperty(argument, hint);
|
|
161
|
+
if (value !== undefined) {
|
|
162
|
+
hints.set(hint, value.kind === ts.SyntaxKind.TrueKeyword);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return new ApiDocModel_1.DocumentedMcpTool(ApiDocExtractor.stringProperty(argument, 'name', folder) ?? '', hints);
|
|
166
|
+
}
|
|
167
|
+
/** `@MaskLog({ refreshToken: 'full' })` -> field name -> mask mode. */
|
|
168
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
169
|
+
static maskLogOf(member) {
|
|
170
|
+
const fields = new Map();
|
|
171
|
+
const argument = ApiDocExtractor.decoratorCall(member, MASK_LOG)?.arguments[0];
|
|
172
|
+
if (argument === undefined || !ts.isObjectLiteralExpression(argument)) {
|
|
173
|
+
return fields;
|
|
174
|
+
}
|
|
175
|
+
for (const property of argument.properties) {
|
|
176
|
+
if (ts.isPropertyAssignment(property) &&
|
|
177
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
178
|
+
ts.isStringLiteralLike(property.initializer)) {
|
|
179
|
+
fields.set(property.name.text, property.initializer.text);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return fields;
|
|
183
|
+
}
|
|
184
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
185
|
+
static booleanProperty(literal, name) {
|
|
186
|
+
const value = literal === undefined ? undefined : ApiDocExtractor.findProperty(literal, name);
|
|
187
|
+
return value?.kind === ts.SyntaxKind.TrueKeyword;
|
|
188
|
+
}
|
|
189
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
190
|
+
static stringProperty(literal, name, folder) {
|
|
191
|
+
const value = literal === undefined ? undefined : ApiDocExtractor.findProperty(literal, name);
|
|
192
|
+
return value === undefined ? undefined : folder.tryFoldString(value);
|
|
193
|
+
}
|
|
194
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
195
|
+
static findProperty(literal, name) {
|
|
196
|
+
for (const property of literal.properties) {
|
|
197
|
+
if (ts.isPropertyAssignment(property) &&
|
|
198
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
199
|
+
property.name.text === name) {
|
|
200
|
+
return property.initializer;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
206
|
+
static decoratorCall(node, decoratorName) {
|
|
207
|
+
const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
208
|
+
for (const decorator of decorators) {
|
|
209
|
+
const call = decorator.expression;
|
|
210
|
+
if (ts.isCallExpression(call) &&
|
|
211
|
+
ts.isIdentifier(call.expression) &&
|
|
212
|
+
call.expression.text === decoratorName) {
|
|
213
|
+
return call;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
exports.ApiDocExtractor = ApiDocExtractor;
|
|
220
|
+
//# sourceMappingURL=ApiDocExtractor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiDocExtractor.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/extract/ApiDocExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["import * as ts from 'typescript';\nimport {\n ApiDocModel,\n DocumentedAuth,\n DocumentedEndpoint,\n DocumentedEndpointOptions,\n DocumentedMcpTool,\n} from '../model/ApiDocModel';\nimport { TypeRef } from '../model/TypeRef';\nimport { ApiDocExtractionError } from './ApiDocExtractionError';\nimport { ConstantFolder } from './ConstantFolder';\nimport { JsDoc } from './JsDoc';\nimport { SourceLocation } from './SourceLocation';\nimport { TypeResolver } from './TypeResolver';\n\n/** The decorator names this extractor reads, by NAME. It imports none of them — see the class doc. */\nconst API_PATH = 'ApiPath';\nconst ENDPOINT = 'Endpoint';\nconst MASK_LOG = 'MaskLog';\nconst MCP_TOOL = 'WpMcpTool';\nconst MCP_AUTH = 'WpMcpAuthJwt';\nconst AUTH_PREFIX = 'WpAuth';\n\n/** The MCP tool hints a renderer surfaces to an agent. */\nconst TOOL_HINTS = ['readOnly', 'destructive', 'idempotent', 'openWorld'];\n\n/**\n * ONE contract file -> ONE {@link ApiDocModel}. The single extraction pass both the OpenAPI documents\n * and the MCP tool list (#982) are rendered from.\n *\n * ## Why the compiler API at all\n *\n * A DTO field's TYPE IS ERASED AT RUNTIME. Reflection can see that `save` takes one argument; it\n * cannot see that the argument has a `deliveryWindow` that is a discriminated union of two shapes,\n * one of which carries an ISO timestamp. Those are precisely the shapes a partner-grade document is\n * made of, so the only place they exist is the source, and the only honest way to read the source is\n * the compiler.\n *\n * ## Why it imports NO webpieces package\n *\n * It depends on `typescript` and nothing else, so it can be pointed at ANY upstream project's\n * contract — which is the whole point of shipping it as a package rather than as a script in this\n * repo. Decorators are therefore matched BY NAME on the syntax, never by importing the real\n * decorator. One `@webpieces/core-util` import here would make the package unusable by anybody whose\n * webpieces version differs from ours, which is everybody.\n *\n * ## What it does NOT do\n *\n * It emits nothing. No OpenAPI, no MCP tool definitions, no file — that is #982, and keeping the\n * render out means the two renderers cannot drift apart about what the contract SAYS.\n */\nexport class ApiDocExtractor {\n /**\n * Extract the contract in `entryFile`.\n *\n * @param entryFile absolute path to the `.ts` file holding the `@ApiPath` class.\n * @param compilerOptions handed straight to `ts.createProgram`; the caller owns them because\n * only the caller knows its own `paths` / `lib` setup.\n * @throws ApiDocExtractionError when the file holds no `@ApiPath` class, or when something that\n * must be exact (a path constant, a numeric bound) cannot be established.\n */\n extractFile(entryFile: string, compilerOptions: ts.CompilerOptions = {}): ApiDocModel {\n const program = ts.createProgram([entryFile], compilerOptions);\n const source = program.getSourceFile(entryFile);\n if (source === undefined) {\n throw new ApiDocExtractionError(\n 'entry file is not part of the program',\n entryFile,\n 'Pass an absolute path to a .ts file that exists.',\n );\n }\n return this.extract(program, source);\n }\n\n /** The same extraction against a program the caller already built. */\n extract(program: ts.Program, source: ts.SourceFile): ApiDocModel {\n const checker = program.getTypeChecker();\n const folder = new ConstantFolder(checker);\n const resolver = new TypeResolver(checker);\n\n const contract = this.findContract(source);\n const pathDecorator = ApiDocExtractor.decoratorCall(contract, API_PATH)!;\n const basePathArgument = pathDecorator.arguments[0];\n const basePath =\n basePathArgument === undefined\n ? ''\n : folder.foldString(basePathArgument, '@ApiPath argument');\n\n const endpoints: DocumentedEndpoint[] = [];\n for (const member of contract.members) {\n const endpoint = this.endpointOf(member, folder, resolver);\n if (endpoint !== undefined) {\n endpoints.push(endpoint);\n }\n }\n\n return new ApiDocModel(\n contract.name?.text ?? '<anonymous>',\n basePath,\n JsDoc.read(contract).description,\n endpoints,\n resolver.collectedTypes(),\n resolver.collectedUnmapped(),\n );\n }\n\n /** The one `@ApiPath` class in the file. Zero is a hard failure; the FIRST wins if there are two. */\n private findContract(source: ts.SourceFile): ts.ClassDeclaration {\n for (const statement of source.statements) {\n if (\n ts.isClassDeclaration(statement) &&\n ApiDocExtractor.decoratorCall(statement, API_PATH) !== undefined\n ) {\n return statement;\n }\n }\n throw new ApiDocExtractionError(\n 'no @ApiPath class in this file',\n source.fileName,\n 'Point the extractor at the contract file — the one whose class carries @ApiPath.',\n );\n }\n\n /** One `@Endpoint` method. Members without the decorator are not part of the contract. */\n private endpointOf(\n member: ts.ClassElement,\n folder: ConstantFolder,\n resolver: TypeResolver,\n ): DocumentedEndpoint | undefined {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) {\n return undefined;\n }\n const call = ApiDocExtractor.decoratorCall(member, ENDPOINT);\n if (call === undefined) {\n return undefined;\n }\n const methodName = member.name.text;\n\n const pathArgument = call.arguments[0];\n if (pathArgument === undefined) {\n throw new ApiDocExtractionError(\n `@Endpoint on '${methodName}' declares no path`,\n SourceLocation.of(call),\n \"Give it a path: @Endpoint('/thing', 'rpc').\",\n );\n }\n const path = folder.foldString(pathArgument, `@Endpoint path on '${methodName}'`);\n\n const kindArgument = call.arguments[1];\n if (kindArgument === undefined) {\n throw new ApiDocExtractionError(\n `@Endpoint on '${methodName}' declares no kind`,\n SourceLocation.of(call),\n \"State what triggers it: 'rpc', 'cloudtasks', 'cron' or 'external'.\",\n );\n }\n const kind = folder.foldString(kindArgument, `@Endpoint kind on '${methodName}'`);\n\n const options = call.arguments[2];\n const literal =\n options !== undefined && ts.isObjectLiteralExpression(options) ? options : undefined;\n\n const doc = JsDoc.read(member);\n return new DocumentedEndpoint(\n methodName,\n path,\n kind,\n ApiDocExtractor.booleanProperty(literal, 'hidden'),\n new DocumentedEndpointOptions(\n ApiDocExtractor.booleanProperty(literal, 'formPost'),\n ApiDocExtractor.stringProperty(literal, 'calledBy', folder),\n ApiDocExtractor.stringProperty(literal, 'callerKind', folder),\n ),\n ApiDocExtractor.authOf(member),\n ApiDocExtractor.mcpToolOf(member, folder),\n ApiDocExtractor.decoratorCall(member, MCP_AUTH)?.arguments[0]?.getText(),\n ApiDocExtractor.maskLogOf(member),\n doc.description,\n doc.mcp,\n this.requestOf(member, methodName, resolver),\n this.responseOf(member, methodName, resolver),\n );\n }\n\n /** The FIRST parameter's declared type. An endpoint with no parameter has no request document. */\n private requestOf(\n member: ts.MethodDeclaration,\n methodName: string,\n resolver: TypeResolver,\n ): TypeRef | undefined {\n const parameter = member.parameters[0];\n if (parameter?.type === undefined) {\n return undefined;\n }\n return resolver.resolve(parameter.type, `${methodName}.request`);\n }\n\n /** The declared return type, unwrapped from `Promise<...>` by the resolver. */\n private responseOf(\n member: ts.MethodDeclaration,\n methodName: string,\n resolver: TypeResolver,\n ): TypeRef | undefined {\n if (member.type === undefined) {\n return undefined;\n }\n return resolver.resolve(member.type, `${methodName}.response`);\n }\n\n /** `@WpAuthPublic()`, `@WpAuthJwt({...})`, … — recorded verbatim; this package rules on nothing. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static authOf(member: ts.Node): DocumentedAuth | undefined {\n const decorators = ts.canHaveDecorators(member) ? (ts.getDecorators(member) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) {\n continue;\n }\n const name = call.expression.text;\n if (name.startsWith(AUTH_PREFIX) && name !== MCP_AUTH) {\n return new DocumentedAuth(name, call.arguments[0]?.getText());\n }\n }\n return undefined;\n }\n\n /** `@WpMcpTool({ name, readOnly, ... })` — the name plus whichever hints were declared. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static mcpToolOf(\n member: ts.Node,\n folder: ConstantFolder,\n ): DocumentedMcpTool | undefined {\n const call = ApiDocExtractor.decoratorCall(member, MCP_TOOL);\n const argument = call?.arguments[0];\n if (argument === undefined || !ts.isObjectLiteralExpression(argument)) {\n return undefined;\n }\n const hints = new Map<string, boolean>();\n for (const hint of TOOL_HINTS) {\n const value = ApiDocExtractor.findProperty(argument, hint);\n if (value !== undefined) {\n hints.set(hint, value.kind === ts.SyntaxKind.TrueKeyword);\n }\n }\n return new DocumentedMcpTool(\n ApiDocExtractor.stringProperty(argument, 'name', folder) ?? '',\n hints,\n );\n }\n\n /** `@MaskLog({ refreshToken: 'full' })` -> field name -> mask mode. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static maskLogOf(member: ts.Node): ReadonlyMap<string, string> {\n const fields = new Map<string, string>();\n const argument = ApiDocExtractor.decoratorCall(member, MASK_LOG)?.arguments[0];\n if (argument === undefined || !ts.isObjectLiteralExpression(argument)) {\n return fields;\n }\n for (const property of argument.properties) {\n if (\n ts.isPropertyAssignment(property) &&\n (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&\n ts.isStringLiteralLike(property.initializer)\n ) {\n fields.set(property.name.text, property.initializer.text);\n }\n }\n return fields;\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static booleanProperty(\n literal: ts.ObjectLiteralExpression | undefined,\n name: string,\n ): boolean {\n const value =\n literal === undefined ? undefined : ApiDocExtractor.findProperty(literal, name);\n return value?.kind === ts.SyntaxKind.TrueKeyword;\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static stringProperty(\n literal: ts.ObjectLiteralExpression | undefined,\n name: string,\n folder: ConstantFolder,\n ): string | undefined {\n const value =\n literal === undefined ? undefined : ApiDocExtractor.findProperty(literal, name);\n return value === undefined ? undefined : folder.tryFoldString(value);\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static findProperty(\n literal: ts.ObjectLiteralExpression,\n name: string,\n ): ts.Expression | undefined {\n for (const property of literal.properties) {\n if (\n ts.isPropertyAssignment(property) &&\n (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&\n property.name.text === name\n ) {\n return property.initializer;\n }\n }\n return undefined;\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static decoratorCall(\n node: ts.Node,\n decoratorName: string,\n ): ts.CallExpression | undefined {\n const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (\n ts.isCallExpression(call) &&\n ts.isIdentifier(call.expression) &&\n call.expression.text === decoratorName\n ) {\n return call;\n }\n }\n return undefined;\n }\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Decorator arguments are constants as often as they are literals — `@Endpoint(SAVE_PATH, 'rpc')`,
|
|
4
|
+
* `@ApiPath(PATHS.SAVE)` — and a document that printed `SAVE_PATH` as the path would be worse than
|
|
5
|
+
* no document.
|
|
6
|
+
*
|
|
7
|
+
* So the folder resolves them, and where it CANNOT it FAILS. There is deliberately no fallback to
|
|
8
|
+
* the source text and no "best effort" path: a partner-grade document that is quietly wrong about a
|
|
9
|
+
* URL is the one failure mode nobody catches by reading it.
|
|
10
|
+
*
|
|
11
|
+
* It asks the CHECKER first, which is what makes this robust across re-exports, imports and
|
|
12
|
+
* `as const` objects: a `const` initialised with a string has a string-LITERAL type, and the checker
|
|
13
|
+
* has already done the resolution. The syntactic walk below it exists only for the shapes the
|
|
14
|
+
* checker widens.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ConstantFolder {
|
|
17
|
+
private readonly checker;
|
|
18
|
+
constructor(checker: ts.TypeChecker);
|
|
19
|
+
/**
|
|
20
|
+
* The string this expression denotes.
|
|
21
|
+
*
|
|
22
|
+
* @throws ApiDocExtractionError when it cannot be established — never a guess.
|
|
23
|
+
*/
|
|
24
|
+
foldString(expression: ts.Expression, what: string): string;
|
|
25
|
+
/** The string this expression denotes, or undefined. No throw — for optional arguments. */
|
|
26
|
+
tryFoldString(expression: ts.Expression): string | undefined;
|
|
27
|
+
private tryFold;
|
|
28
|
+
/** Follow an identifier / `Obj.MEMBER` to its declaration's initializer and fold that. */
|
|
29
|
+
private foldThroughSymbol;
|
|
30
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConstantFolder = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
6
|
+
const ApiDocExtractionError_1 = require("./ApiDocExtractionError");
|
|
7
|
+
const SourceLocation_1 = require("./SourceLocation");
|
|
8
|
+
/**
|
|
9
|
+
* Decorator arguments are constants as often as they are literals — `@Endpoint(SAVE_PATH, 'rpc')`,
|
|
10
|
+
* `@ApiPath(PATHS.SAVE)` — and a document that printed `SAVE_PATH` as the path would be worse than
|
|
11
|
+
* no document.
|
|
12
|
+
*
|
|
13
|
+
* So the folder resolves them, and where it CANNOT it FAILS. There is deliberately no fallback to
|
|
14
|
+
* the source text and no "best effort" path: a partner-grade document that is quietly wrong about a
|
|
15
|
+
* URL is the one failure mode nobody catches by reading it.
|
|
16
|
+
*
|
|
17
|
+
* It asks the CHECKER first, which is what makes this robust across re-exports, imports and
|
|
18
|
+
* `as const` objects: a `const` initialised with a string has a string-LITERAL type, and the checker
|
|
19
|
+
* has already done the resolution. The syntactic walk below it exists only for the shapes the
|
|
20
|
+
* checker widens.
|
|
21
|
+
*/
|
|
22
|
+
class ConstantFolder {
|
|
23
|
+
checker;
|
|
24
|
+
constructor(checker) {
|
|
25
|
+
this.checker = checker;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The string this expression denotes.
|
|
29
|
+
*
|
|
30
|
+
* @throws ApiDocExtractionError when it cannot be established — never a guess.
|
|
31
|
+
*/
|
|
32
|
+
foldString(expression, what) {
|
|
33
|
+
const folded = this.tryFold(expression, new Set());
|
|
34
|
+
if (folded === undefined) {
|
|
35
|
+
throw new ApiDocExtractionError_1.ApiDocExtractionError(`${what} is not a foldable constant: '${expression.getText()}'`, SourceLocation_1.SourceLocation.of(expression), 'Declare it as a `const` initialised with a string literal (or write the literal inline). ' +
|
|
36
|
+
'A value only known at runtime cannot appear in a published document.');
|
|
37
|
+
}
|
|
38
|
+
return folded;
|
|
39
|
+
}
|
|
40
|
+
/** The string this expression denotes, or undefined. No throw — for optional arguments. */
|
|
41
|
+
tryFoldString(expression) {
|
|
42
|
+
return this.tryFold(expression, new Set());
|
|
43
|
+
}
|
|
44
|
+
tryFold(expression, seen) {
|
|
45
|
+
if (seen.has(expression)) {
|
|
46
|
+
return undefined; // a const initialised from itself; not our problem to diagnose
|
|
47
|
+
}
|
|
48
|
+
seen.add(expression);
|
|
49
|
+
if (ts.isStringLiteralLike(expression)) {
|
|
50
|
+
return expression.text;
|
|
51
|
+
}
|
|
52
|
+
// The checker's answer, which already resolved imports, re-exports and `as const`.
|
|
53
|
+
const type = this.checker.getTypeAtLocation(expression);
|
|
54
|
+
if (type.isStringLiteral()) {
|
|
55
|
+
return type.value;
|
|
56
|
+
}
|
|
57
|
+
if (ts.isTemplateExpression(expression)) {
|
|
58
|
+
let out = expression.head.text;
|
|
59
|
+
for (const span of expression.templateSpans) {
|
|
60
|
+
const part = this.tryFold(span.expression, seen);
|
|
61
|
+
if (part === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
out += part + span.literal.text;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
if (ts.isBinaryExpression(expression) &&
|
|
69
|
+
expression.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
70
|
+
const left = this.tryFold(expression.left, seen);
|
|
71
|
+
const right = this.tryFold(expression.right, seen);
|
|
72
|
+
return left === undefined || right === undefined ? undefined : left + right;
|
|
73
|
+
}
|
|
74
|
+
if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression)) {
|
|
75
|
+
return this.foldThroughSymbol(expression, seen);
|
|
76
|
+
}
|
|
77
|
+
if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)) {
|
|
78
|
+
return this.tryFold(expression.expression, seen);
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Follow an identifier / `Obj.MEMBER` to its declaration's initializer and fold that. */
|
|
83
|
+
foldThroughSymbol(expression, seen) {
|
|
84
|
+
const symbol = this.checker.getSymbolAtLocation(expression);
|
|
85
|
+
const resolved = symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0
|
|
86
|
+
? this.checker.getAliasedSymbol(symbol)
|
|
87
|
+
: symbol;
|
|
88
|
+
for (const declaration of resolved?.declarations ?? []) {
|
|
89
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
90
|
+
return this.tryFold(declaration.initializer, seen);
|
|
91
|
+
}
|
|
92
|
+
if (ts.isPropertyAssignment(declaration) && ts.isExpression(declaration.initializer)) {
|
|
93
|
+
return this.tryFold(declaration.initializer, seen);
|
|
94
|
+
}
|
|
95
|
+
if (ts.isPropertyDeclaration(declaration) && declaration.initializer) {
|
|
96
|
+
return this.tryFold(declaration.initializer, seen);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.ConstantFolder = ConstantFolder;
|
|
103
|
+
//# sourceMappingURL=ConstantFolder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConstantFolder.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/extract/ConstantFolder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as ts from 'typescript';\nimport { ApiDocExtractionError } from './ApiDocExtractionError';\nimport { SourceLocation } from './SourceLocation';\n\n/**\n * Decorator arguments are constants as often as they are literals — `@Endpoint(SAVE_PATH, 'rpc')`,\n * `@ApiPath(PATHS.SAVE)` — and a document that printed `SAVE_PATH` as the path would be worse than\n * no document.\n *\n * So the folder resolves them, and where it CANNOT it FAILS. There is deliberately no fallback to\n * the source text and no \"best effort\" path: a partner-grade document that is quietly wrong about a\n * URL is the one failure mode nobody catches by reading it.\n *\n * It asks the CHECKER first, which is what makes this robust across re-exports, imports and\n * `as const` objects: a `const` initialised with a string has a string-LITERAL type, and the checker\n * has already done the resolution. The syntactic walk below it exists only for the shapes the\n * checker widens.\n */\nexport class ConstantFolder {\n constructor(private readonly checker: ts.TypeChecker) {}\n\n /**\n * The string this expression denotes.\n *\n * @throws ApiDocExtractionError when it cannot be established — never a guess.\n */\n foldString(expression: ts.Expression, what: string): string {\n const folded = this.tryFold(expression, new Set<ts.Node>());\n if (folded === undefined) {\n throw new ApiDocExtractionError(\n `${what} is not a foldable constant: '${expression.getText()}'`,\n SourceLocation.of(expression),\n 'Declare it as a `const` initialised with a string literal (or write the literal inline). ' +\n 'A value only known at runtime cannot appear in a published document.',\n );\n }\n return folded;\n }\n\n /** The string this expression denotes, or undefined. No throw — for optional arguments. */\n tryFoldString(expression: ts.Expression): string | undefined {\n return this.tryFold(expression, new Set<ts.Node>());\n }\n\n private tryFold(expression: ts.Expression, seen: Set<ts.Node>): string | undefined {\n if (seen.has(expression)) {\n return undefined; // a const initialised from itself; not our problem to diagnose\n }\n seen.add(expression);\n\n if (ts.isStringLiteralLike(expression)) {\n return expression.text;\n }\n\n // The checker's answer, which already resolved imports, re-exports and `as const`.\n const type = this.checker.getTypeAtLocation(expression);\n if (type.isStringLiteral()) {\n return type.value;\n }\n\n if (ts.isTemplateExpression(expression)) {\n let out = expression.head.text;\n for (const span of expression.templateSpans) {\n const part = this.tryFold(span.expression, seen);\n if (part === undefined) {\n return undefined;\n }\n out += part + span.literal.text;\n }\n return out;\n }\n\n if (\n ts.isBinaryExpression(expression) &&\n expression.operatorToken.kind === ts.SyntaxKind.PlusToken\n ) {\n const left = this.tryFold(expression.left, seen);\n const right = this.tryFold(expression.right, seen);\n return left === undefined || right === undefined ? undefined : left + right;\n }\n\n if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression)) {\n return this.foldThroughSymbol(expression, seen);\n }\n\n if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)) {\n return this.tryFold(expression.expression, seen);\n }\n\n return undefined;\n }\n\n /** Follow an identifier / `Obj.MEMBER` to its declaration's initializer and fold that. */\n private foldThroughSymbol(expression: ts.Expression, seen: Set<ts.Node>): string | undefined {\n const symbol = this.checker.getSymbolAtLocation(expression);\n const resolved =\n symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0\n ? this.checker.getAliasedSymbol(symbol)\n : symbol;\n for (const declaration of resolved?.declarations ?? []) {\n if (ts.isVariableDeclaration(declaration) && declaration.initializer) {\n return this.tryFold(declaration.initializer, seen);\n }\n if (ts.isPropertyAssignment(declaration) && ts.isExpression(declaration.initializer)) {\n return this.tryFold(declaration.initializer, seen);\n }\n if (ts.isPropertyDeclaration(declaration) && declaration.initializer) {\n return this.tryFold(declaration.initializer, seen);\n }\n }\n return undefined;\n }\n}\n"]}
|