@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 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.809",
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.809",
22
- "@webpieces/api-doc-model": "0.4.809",
23
- "@webpieces/code-rules": "0.4.809",
24
- "@webpieces/eslint-rules": "0.4.809",
25
- "@webpieces/pr-gate": "0.4.809",
26
- "@webpieces/rules-config": "0.4.809",
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 EVERY `@ApiPath` class in the workspace, `@ApiType` or not.
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 RPC whose response is `void`. Fire-and-forget is the CONTRACT of a `cloudtasks` or `cron`
24
- * endpoint and is allowed there; an RPC that answers nothing can never gain a field without a
25
- * breaking change, where a named empty response object grows additively forever.
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. Every check below runs on every
33
- * contract in the workspace.
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 — see `defaultRules`. */
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 — see `defaultRules`. */
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 RPC must NAME a response DTO, even an empty one.
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 RPC it is a one-way door: a `void` response can never gain a
91
- * field without breaking every generated client, where `{}` grows additively forever. This is a
92
- * contract-EVOLUTION rule, which is why it is here and not in the MCP half: it is worth having on
93
- * an RPC that never becomes a tool.
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 judgeRpcResponses;
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 EVERY `@ApiPath` class in the workspace, `@ApiType` or not.
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 RPC whose response is `void`. Fire-and-forget is the CONTRACT of a `cloudtasks` or `cron`
25
- * endpoint and is allowed there; an RPC that answers nothing can never gain a field without a
26
- * breaking change, where a named empty response object grows additively forever.
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. Every check below runs on every
34
- * contract in the workspace.
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 — see `defaultRules`. */
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.judgeRpcResponses(model, source, lines, sink);
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 RPC must NAME a response DTO, even an empty one.
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 RPC it is a one-way door: a `void` response can never gain a
276
- * field without breaking every generated client, where `{}` grows additively forever. This is a
277
- * contract-EVOLUTION rule, which is why it is here and not in the MCP half: it is worth having on
278
- * an RPC that never becomes a tool.
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
- judgeRpcResponses(model, source, lines, sink) {
287
+ judgeAnsweringResponses(model, source, lines, sink) {
281
288
  for (const endpoint of model.endpoints) {
282
- if (endpoint.kind !== api_doc_rules_verdicts_1.RPC_KIND)
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, 'an RPC returns nothing a document can name (void, unknown, or no declared ' +
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
- const openApi = this.openApiRule.enabled &&
323
- !(0, rules_config_1.matchesAnyGlob)(info.root, this.openApiRule.allowedPaths);
324
- const mcp = this.mcpRule.enabled && !(0, rules_config_1.matchesAnyGlob)(info.root, this.mcpRule.allowedPaths);
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');