@webpieces/nx-webpieces-rules 0.4.810 → 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/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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared half of the `openapi-generate` and `docs-generate` executors: WHERE a generated document
|
|
3
|
+
* goes, and the proof that the target is wired so that nx both orders and caches it correctly.
|
|
4
|
+
*
|
|
5
|
+
* Both halves read the project's own declarations and never supply a value of their own:
|
|
6
|
+
*
|
|
7
|
+
* - the output directory is the project's `build` target's `options.outputPath` — the directory tsc
|
|
8
|
+
* writes and the package is packed from, so the documents ship INSIDE the published package. It is
|
|
9
|
+
* ASKED of nx, never assumed: this repo builds into a workspace-root `dist/apps/...`, another
|
|
10
|
+
* consumer builds into a project-local `<project>/dist`, and hardcoding either one generates the
|
|
11
|
+
* document somewhere the package is not packed from;
|
|
12
|
+
* - the ordering and the cache are the target's own `dependsOn` and `outputs`, which the executor
|
|
13
|
+
* checks rather than trusts, because both failures are silent: a missing `dependsOn: ["build"]`
|
|
14
|
+
* lets tsc's clean of `outputPath` race the write (the document vanishes and a dependent test dies on
|
|
15
|
+
* ENOENT, intermittently, only in CI), and `outputs` that miss a written file make every cache hit
|
|
16
|
+
* restore a package without that file.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExecutorContext, TargetConfiguration } from '@nx/devkit';
|
|
19
|
+
export declare class GeneratorTarget {
|
|
20
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
21
|
+
readonly ruleName: string;
|
|
22
|
+
readonly workspaceRoot: string;
|
|
23
|
+
readonly projectName: string;
|
|
24
|
+
/** Workspace-relative. */
|
|
25
|
+
readonly projectRoot: string;
|
|
26
|
+
readonly targetName: string;
|
|
27
|
+
readonly target: TargetConfiguration;
|
|
28
|
+
readonly build: TargetConfiguration | undefined;
|
|
29
|
+
constructor(
|
|
30
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
31
|
+
ruleName: string, workspaceRoot: string, projectName: string,
|
|
32
|
+
/** Workspace-relative. */
|
|
33
|
+
projectRoot: string, targetName: string, target: TargetConfiguration, build: TargetConfiguration | undefined);
|
|
34
|
+
static of(ruleName: string, context: ExecutorContext): GeneratorTarget;
|
|
35
|
+
/** Absolute path to the project's declared `build` `outputPath` — the directory the package is packed from. */
|
|
36
|
+
buildOutputDir(): string;
|
|
37
|
+
/** Refuse unless this target `dependsOn` the sibling `required` target. */
|
|
38
|
+
assertDependsOn(required: string): void;
|
|
39
|
+
/** Refuse unless every written file is covered by the target's declared `outputs`, so a cache hit restores it. */
|
|
40
|
+
assertOutputsCover(writtenAbs: readonly string[]): void;
|
|
41
|
+
/** A workspace-relative path the consumer stated, from `options`, required and never defaulted. */
|
|
42
|
+
requiredOption(value: string | undefined, name: string): string;
|
|
43
|
+
private namesSibling;
|
|
44
|
+
/** nx's own tokens, so a path written the way nx accepts it resolves the way nx resolves it. */
|
|
45
|
+
private interpolate;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate into a STAGING directory, then publish into the output directory — so the executor knows
|
|
49
|
+
* exactly which files the run produced (for {@link GeneratorTarget.assertOutputsCover}) without
|
|
50
|
+
* parsing a generator's console output, and a failed run leaves the output directory untouched.
|
|
51
|
+
*/
|
|
52
|
+
export declare class StagedOutput {
|
|
53
|
+
publish(stagingDir: string, destDir: string): string[];
|
|
54
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The shared half of the `openapi-generate` and `docs-generate` executors: WHERE a generated document
|
|
4
|
+
* goes, and the proof that the target is wired so that nx both orders and caches it correctly.
|
|
5
|
+
*
|
|
6
|
+
* Both halves read the project's own declarations and never supply a value of their own:
|
|
7
|
+
*
|
|
8
|
+
* - the output directory is the project's `build` target's `options.outputPath` — the directory tsc
|
|
9
|
+
* writes and the package is packed from, so the documents ship INSIDE the published package. It is
|
|
10
|
+
* ASKED of nx, never assumed: this repo builds into a workspace-root `dist/apps/...`, another
|
|
11
|
+
* consumer builds into a project-local `<project>/dist`, and hardcoding either one generates the
|
|
12
|
+
* document somewhere the package is not packed from;
|
|
13
|
+
* - the ordering and the cache are the target's own `dependsOn` and `outputs`, which the executor
|
|
14
|
+
* checks rather than trusts, because both failures are silent: a missing `dependsOn: ["build"]`
|
|
15
|
+
* lets tsc's clean of `outputPath` race the write (the document vanishes and a dependent test dies on
|
|
16
|
+
* ENOENT, intermittently, only in CI), and `outputs` that miss a written file make every cache hit
|
|
17
|
+
* restore a package without that file.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.StagedOutput = exports.GeneratorTarget = void 0;
|
|
21
|
+
const tslib_1 = require("tslib");
|
|
22
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
23
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
24
|
+
const path = tslib_1.__importStar(require("path"));
|
|
25
|
+
class GeneratorTarget {
|
|
26
|
+
ruleName;
|
|
27
|
+
workspaceRoot;
|
|
28
|
+
projectName;
|
|
29
|
+
projectRoot;
|
|
30
|
+
targetName;
|
|
31
|
+
target;
|
|
32
|
+
build;
|
|
33
|
+
constructor(
|
|
34
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
35
|
+
ruleName, workspaceRoot, projectName,
|
|
36
|
+
/** Workspace-relative. */
|
|
37
|
+
projectRoot, targetName, target, build) {
|
|
38
|
+
this.ruleName = ruleName;
|
|
39
|
+
this.workspaceRoot = workspaceRoot;
|
|
40
|
+
this.projectName = projectName;
|
|
41
|
+
this.projectRoot = projectRoot;
|
|
42
|
+
this.targetName = targetName;
|
|
43
|
+
this.target = target;
|
|
44
|
+
this.build = build;
|
|
45
|
+
}
|
|
46
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
47
|
+
static of(ruleName, context) {
|
|
48
|
+
const projectName = context.projectName ?? '';
|
|
49
|
+
const project = context.projectsConfigurations?.projects[projectName];
|
|
50
|
+
const targetName = context.targetName ?? '';
|
|
51
|
+
const target = project?.targets?.[targetName];
|
|
52
|
+
if (project === undefined || target === undefined) {
|
|
53
|
+
// nx hands every executor its own project and target; their absence is our bug, not the consumer's.
|
|
54
|
+
throw new Error(`${ruleName}: nx did not describe project '${projectName}' target '${targetName}'`);
|
|
55
|
+
}
|
|
56
|
+
return new GeneratorTarget(ruleName, context.root, projectName, project.root, targetName, target, project.targets?.['build']);
|
|
57
|
+
}
|
|
58
|
+
/** Absolute path to the project's declared `build` `outputPath` — the directory the package is packed from. */
|
|
59
|
+
buildOutputDir() {
|
|
60
|
+
const declared = this.build?.options?.['outputPath'];
|
|
61
|
+
if (typeof declared !== 'string' || declared.trim() === '') {
|
|
62
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName} has no build target with an options.outputPath, so ${this.ruleName} has ` +
|
|
63
|
+
`nowhere to write that gets published. The documents go into the build's own output ` +
|
|
64
|
+
`directory — the one the package is packed from — and are never assumed to be ./dist.`, undefined, undefined, [new rules_config_1.Option(`Declare targets.build.options.outputPath in ${this.projectRoot}/project.json`, true)]);
|
|
65
|
+
}
|
|
66
|
+
return path.resolve(this.workspaceRoot, this.interpolate(declared));
|
|
67
|
+
}
|
|
68
|
+
/** Refuse unless this target `dependsOn` the sibling `required` target. */
|
|
69
|
+
assertDependsOn(required) {
|
|
70
|
+
const entries = this.target.dependsOn ?? [];
|
|
71
|
+
if (entries.some((entry) => this.namesSibling(entry, required)))
|
|
72
|
+
return;
|
|
73
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} does not declare dependsOn "${required}". It writes into ` +
|
|
74
|
+
`the build's outputPath, so without that edge nx may run it before or alongside ${required} ` +
|
|
75
|
+
`— and ${required}'s clean of that directory deletes the document it just wrote. That ` +
|
|
76
|
+
`fails intermittently and mostly in CI, which is why it is refused here instead.`, undefined, undefined, [new rules_config_1.Option(`Add "dependsOn": ["${required}"] to the ${this.targetName} target in ${this.projectRoot}/project.json`, true)]);
|
|
77
|
+
}
|
|
78
|
+
/** Refuse unless every written file is covered by the target's declared `outputs`, so a cache hit restores it. */
|
|
79
|
+
assertOutputsCover(writtenAbs) {
|
|
80
|
+
const patterns = (this.target.outputs ?? []).map((output) => this.interpolate(output));
|
|
81
|
+
const uncovered = writtenAbs
|
|
82
|
+
.map((file) => path.relative(this.workspaceRoot, file).split(path.sep).join('/'))
|
|
83
|
+
.filter((file) => !(0, rules_config_1.matchesAnyGlob)(file, patterns));
|
|
84
|
+
if (uncovered.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} wrote files its declared outputs do not cover, so a ` +
|
|
87
|
+
`cache hit would restore a package WITHOUT them:\n` +
|
|
88
|
+
uncovered.map((file) => ` ${file}`).join('\n'), undefined, undefined, [new rules_config_1.Option(`Cover them in the ${this.targetName} target's "outputs" in ${this.projectRoot}/project.json, ` +
|
|
89
|
+
`e.g. "{workspaceRoot}/${path.posix.dirname(uncovered[0])}/<the files>"`, true)]);
|
|
90
|
+
}
|
|
91
|
+
/** A workspace-relative path the consumer stated, from `options`, required and never defaulted. */
|
|
92
|
+
requiredOption(value, name) {
|
|
93
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
94
|
+
return value;
|
|
95
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} has no options.${name}. It is required and has no default.`, undefined, undefined, [new rules_config_1.Option(`Set options.${name} on the ${this.targetName} target in ${this.projectRoot}/project.json`, true)]);
|
|
96
|
+
}
|
|
97
|
+
namesSibling(entry, required) {
|
|
98
|
+
if (typeof entry === 'string')
|
|
99
|
+
return entry === required;
|
|
100
|
+
const projects = entry.projects;
|
|
101
|
+
const self = projects === undefined || projects === 'self' ||
|
|
102
|
+
(Array.isArray(projects) && projects.length === 1 && projects[0] === this.projectName);
|
|
103
|
+
return entry.target === required && entry.dependencies !== true && self;
|
|
104
|
+
}
|
|
105
|
+
/** nx's own tokens, so a path written the way nx accepts it resolves the way nx resolves it. */
|
|
106
|
+
interpolate(text) {
|
|
107
|
+
return text
|
|
108
|
+
.replace(/\{workspaceRoot\}\/?/g, '')
|
|
109
|
+
.replace(/\{projectRoot\}/g, this.projectRoot)
|
|
110
|
+
.replace(/\{projectName\}/g, this.projectName)
|
|
111
|
+
.replace(/\{options\.([A-Za-z0-9_]+)\}/g, (whole, key) => {
|
|
112
|
+
const value = this.target.options?.[key];
|
|
113
|
+
return typeof value === 'string' ? value : whole;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.GeneratorTarget = GeneratorTarget;
|
|
118
|
+
/**
|
|
119
|
+
* Generate into a STAGING directory, then publish into the output directory — so the executor knows
|
|
120
|
+
* exactly which files the run produced (for {@link GeneratorTarget.assertOutputsCover}) without
|
|
121
|
+
* parsing a generator's console output, and a failed run leaves the output directory untouched.
|
|
122
|
+
*/
|
|
123
|
+
class StagedOutput {
|
|
124
|
+
publish(stagingDir, destDir) {
|
|
125
|
+
const written = [];
|
|
126
|
+
const copy = (from, to) => {
|
|
127
|
+
fs.mkdirSync(to, { recursive: true });
|
|
128
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
129
|
+
const source = path.join(from, entry.name);
|
|
130
|
+
const target = path.join(to, entry.name);
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
copy(source, target);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
fs.copyFileSync(source, target);
|
|
136
|
+
written.push(target);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
copy(stagingDir, destDir);
|
|
141
|
+
return written.sort();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.StagedOutput = StagedOutput;
|
|
145
|
+
//# sourceMappingURL=generator-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator-target.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/generated-docs/generator-target.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared half of the `openapi-generate` and `docs-generate` executors: WHERE a generated document\n * goes, and the proof that the target is wired so that nx both orders and caches it correctly.\n *\n * Both halves read the project's own declarations and never supply a value of their own:\n *\n * - the output directory is the project's `build` target's `options.outputPath` — the directory tsc\n * writes and the package is packed from, so the documents ship INSIDE the published package. It is\n * ASKED of nx, never assumed: this repo builds into a workspace-root `dist/apps/...`, another\n * consumer builds into a project-local `<project>/dist`, and hardcoding either one generates the\n * document somewhere the package is not packed from;\n * - the ordering and the cache are the target's own `dependsOn` and `outputs`, which the executor\n * checks rather than trusts, because both failures are silent: a missing `dependsOn: [\"build\"]`\n * lets tsc's clean of `outputPath` race the write (the document vanishes and a dependent test dies on\n * ENOENT, intermittently, only in CI), and `outputs` that miss a written file make every cache hit\n * restore a package without that file.\n */\n\nimport type { ExecutorContext, TargetConfiguration, TargetDependencyConfig } from '@nx/devkit';\nimport { Option, RuleFailError, matchesAnyGlob } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\n/** What `dependsOn` may name: a sibling target by name, or nx's object form of the same. */\ntype DependsOnEntry = string | TargetDependencyConfig;\n\nexport class GeneratorTarget {\n constructor(\n /** The executor's name, which is what every refusal is reported under. */\n readonly ruleName: string,\n readonly workspaceRoot: string,\n readonly projectName: string,\n /** Workspace-relative. */\n readonly projectRoot: string,\n readonly targetName: string,\n readonly target: TargetConfiguration,\n readonly build: TargetConfiguration | undefined,\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static of(ruleName: string, context: ExecutorContext): GeneratorTarget {\n const projectName = context.projectName ?? '';\n const project = context.projectsConfigurations?.projects[projectName];\n const targetName = context.targetName ?? '';\n const target = project?.targets?.[targetName];\n if (project === undefined || target === undefined) {\n // nx hands every executor its own project and target; their absence is our bug, not the consumer's.\n throw new Error(`${ruleName}: nx did not describe project '${projectName}' target '${targetName}'`);\n }\n return new GeneratorTarget(\n ruleName, context.root, projectName, project.root, targetName, target, project.targets?.['build']);\n }\n\n /** Absolute path to the project's declared `build` `outputPath` — the directory the package is packed from. */\n buildOutputDir(): string {\n const declared = this.build?.options?.['outputPath'];\n if (typeof declared !== 'string' || declared.trim() === '') {\n throw new RuleFailError(\n this.ruleName,\n `${this.projectName} has no build target with an options.outputPath, so ${this.ruleName} has ` +\n `nowhere to write that gets published. The documents go into the build's own output ` +\n `directory — the one the package is packed from — and are never assumed to be ./dist.`,\n undefined,\n undefined,\n [new Option(`Declare targets.build.options.outputPath in ${this.projectRoot}/project.json`, true)],\n );\n }\n return path.resolve(this.workspaceRoot, this.interpolate(declared));\n }\n\n /** Refuse unless this target `dependsOn` the sibling `required` target. */\n assertDependsOn(required: string): void {\n const entries: readonly DependsOnEntry[] = this.target.dependsOn ?? [];\n if (entries.some((entry: DependsOnEntry) => this.namesSibling(entry, required))) return;\n throw new RuleFailError(\n this.ruleName,\n `${this.projectName}:${this.targetName} does not declare dependsOn \"${required}\". It writes into ` +\n `the build's outputPath, so without that edge nx may run it before or alongside ${required} ` +\n `— and ${required}'s clean of that directory deletes the document it just wrote. That ` +\n `fails intermittently and mostly in CI, which is why it is refused here instead.`,\n undefined,\n undefined,\n [new Option(`Add \"dependsOn\": [\"${required}\"] to the ${this.targetName} target in ${this.projectRoot}/project.json`, true)],\n );\n }\n\n /** Refuse unless every written file is covered by the target's declared `outputs`, so a cache hit restores it. */\n assertOutputsCover(writtenAbs: readonly string[]): void {\n const patterns = (this.target.outputs ?? []).map((output: string) => this.interpolate(output));\n const uncovered = writtenAbs\n .map((file: string) => path.relative(this.workspaceRoot, file).split(path.sep).join('/'))\n .filter((file: string) => !matchesAnyGlob(file, patterns));\n if (uncovered.length === 0) return;\n throw new RuleFailError(\n this.ruleName,\n `${this.projectName}:${this.targetName} wrote files its declared outputs do not cover, so a ` +\n `cache hit would restore a package WITHOUT them:\\n` +\n uncovered.map((file: string) => ` ${file}`).join('\\n'),\n undefined,\n undefined,\n [new Option(\n `Cover them in the ${this.targetName} target's \"outputs\" in ${this.projectRoot}/project.json, ` +\n `e.g. \"{workspaceRoot}/${path.posix.dirname(uncovered[0]!)}/<the files>\"`,\n true,\n )],\n );\n }\n\n /** A workspace-relative path the consumer stated, from `options`, required and never defaulted. */\n requiredOption(value: string | undefined, name: string): string {\n if (typeof value === 'string' && value.trim() !== '') return value;\n throw new RuleFailError(\n this.ruleName,\n `${this.projectName}:${this.targetName} has no options.${name}. It is required and has no default.`,\n undefined,\n undefined,\n [new Option(`Set options.${name} on the ${this.targetName} target in ${this.projectRoot}/project.json`, true)],\n );\n }\n\n private namesSibling(entry: DependsOnEntry, required: string): boolean {\n if (typeof entry === 'string') return entry === required;\n const projects = entry.projects;\n const self = projects === undefined || projects === 'self' ||\n (Array.isArray(projects) && projects.length === 1 && projects[0] === this.projectName);\n return entry.target === required && entry.dependencies !== true && self;\n }\n\n /** nx's own tokens, so a path written the way nx accepts it resolves the way nx resolves it. */\n private interpolate(text: string): string {\n return text\n .replace(/\\{workspaceRoot\\}\\/?/g, '')\n .replace(/\\{projectRoot\\}/g, this.projectRoot)\n .replace(/\\{projectName\\}/g, this.projectName)\n .replace(/\\{options\\.([A-Za-z0-9_]+)\\}/g, (whole: string, key: string): string => {\n const value = this.target.options?.[key];\n return typeof value === 'string' ? value : whole;\n });\n }\n}\n\n/**\n * Generate into a STAGING directory, then publish into the output directory — so the executor knows\n * exactly which files the run produced (for {@link GeneratorTarget.assertOutputsCover}) without\n * parsing a generator's console output, and a failed run leaves the output directory untouched.\n */\nexport class StagedOutput {\n publish(stagingDir: string, destDir: string): string[] {\n const written: string[] = [];\n const copy = (from: string, to: string): void => {\n fs.mkdirSync(to, { recursive: true });\n for (const entry of fs.readdirSync(from, { withFileTypes: true })) {\n const source = path.join(from, entry.name);\n const target = path.join(to, entry.name);\n if (entry.isDirectory()) {\n copy(source, target);\n } else {\n fs.copyFileSync(source, target);\n written.push(target);\n }\n }\n };\n copy(stagingDir, destDir);\n return written.sort();\n }\n}\n"]}
|