@webpieces/nx-webpieces-rules 0.4.809 → 0.4.811
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/executors.json +10 -0
- package/package.json +7 -7
- package/src/executors/docs-generate/executor.d.ts +47 -0
- package/src/executors/docs-generate/executor.js +102 -0
- package/src/executors/docs-generate/executor.js.map +1 -0
- package/src/executors/docs-generate/schema.json +21 -0
- package/src/executors/openapi-generate/executor.d.ts +46 -0
- package/src/executors/openapi-generate/executor.js +94 -0
- package/src/executors/openapi-generate/executor.js.map +1 -0
- package/src/executors/openapi-generate/schema.json +18 -0
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +22 -14
- package/src/lib/api-usage/api-doc-rules-scan.js +32 -21
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +14 -1
- package/src/lib/api-usage/api-doc-rules-verdicts.js +19 -4
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules.d.ts +42 -5
- package/src/lib/api-usage/api-doc-rules.js +65 -9
- package/src/lib/api-usage/api-doc-rules.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +5 -5
- package/src/lib/api-usage/api-scanner.js +2 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/generated-docs/generator-target.d.ts +54 -0
- package/src/lib/generated-docs/generator-target.js +145 -0
- package/src/lib/generated-docs/generator-target.js.map +1 -0
package/executors.json
CHANGED
|
@@ -150,6 +150,16 @@
|
|
|
150
150
|
"schema": "./src/executors/di-graph-generate/schema.json",
|
|
151
151
|
"description": "Per-project: generate the Inversify DI dependency DAG into design.json + design.md at the project root"
|
|
152
152
|
},
|
|
153
|
+
"openapi-generate": {
|
|
154
|
+
"implementation": "./src/executors/openapi-generate/executor",
|
|
155
|
+
"schema": "./src/executors/openapi-generate/schema.json",
|
|
156
|
+
"description": "Per-project: render the contracts to OpenAPI + mcp-tools.json into the build outputPath, with the consumer's own @webpieces/openapi-generator"
|
|
157
|
+
},
|
|
158
|
+
"docs-generate": {
|
|
159
|
+
"implementation": "./src/executors/docs-generate/executor",
|
|
160
|
+
"schema": "./src/executors/docs-generate/schema.json",
|
|
161
|
+
"description": "Per-project: render the static API reference site from a generated document into the build outputPath, with the consumer's own @webpieces/docs-site"
|
|
162
|
+
},
|
|
153
163
|
"validate-nx-wiring": {
|
|
154
164
|
"implementation": "./src/executors/validate-nx-wiring/executor",
|
|
155
165
|
"schema": "./src/executors/validate-nx-wiring/schema.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.811",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
22
|
-
"@webpieces/api-doc-model": "0.4.
|
|
23
|
-
"@webpieces/code-rules": "0.4.
|
|
24
|
-
"@webpieces/eslint-rules": "0.4.
|
|
25
|
-
"@webpieces/pr-gate": "0.4.
|
|
26
|
-
"@webpieces/rules-config": "0.4.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.811",
|
|
22
|
+
"@webpieces/api-doc-model": "0.4.811",
|
|
23
|
+
"@webpieces/code-rules": "0.4.811",
|
|
24
|
+
"@webpieces/eslint-rules": "0.4.811",
|
|
25
|
+
"@webpieces/pr-gate": "0.4.811",
|
|
26
|
+
"@webpieces/rules-config": "0.4.811",
|
|
27
27
|
"madge": "8.0.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docs-generate Executor
|
|
3
|
+
*
|
|
4
|
+
* Renders the static API reference site from a document `openapi-generate` already wrote into the
|
|
5
|
+
* project's build `outputPath`, and writes the site into `<outputPath>/<siteDir>` — build output,
|
|
6
|
+
* published with the package, never committed.
|
|
7
|
+
*
|
|
8
|
+
* Usage (project.json):
|
|
9
|
+
*
|
|
10
|
+
* "docs-generate": {
|
|
11
|
+
* "executor": "@webpieces/nx-webpieces-rules:docs-generate",
|
|
12
|
+
* "dependsOn": ["openapi-generate"],
|
|
13
|
+
* "cache": true,
|
|
14
|
+
* "inputs": ["default", "^default"],
|
|
15
|
+
* "outputs": ["{workspaceRoot}/dist/<project>/docs-site"],
|
|
16
|
+
* "options": { "document": "public-openapi.json", "siteDir": "docs-site", "prose": "<project>/docs" }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* `document` should be the PARTNER-facing document: a site built from `full-private-openapi.json`
|
|
20
|
+
* publishes exactly the operations somebody decided not to publish. `prose` is optional because a site
|
|
21
|
+
* with no prose pages is a legal site; `document` and `siteDir` are required and have no defaults.
|
|
22
|
+
*
|
|
23
|
+
* The renderer is the CONSUMER's `@webpieces/docs-site`, resolved from its node_modules and
|
|
24
|
+
* version-checked; this plugin bundles no copy of it.
|
|
25
|
+
*/
|
|
26
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
27
|
+
import { ConsumerBinResolver, GeneratorRunner } from '@webpieces/pr-gate';
|
|
28
|
+
import { RepoScratchDirs } from '@webpieces/rules-config';
|
|
29
|
+
import { ExecutorResult } from '../../executor-result';
|
|
30
|
+
export interface DocsGenerateOptions {
|
|
31
|
+
/** A document name inside the build outputPath, e.g. `public-openapi.json`. Required. */
|
|
32
|
+
document?: string;
|
|
33
|
+
/** The site's directory name inside the build outputPath, e.g. `docs-site`. Required. */
|
|
34
|
+
siteDir?: string;
|
|
35
|
+
/** Workspace-relative directory holding `docs.manifest.json` and its markdown. Optional. */
|
|
36
|
+
prose?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */
|
|
39
|
+
export declare class DocsGenerate {
|
|
40
|
+
private readonly resolver;
|
|
41
|
+
private readonly runner;
|
|
42
|
+
private readonly scratch;
|
|
43
|
+
constructor(resolver?: ConsumerBinResolver, runner?: GeneratorRunner, scratch?: RepoScratchDirs);
|
|
44
|
+
/** @returns the files written, absolute. Throws RuleFailError on every refusal. */
|
|
45
|
+
run(options: DocsGenerateOptions, context: ExecutorContext): string[];
|
|
46
|
+
}
|
|
47
|
+
export default function runExecutor(options: DocsGenerateOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docs-generate Executor
|
|
4
|
+
*
|
|
5
|
+
* Renders the static API reference site from a document `openapi-generate` already wrote into the
|
|
6
|
+
* project's build `outputPath`, and writes the site into `<outputPath>/<siteDir>` — build output,
|
|
7
|
+
* published with the package, never committed.
|
|
8
|
+
*
|
|
9
|
+
* Usage (project.json):
|
|
10
|
+
*
|
|
11
|
+
* "docs-generate": {
|
|
12
|
+
* "executor": "@webpieces/nx-webpieces-rules:docs-generate",
|
|
13
|
+
* "dependsOn": ["openapi-generate"],
|
|
14
|
+
* "cache": true,
|
|
15
|
+
* "inputs": ["default", "^default"],
|
|
16
|
+
* "outputs": ["{workspaceRoot}/dist/<project>/docs-site"],
|
|
17
|
+
* "options": { "document": "public-openapi.json", "siteDir": "docs-site", "prose": "<project>/docs" }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* `document` should be the PARTNER-facing document: a site built from `full-private-openapi.json`
|
|
21
|
+
* publishes exactly the operations somebody decided not to publish. `prose` is optional because a site
|
|
22
|
+
* with no prose pages is a legal site; `document` and `siteDir` are required and have no defaults.
|
|
23
|
+
*
|
|
24
|
+
* The renderer is the CONSUMER's `@webpieces/docs-site`, resolved from its node_modules and
|
|
25
|
+
* version-checked; this plugin bundles no copy of it.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.DocsGenerate = void 0;
|
|
29
|
+
exports.default = runExecutor;
|
|
30
|
+
const tslib_1 = require("tslib");
|
|
31
|
+
const pr_gate_1 = require("@webpieces/pr-gate");
|
|
32
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
33
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
34
|
+
const path = tslib_1.__importStar(require("path"));
|
|
35
|
+
const executor_result_1 = require("../../executor-result");
|
|
36
|
+
const generator_target_1 = require("../../lib/generated-docs/generator-target");
|
|
37
|
+
const toError_1 = require("../../toError");
|
|
38
|
+
const RULE_NAME = 'docs-generate';
|
|
39
|
+
/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */
|
|
40
|
+
class DocsGenerate {
|
|
41
|
+
resolver;
|
|
42
|
+
runner;
|
|
43
|
+
scratch;
|
|
44
|
+
constructor(resolver = new pr_gate_1.ConsumerBinResolver(), runner = new pr_gate_1.GeneratorRunner(), scratch = new rules_config_1.RepoScratchDirs()) {
|
|
45
|
+
this.resolver = resolver;
|
|
46
|
+
this.runner = runner;
|
|
47
|
+
this.scratch = scratch;
|
|
48
|
+
}
|
|
49
|
+
/** @returns the files written, absolute. Throws RuleFailError on every refusal. */
|
|
50
|
+
run(options, context) {
|
|
51
|
+
const target = generator_target_1.GeneratorTarget.of(RULE_NAME, context);
|
|
52
|
+
target.assertDependsOn('openapi-generate');
|
|
53
|
+
const document = target.requiredOption(options.document, 'document');
|
|
54
|
+
const siteDir = target.requiredOption(options.siteDir, 'siteDir');
|
|
55
|
+
const outDir = target.buildOutputDir();
|
|
56
|
+
const spec = path.join(outDir, document);
|
|
57
|
+
if (!fs.existsSync(spec)) {
|
|
58
|
+
throw new rules_config_1.RuleFailError(RULE_NAME, `${path.relative(context.root, spec)} does not exist, so there is nothing to render. ` +
|
|
59
|
+
`openapi-generate writes only the documents the contracts' @ApiType(...) ask for.`, undefined, undefined, [new rules_config_1.Option(`Set options.document to a file openapi-generate writes into ${path.relative(context.root, outDir)}`, true)]);
|
|
60
|
+
}
|
|
61
|
+
const bin = this.resolver.resolve(new pr_gate_1.ConsumerBinRequest(RULE_NAME, pr_gate_1.DOCS_SITE, [path.join(context.root, target.projectRoot), context.root]));
|
|
62
|
+
const staging = this.scratch.make(context.root, 'wp-docs-generate-');
|
|
63
|
+
// webpieces-disable no-unmanaged-exceptions -- try/FINALLY only, nothing is caught: the staging
|
|
64
|
+
// directory is removed however this ends, and every throw still reaches runExecutor below
|
|
65
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
66
|
+
try {
|
|
67
|
+
const args = ['--spec', spec, '--out', staging];
|
|
68
|
+
if (options.prose !== undefined)
|
|
69
|
+
args.push('--prose', path.resolve(context.root, options.prose));
|
|
70
|
+
const run = this.runner.run(bin, args, context.root);
|
|
71
|
+
if (!run.ok) {
|
|
72
|
+
throw new rules_config_1.RuleFailError(RULE_NAME, `${bin.packageName} ${bin.version} refused ${document}:\n${run.output}`);
|
|
73
|
+
}
|
|
74
|
+
// The site REPLACES the previous one: a page for an operation that no longer exists must not
|
|
75
|
+
// survive into the published package.
|
|
76
|
+
const siteOut = path.join(outDir, siteDir);
|
|
77
|
+
fs.rmSync(siteOut, { recursive: true, force: true });
|
|
78
|
+
const written = new generator_target_1.StagedOutput().publish(staging, siteOut);
|
|
79
|
+
target.assertOutputsCover(written);
|
|
80
|
+
return written;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.DocsGenerate = DocsGenerate;
|
|
88
|
+
// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here
|
|
89
|
+
async function runExecutor(options, context) {
|
|
90
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- this IS the executor's single top-level handler
|
|
91
|
+
try {
|
|
92
|
+
const written = new DocsGenerate().run(options, context);
|
|
93
|
+
console.log(`wrote ${written.length} file(s) of the docs site for ${context.projectName ?? ''}`);
|
|
94
|
+
return new executor_result_1.ExecutorResult(true);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const error = (0, toError_1.toError)(err);
|
|
98
|
+
console.error(`❌ ${RULE_NAME}: ${error instanceof rules_config_1.RuleFailError ? (0, rules_config_1.renderRuleFailForHuman)(error) : error.message}`);
|
|
99
|
+
return new executor_result_1.ExecutorResult(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/docs-generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * docs-generate Executor\n *\n * Renders the static API reference site from a document `openapi-generate` already wrote into the\n * project's build `outputPath`, and writes the site into `<outputPath>/<siteDir>` — build output,\n * published with the package, never committed.\n *\n * Usage (project.json):\n *\n * \"docs-generate\": {\n * \"executor\": \"@webpieces/nx-webpieces-rules:docs-generate\",\n * \"dependsOn\": [\"openapi-generate\"],\n * \"cache\": true,\n * \"inputs\": [\"default\", \"^default\"],\n * \"outputs\": [\"{workspaceRoot}/dist/<project>/docs-site\"],\n * \"options\": { \"document\": \"public-openapi.json\", \"siteDir\": \"docs-site\", \"prose\": \"<project>/docs\" }\n * }\n *\n * `document` should be the PARTNER-facing document: a site built from `full-private-openapi.json`\n * publishes exactly the operations somebody decided not to publish. `prose` is optional because a site\n * with no prose pages is a legal site; `document` and `siteDir` are required and have no defaults.\n *\n * The renderer is the CONSUMER's `@webpieces/docs-site`, resolved from its node_modules and\n * version-checked; this plugin bundles no copy of it.\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { ConsumerBinRequest, ConsumerBinResolver, DOCS_SITE, GeneratorRunner } from '@webpieces/pr-gate';\nimport { Option, RepoScratchDirs, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ExecutorResult } from '../../executor-result';\nimport { GeneratorTarget, StagedOutput } from '../../lib/generated-docs/generator-target';\nimport { toError } from '../../toError';\n\nexport interface DocsGenerateOptions {\n /** A document name inside the build outputPath, e.g. `public-openapi.json`. Required. */\n document?: string;\n /** The site's directory name inside the build outputPath, e.g. `docs-site`. Required. */\n siteDir?: string;\n /** Workspace-relative directory holding `docs.manifest.json` and its markdown. Optional. */\n prose?: string;\n}\n\nconst RULE_NAME = 'docs-generate';\n\n/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */\nexport class DocsGenerate {\n constructor(\n private readonly resolver: ConsumerBinResolver = new ConsumerBinResolver(),\n private readonly runner: GeneratorRunner = new GeneratorRunner(),\n private readonly scratch: RepoScratchDirs = new RepoScratchDirs(),\n ) {}\n\n /** @returns the files written, absolute. Throws RuleFailError on every refusal. */\n run(options: DocsGenerateOptions, context: ExecutorContext): string[] {\n const target = GeneratorTarget.of(RULE_NAME, context);\n target.assertDependsOn('openapi-generate');\n const document = target.requiredOption(options.document, 'document');\n const siteDir = target.requiredOption(options.siteDir, 'siteDir');\n const outDir = target.buildOutputDir();\n const spec = path.join(outDir, document);\n if (!fs.existsSync(spec)) {\n throw new RuleFailError(\n RULE_NAME,\n `${path.relative(context.root, spec)} does not exist, so there is nothing to render. ` +\n `openapi-generate writes only the documents the contracts' @ApiType(...) ask for.`,\n undefined,\n undefined,\n [new Option(`Set options.document to a file openapi-generate writes into ${path.relative(context.root, outDir)}`, true)],\n );\n }\n const bin = this.resolver.resolve(new ConsumerBinRequest(\n RULE_NAME, DOCS_SITE, [path.join(context.root, target.projectRoot), context.root]));\n\n const staging = this.scratch.make(context.root, 'wp-docs-generate-');\n // webpieces-disable no-unmanaged-exceptions -- try/FINALLY only, nothing is caught: the staging\n // directory is removed however this ends, and every throw still reaches runExecutor below\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const args = ['--spec', spec, '--out', staging];\n if (options.prose !== undefined) args.push('--prose', path.resolve(context.root, options.prose));\n const run = this.runner.run(bin, args, context.root);\n if (!run.ok) {\n throw new RuleFailError(RULE_NAME, `${bin.packageName} ${bin.version} refused ${document}:\\n${run.output}`);\n }\n // The site REPLACES the previous one: a page for an operation that no longer exists must not\n // survive into the published package.\n const siteOut = path.join(outDir, siteDir);\n fs.rmSync(siteOut, { recursive: true, force: true });\n const written = new StagedOutput().publish(staging, siteOut);\n target.assertOutputsCover(written);\n return written;\n } finally {\n fs.rmSync(staging, { recursive: true, force: true });\n }\n }\n}\n\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here\nexport default async function runExecutor(\n options: DocsGenerateOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- this IS the executor's single top-level handler\n try {\n const written = new DocsGenerate().run(options, context);\n console.log(`wrote ${written.length} file(s) of the docs site for ${context.projectName ?? ''}`);\n return new ExecutorResult(true);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`❌ ${RULE_NAME}: ${error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message}`);\n return new ExecutorResult(false);\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "Docs Generate Executor",
|
|
4
|
+
"description": "Render the static API reference site from a document openapi-generate wrote into the build outputPath, with the consumer's own @webpieces/docs-site, into <outputPath>/<siteDir>. The target must dependsOn openapi-generate and declare outputs covering the site.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"document": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The document inside the build outputPath to render, e.g. public-openapi.json. No default."
|
|
10
|
+
},
|
|
11
|
+
"siteDir": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "The site's directory name inside the build outputPath, e.g. docs-site. No default."
|
|
14
|
+
},
|
|
15
|
+
"prose": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Workspace-relative directory holding docs.manifest.json and the markdown it names. Optional: a site with no prose pages is legal."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": ["document", "siteDir"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openapi-generate Executor
|
|
3
|
+
*
|
|
4
|
+
* Renders a project's contracts to OpenAPI (and `mcp-tools.json`) INTO the project's own build
|
|
5
|
+
* `outputPath`, so the documents are packed and published inside the api library's npm package: a
|
|
6
|
+
* consumer installs the library and has the contract. They are build output — never committed.
|
|
7
|
+
*
|
|
8
|
+
* Usage (project.json):
|
|
9
|
+
*
|
|
10
|
+
* "openapi-generate": {
|
|
11
|
+
* "executor": "@webpieces/nx-webpieces-rules:openapi-generate",
|
|
12
|
+
* "dependsOn": ["build"],
|
|
13
|
+
* "cache": true,
|
|
14
|
+
* "inputs": ["default", "^default"],
|
|
15
|
+
* "outputs": [
|
|
16
|
+
* "{workspaceRoot}/dist/<project>/*openapi.json",
|
|
17
|
+
* "{workspaceRoot}/dist/<project>/*openapi.yaml",
|
|
18
|
+
* "{workspaceRoot}/dist/<project>/mcp-tools.json"
|
|
19
|
+
* ],
|
|
20
|
+
* "options": { "manifest": "<project>/openapi.manifest.json", "format": "both" }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* `dependsOn: ["build"]` and `outputs` are CHECKED, not merely recommended — see GeneratorTarget. The
|
|
24
|
+
* generator itself is the CONSUMER's `@webpieces/openapi-generator`, resolved from its node_modules and
|
|
25
|
+
* version-checked; this plugin bundles no copy of it (see ConsumerBinResolver in @webpieces/pr-gate).
|
|
26
|
+
*/
|
|
27
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
28
|
+
import { ConsumerBinResolver, GeneratorRunner } from '@webpieces/pr-gate';
|
|
29
|
+
import { RepoScratchDirs } from '@webpieces/rules-config';
|
|
30
|
+
import { ExecutorResult } from '../../executor-result';
|
|
31
|
+
export interface OpenApiGenerateOptions {
|
|
32
|
+
/** Workspace-relative path to the project's `openapi.manifest.json`. Required; no default. */
|
|
33
|
+
manifest?: string;
|
|
34
|
+
/** `json`, `yaml` or `both`. Required; no default — it decides what the package ships. */
|
|
35
|
+
format?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */
|
|
38
|
+
export declare class OpenApiGenerate {
|
|
39
|
+
private readonly resolver;
|
|
40
|
+
private readonly runner;
|
|
41
|
+
private readonly scratch;
|
|
42
|
+
constructor(resolver?: ConsumerBinResolver, runner?: GeneratorRunner, scratch?: RepoScratchDirs);
|
|
43
|
+
/** @returns the files written, absolute. Throws RuleFailError on every refusal. */
|
|
44
|
+
run(options: OpenApiGenerateOptions, context: ExecutorContext): string[];
|
|
45
|
+
}
|
|
46
|
+
export default function runExecutor(options: OpenApiGenerateOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* openapi-generate Executor
|
|
4
|
+
*
|
|
5
|
+
* Renders a project's contracts to OpenAPI (and `mcp-tools.json`) INTO the project's own build
|
|
6
|
+
* `outputPath`, so the documents are packed and published inside the api library's npm package: a
|
|
7
|
+
* consumer installs the library and has the contract. They are build output — never committed.
|
|
8
|
+
*
|
|
9
|
+
* Usage (project.json):
|
|
10
|
+
*
|
|
11
|
+
* "openapi-generate": {
|
|
12
|
+
* "executor": "@webpieces/nx-webpieces-rules:openapi-generate",
|
|
13
|
+
* "dependsOn": ["build"],
|
|
14
|
+
* "cache": true,
|
|
15
|
+
* "inputs": ["default", "^default"],
|
|
16
|
+
* "outputs": [
|
|
17
|
+
* "{workspaceRoot}/dist/<project>/*openapi.json",
|
|
18
|
+
* "{workspaceRoot}/dist/<project>/*openapi.yaml",
|
|
19
|
+
* "{workspaceRoot}/dist/<project>/mcp-tools.json"
|
|
20
|
+
* ],
|
|
21
|
+
* "options": { "manifest": "<project>/openapi.manifest.json", "format": "both" }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* `dependsOn: ["build"]` and `outputs` are CHECKED, not merely recommended — see GeneratorTarget. The
|
|
25
|
+
* generator itself is the CONSUMER's `@webpieces/openapi-generator`, resolved from its node_modules and
|
|
26
|
+
* version-checked; this plugin bundles no copy of it (see ConsumerBinResolver in @webpieces/pr-gate).
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.OpenApiGenerate = void 0;
|
|
30
|
+
exports.default = runExecutor;
|
|
31
|
+
const tslib_1 = require("tslib");
|
|
32
|
+
const pr_gate_1 = require("@webpieces/pr-gate");
|
|
33
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
34
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
35
|
+
const path = tslib_1.__importStar(require("path"));
|
|
36
|
+
const executor_result_1 = require("../../executor-result");
|
|
37
|
+
const generator_target_1 = require("../../lib/generated-docs/generator-target");
|
|
38
|
+
const toError_1 = require("../../toError");
|
|
39
|
+
const RULE_NAME = 'openapi-generate';
|
|
40
|
+
/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */
|
|
41
|
+
class OpenApiGenerate {
|
|
42
|
+
resolver;
|
|
43
|
+
runner;
|
|
44
|
+
scratch;
|
|
45
|
+
constructor(resolver = new pr_gate_1.ConsumerBinResolver(), runner = new pr_gate_1.GeneratorRunner(), scratch = new rules_config_1.RepoScratchDirs()) {
|
|
46
|
+
this.resolver = resolver;
|
|
47
|
+
this.runner = runner;
|
|
48
|
+
this.scratch = scratch;
|
|
49
|
+
}
|
|
50
|
+
/** @returns the files written, absolute. Throws RuleFailError on every refusal. */
|
|
51
|
+
run(options, context) {
|
|
52
|
+
const target = generator_target_1.GeneratorTarget.of(RULE_NAME, context);
|
|
53
|
+
target.assertDependsOn('build');
|
|
54
|
+
const manifest = target.requiredOption(options.manifest, 'manifest');
|
|
55
|
+
const format = target.requiredOption(options.format, 'format');
|
|
56
|
+
const outDir = target.buildOutputDir();
|
|
57
|
+
const bin = this.resolver.resolve(new pr_gate_1.ConsumerBinRequest(RULE_NAME, pr_gate_1.OPENAPI_GENERATOR, [path.join(context.root, target.projectRoot), context.root]));
|
|
58
|
+
const staging = this.scratch.make(context.root, 'wp-openapi-generate-');
|
|
59
|
+
// webpieces-disable no-unmanaged-exceptions -- try/FINALLY only, nothing is caught: the staging
|
|
60
|
+
// directory is removed however this ends, and every throw still reaches runExecutor below
|
|
61
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
62
|
+
try {
|
|
63
|
+
const run = this.runner.run(bin, [
|
|
64
|
+
'--manifest', path.resolve(context.root, manifest), '--out', staging, '--format', format,
|
|
65
|
+
], context.root);
|
|
66
|
+
if (!run.ok) {
|
|
67
|
+
throw new rules_config_1.RuleFailError(RULE_NAME, `${bin.packageName} ${bin.version} refused ${manifest}:\n${run.output}`);
|
|
68
|
+
}
|
|
69
|
+
const written = new generator_target_1.StagedOutput().publish(staging, outDir);
|
|
70
|
+
target.assertOutputsCover(written);
|
|
71
|
+
return written;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.OpenApiGenerate = OpenApiGenerate;
|
|
79
|
+
// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here
|
|
80
|
+
async function runExecutor(options, context) {
|
|
81
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- this IS the executor's single top-level handler
|
|
82
|
+
try {
|
|
83
|
+
const written = new OpenApiGenerate().run(options, context);
|
|
84
|
+
for (const file of written)
|
|
85
|
+
console.log(`wrote ${path.relative(context.root, file)}`);
|
|
86
|
+
return new executor_result_1.ExecutorResult(true);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const error = (0, toError_1.toError)(err);
|
|
90
|
+
console.error(`❌ ${RULE_NAME}: ${error instanceof rules_config_1.RuleFailError ? (0, rules_config_1.renderRuleFailForHuman)(error) : error.message}`);
|
|
91
|
+
return new executor_result_1.ExecutorResult(false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/openapi-generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * openapi-generate Executor\n *\n * Renders a project's contracts to OpenAPI (and `mcp-tools.json`) INTO the project's own build\n * `outputPath`, so the documents are packed and published inside the api library's npm package: a\n * consumer installs the library and has the contract. They are build output — never committed.\n *\n * Usage (project.json):\n *\n * \"openapi-generate\": {\n * \"executor\": \"@webpieces/nx-webpieces-rules:openapi-generate\",\n * \"dependsOn\": [\"build\"],\n * \"cache\": true,\n * \"inputs\": [\"default\", \"^default\"],\n * \"outputs\": [\n * \"{workspaceRoot}/dist/<project>/*openapi.json\",\n * \"{workspaceRoot}/dist/<project>/*openapi.yaml\",\n * \"{workspaceRoot}/dist/<project>/mcp-tools.json\"\n * ],\n * \"options\": { \"manifest\": \"<project>/openapi.manifest.json\", \"format\": \"both\" }\n * }\n *\n * `dependsOn: [\"build\"]` and `outputs` are CHECKED, not merely recommended — see GeneratorTarget. The\n * generator itself is the CONSUMER's `@webpieces/openapi-generator`, resolved from its node_modules and\n * version-checked; this plugin bundles no copy of it (see ConsumerBinResolver in @webpieces/pr-gate).\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport {\n ConsumerBinRequest, ConsumerBinResolver, GeneratorRunner, OPENAPI_GENERATOR,\n} from '@webpieces/pr-gate';\nimport { RepoScratchDirs, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ExecutorResult } from '../../executor-result';\nimport { GeneratorTarget, StagedOutput } from '../../lib/generated-docs/generator-target';\nimport { toError } from '../../toError';\n\nexport interface OpenApiGenerateOptions {\n /** Workspace-relative path to the project's `openapi.manifest.json`. Required; no default. */\n manifest?: string;\n /** `json`, `yaml` or `both`. Required; no default — it decides what the package ships. */\n format?: string;\n}\n\nconst RULE_NAME = 'openapi-generate';\n\n/** Everything except the process-facing reporting, so the suite drives it exactly as nx does. */\nexport class OpenApiGenerate {\n constructor(\n private readonly resolver: ConsumerBinResolver = new ConsumerBinResolver(),\n private readonly runner: GeneratorRunner = new GeneratorRunner(),\n private readonly scratch: RepoScratchDirs = new RepoScratchDirs(),\n ) {}\n\n /** @returns the files written, absolute. Throws RuleFailError on every refusal. */\n run(options: OpenApiGenerateOptions, context: ExecutorContext): string[] {\n const target = GeneratorTarget.of(RULE_NAME, context);\n target.assertDependsOn('build');\n const manifest = target.requiredOption(options.manifest, 'manifest');\n const format = target.requiredOption(options.format, 'format');\n const outDir = target.buildOutputDir();\n const bin = this.resolver.resolve(new ConsumerBinRequest(\n RULE_NAME, OPENAPI_GENERATOR, [path.join(context.root, target.projectRoot), context.root]));\n\n const staging = this.scratch.make(context.root, 'wp-openapi-generate-');\n // webpieces-disable no-unmanaged-exceptions -- try/FINALLY only, nothing is caught: the staging\n // directory is removed however this ends, and every throw still reaches runExecutor below\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const run = this.runner.run(bin, [\n '--manifest', path.resolve(context.root, manifest), '--out', staging, '--format', format,\n ], context.root);\n if (!run.ok) {\n throw new RuleFailError(\n RULE_NAME,\n `${bin.packageName} ${bin.version} refused ${manifest}:\\n${run.output}`,\n );\n }\n const written = new StagedOutput().publish(staging, outDir);\n target.assertOutputsCover(written);\n return written;\n } finally {\n fs.rmSync(staging, { recursive: true, force: true });\n }\n }\n}\n\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here\nexport default async function runExecutor(\n options: OpenApiGenerateOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- this IS the executor's single top-level handler\n try {\n const written = new OpenApiGenerate().run(options, context);\n for (const file of written) console.log(`wrote ${path.relative(context.root, file)}`);\n return new ExecutorResult(true);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`❌ ${RULE_NAME}: ${error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message}`);\n return new ExecutorResult(false);\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "OpenAPI Generate Executor",
|
|
4
|
+
"description": "Render the project's contracts to OpenAPI (and mcp-tools.json) into the project's build outputPath, with the consumer's own @webpieces/openapi-generator. The target must dependsOn build and declare outputs covering what it writes.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"manifest": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Workspace-relative path to the project's openapi.manifest.json. No default."
|
|
10
|
+
},
|
|
11
|
+
"format": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": ["json", "yaml", "both"],
|
|
14
|
+
"description": "Which serializations to write, and therefore publish. No default."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": ["manifest", "format"]
|
|
18
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
3
|
-
* publishable", on
|
|
3
|
+
* publishable", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.
|
|
4
|
+
*
|
|
5
|
+
* "In scope" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the
|
|
6
|
+
* diff touched — the granularity nx already builds at, and the mode a consumer normally picks —
|
|
7
|
+
* while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`
|
|
8
|
+
* is the one place that answers it, for both rules.
|
|
4
9
|
*
|
|
5
10
|
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
6
11
|
*
|
|
@@ -20,17 +25,19 @@
|
|
|
20
25
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
21
26
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
22
27
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
23
|
-
* - an
|
|
24
|
-
* endpoint and is allowed there; an
|
|
25
|
-
* breaking change, where a named empty response object
|
|
28
|
+
* - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
29
|
+
* `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers
|
|
30
|
+
* nothing can never gain a field without a breaking change, where a named empty response object
|
|
31
|
+
* grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was
|
|
32
|
+
* unstated).
|
|
26
33
|
*
|
|
27
34
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
28
35
|
*
|
|
29
36
|
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
30
37
|
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
31
38
|
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
32
|
-
* expressible, by which time it is in partners' generated clients.
|
|
33
|
-
* contract in
|
|
39
|
+
* expressible, by which time it is in partners' generated clients. So every check below runs on every
|
|
40
|
+
* contract in scope, opted in or not — `@ApiType` narrows nothing here.
|
|
34
41
|
*
|
|
35
42
|
* ## Root-level unions are NOT re-checked here
|
|
36
43
|
*
|
|
@@ -54,7 +61,7 @@ import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
|
|
|
54
61
|
export declare class ApiDocRulesScan {
|
|
55
62
|
private readonly workspaceRoot;
|
|
56
63
|
private readonly projectInfos;
|
|
57
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
64
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
58
65
|
private readonly openApiRule;
|
|
59
66
|
private readonly mcpRule;
|
|
60
67
|
/**
|
|
@@ -66,7 +73,7 @@ export declare class ApiDocRulesScan {
|
|
|
66
73
|
*/
|
|
67
74
|
private readonly exclusions;
|
|
68
75
|
constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>,
|
|
69
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
76
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
70
77
|
openApiRule?: ApiDocRule, mcpRule?: ApiDocRule);
|
|
71
78
|
run(): ApiDocRulesFindings;
|
|
72
79
|
/** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
|
|
@@ -84,15 +91,16 @@ export declare class ApiDocRulesScan {
|
|
|
84
91
|
/** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */
|
|
85
92
|
private judgeUnknownValues;
|
|
86
93
|
/**
|
|
87
|
-
* An
|
|
94
|
+
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
88
95
|
*
|
|
89
96
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
90
|
-
* to shape — and is allowed. On an
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
97
|
+
* to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are
|
|
98
|
+
* synchronous request/response, an outside caller reads what comes back, and a `void` response
|
|
99
|
+
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
100
|
+
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
101
|
+
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
94
102
|
*/
|
|
95
|
-
private
|
|
103
|
+
private judgeAnsweringResponses;
|
|
96
104
|
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
97
105
|
private judgeTools;
|
|
98
106
|
/** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
4
|
-
* publishable", on
|
|
4
|
+
* publishable", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.
|
|
5
|
+
*
|
|
6
|
+
* "In scope" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the
|
|
7
|
+
* diff touched — the granularity nx already builds at, and the mode a consumer normally picks —
|
|
8
|
+
* while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`
|
|
9
|
+
* is the one place that answers it, for both rules.
|
|
5
10
|
*
|
|
6
11
|
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
7
12
|
*
|
|
@@ -21,17 +26,19 @@
|
|
|
21
26
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
22
27
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
23
28
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
24
|
-
* - an
|
|
25
|
-
* endpoint and is allowed there; an
|
|
26
|
-
* breaking change, where a named empty response object
|
|
29
|
+
* - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
30
|
+
* `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers
|
|
31
|
+
* nothing can never gain a field without a breaking change, where a named empty response object
|
|
32
|
+
* grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was
|
|
33
|
+
* unstated).
|
|
27
34
|
*
|
|
28
35
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
29
36
|
*
|
|
30
37
|
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
31
38
|
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
32
39
|
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
33
|
-
* expressible, by which time it is in partners' generated clients.
|
|
34
|
-
* contract in
|
|
40
|
+
* expressible, by which time it is in partners' generated clients. So every check below runs on every
|
|
41
|
+
* contract in scope, opted in or not — `@ApiType` narrows nothing here.
|
|
35
42
|
*
|
|
36
43
|
* ## Root-level unions are NOT re-checked here
|
|
37
44
|
*
|
|
@@ -48,7 +55,6 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
48
55
|
const path = tslib_1.__importStar(require("path"));
|
|
49
56
|
const ts = tslib_1.__importStar(require("typescript"));
|
|
50
57
|
const api_doc_model_1 = require("@webpieces/api-doc-model");
|
|
51
|
-
const rules_config_1 = require("@webpieces/rules-config");
|
|
52
58
|
const api_ast_1 = require("./api-ast");
|
|
53
59
|
const api_doc_rules_1 = require("./api-doc-rules");
|
|
54
60
|
const api_doc_rules_verdicts_1 = require("./api-doc-rules-verdicts");
|
|
@@ -188,7 +194,7 @@ class ApiDocRulesScan {
|
|
|
188
194
|
*/
|
|
189
195
|
exclusions = [];
|
|
190
196
|
constructor(workspaceRoot, projectInfos,
|
|
191
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
197
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
192
198
|
openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE), mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
|
|
193
199
|
this.workspaceRoot = workspaceRoot;
|
|
194
200
|
this.projectInfos = projectInfos;
|
|
@@ -245,7 +251,7 @@ class ApiDocRulesScan {
|
|
|
245
251
|
const lines = (0, api_doc_rules_verdicts_1.contractLinesOf)(source, model.contractName);
|
|
246
252
|
this.judgeUnmapped(model, sink, source.fileName);
|
|
247
253
|
this.judgeUnknownValues(model, sink, source.fileName);
|
|
248
|
-
this.
|
|
254
|
+
this.judgeAnsweringResponses(model, source, lines, sink);
|
|
249
255
|
this.judgeTools(model, source, lines, sink, toolNames);
|
|
250
256
|
}
|
|
251
257
|
/** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
|
|
@@ -269,23 +275,26 @@ class ApiDocRulesScan {
|
|
|
269
275
|
}
|
|
270
276
|
}
|
|
271
277
|
/**
|
|
272
|
-
* An
|
|
278
|
+
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
273
279
|
*
|
|
274
280
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
275
|
-
* to shape — and is allowed. On an
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
281
|
+
* to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are
|
|
282
|
+
* synchronous request/response, an outside caller reads what comes back, and a `void` response
|
|
283
|
+
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
284
|
+
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
285
|
+
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
279
286
|
*/
|
|
280
|
-
|
|
287
|
+
judgeAnsweringResponses(model, source, lines, sink) {
|
|
281
288
|
for (const endpoint of model.endpoints) {
|
|
282
|
-
|
|
289
|
+
// `.some` and not `.includes`: the model's `kind` is a widened string, and the list is
|
|
290
|
+
// typed EndpointKind so a kind that stops existing is a compile error here.
|
|
291
|
+
if (!api_doc_rules_verdicts_1.ANSWERING_KINDS.some((kind) => kind === endpoint.kind))
|
|
283
292
|
continue;
|
|
284
293
|
if (endpoint.response !== undefined && !(0, api_doc_rules_verdicts_1.isVoidLike)(endpoint.response))
|
|
285
294
|
continue;
|
|
286
295
|
const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));
|
|
287
|
-
sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, endpoint.methodName,
|
|
288
|
-
'return type)', site.relativeTo(this.workspaceRoot), api_doc_rules_verdicts_1.VOID_RPC_CURE, model.apiTypes), site);
|
|
296
|
+
sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, endpoint.methodName, `an ${endpoint.kind} endpoint returns nothing a document can name (void, ` +
|
|
297
|
+
'unknown, or no declared return type)', site.relativeTo(this.workspaceRoot), api_doc_rules_verdicts_1.VOID_RPC_CURE, model.apiTypes), site);
|
|
289
298
|
}
|
|
290
299
|
}
|
|
291
300
|
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
@@ -319,9 +328,11 @@ class ApiDocRulesScan {
|
|
|
319
328
|
for (const info of this.projectInfos.values()) {
|
|
320
329
|
if (info.root === '' || info.root === '.')
|
|
321
330
|
continue;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
// ONE question, asked of the rule itself: `mode` (OFF / AFFECTED_PROJECT /
|
|
332
|
+
// RUN_EVERY_TIME) and `allowedPaths` are both folded into coversProject, so the
|
|
333
|
+
// affected-project narrowing cannot be honoured in one branch and skipped in the other.
|
|
334
|
+
const openApi = this.openApiRule.coversProject(info.root);
|
|
335
|
+
const mcp = this.mcpRule.coversProject(info.root);
|
|
325
336
|
if (!openApi && !mcp)
|
|
326
337
|
continue;
|
|
327
338
|
const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');
|