@webpieces/nx-webpieces-rules 0.4.810 → 0.4.812
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 +8 -7
- package/src/executors/docs-generate/executor.d.ts +48 -0
- package/src/executors/docs-generate/executor.js +104 -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 +51 -0
- package/src/executors/openapi-generate/executor.js +100 -0
- package/src/executors/openapi-generate/executor.js.map +1 -0
- package/src/executors/openapi-generate/schema.json +18 -0
- package/src/executors/validate-nx-wiring/executor.d.ts +4 -0
- package/src/executors/validate-nx-wiring/executor.js +14 -1
- package/src/executors/validate-nx-wiring/executor.js.map +1 -1
- package/src/generate-targets.d.ts +59 -0
- package/src/generate-targets.js +118 -0
- package/src/generate-targets.js.map +1 -0
- package/src/lib/api-docs/consumer-bin-resolver.d.ts +62 -0
- package/src/lib/api-docs/consumer-bin-resolver.js +148 -0
- package/src/lib/api-docs/consumer-bin-resolver.js.map +1 -0
- package/src/lib/api-docs/generate-wiring.d.ts +61 -0
- package/src/lib/api-docs/generate-wiring.js +163 -0
- package/src/lib/api-docs/generate-wiring.js.map +1 -0
- package/src/lib/api-docs/generator-package.d.ts +23 -0
- package/src/lib/api-docs/generator-package.js +32 -0
- package/src/lib/api-docs/generator-package.js.map +1 -0
- package/src/lib/api-docs/generator-runner.d.ts +21 -0
- package/src/lib/api-docs/generator-runner.js +38 -0
- package/src/lib/api-docs/generator-runner.js.map +1 -0
- package/src/lib/api-docs/generator-target.d.ts +61 -0
- package/src/lib/api-docs/generator-target.js +161 -0
- package/src/lib/api-docs/generator-target.js.map +1 -0
- package/src/lib/api-docs/json-value.d.ts +6 -0
- package/src/lib/api-docs/json-value.js +3 -0
- package/src/lib/api-docs/json-value.js.map +1 -0
- package/src/plugin.d.ts +2 -0
- package/src/plugin.js +15 -2
- package/src/plugin.js.map +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The build SHAPE a project opting into generated API documents must have, checked by
|
|
4
|
+
* `validate-nx-wiring` (#1021):
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* <api-lib>:compile @nx/js:tsc → its outputPath (dependsOn ^build)
|
|
8
|
+
* <api-lib>:openapi-generate dependsOn ["compile"] → writes into compile's outputPath
|
|
9
|
+
* <api-lib>:build nx:noop, dependsOn ["compile", "openapi-generate"]
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Why that shape and no other: nx `dependsOn` only points BACKWARD, generation must run AFTER tsc
|
|
13
|
+
* (whose cleanup wipes the outputPath), and every consumer only ever asks for `^build` — servers,
|
|
14
|
+
* Dockerfiles (`nx run-many -t build -p lang-server`). With `build` as a noop over both, every existing
|
|
15
|
+
* `^build` pulls generation in with zero consumer change.
|
|
16
|
+
*
|
|
17
|
+
* And one rule for the projects that DEPEND on such a library: their `test` must dependsOn `^build`.
|
|
18
|
+
* A server spec that boots the MCP server reads the library's generated tool catalogs out of its build
|
|
19
|
+
* output (`McpToolCatalog.fromPackages` on a workspace source directory), so a test run that did not
|
|
20
|
+
* build the library first fails on a missing file — or, worse, passes against a stale one.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.GenerateWiring = exports.ProjectDependency = exports.GenerateWiringProblem = void 0;
|
|
24
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
25
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
26
|
+
const generate_targets_1 = require("../../generate-targets");
|
|
27
|
+
/** One project's wiring defect, and the exact project.json edit that fixes it. Data-only. */
|
|
28
|
+
class GenerateWiringProblem {
|
|
29
|
+
project;
|
|
30
|
+
problem;
|
|
31
|
+
cure;
|
|
32
|
+
constructor(project, problem, cure) {
|
|
33
|
+
this.project = project;
|
|
34
|
+
this.problem = problem;
|
|
35
|
+
this.cure = cure;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.GenerateWiringProblem = GenerateWiringProblem;
|
|
39
|
+
/** The one field of an nx graph dependency this check reads. Data-only. */
|
|
40
|
+
class ProjectDependency {
|
|
41
|
+
target;
|
|
42
|
+
constructor(target) {
|
|
43
|
+
this.target = target;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.ProjectDependency = ProjectDependency;
|
|
47
|
+
/** The executors the plugin infers from a tag — a project.json naming one by hand is a second opt-in. */
|
|
48
|
+
const INFERRED_EXECUTORS = [
|
|
49
|
+
'@webpieces/nx-webpieces-rules:openapi-generate',
|
|
50
|
+
'@webpieces/nx-webpieces-rules:docs-generate',
|
|
51
|
+
];
|
|
52
|
+
class GenerateWiring {
|
|
53
|
+
projects;
|
|
54
|
+
dependencies;
|
|
55
|
+
constructor(
|
|
56
|
+
/** nx's RESOLVED project configurations — targetDefaults and inferred targets merged in. */
|
|
57
|
+
projects,
|
|
58
|
+
/** The project graph's dependencies, by source project. */
|
|
59
|
+
dependencies) {
|
|
60
|
+
this.projects = projects;
|
|
61
|
+
this.dependencies = dependencies;
|
|
62
|
+
}
|
|
63
|
+
/** The projects tagged to generate API documents. */
|
|
64
|
+
generating() {
|
|
65
|
+
return Object.keys(this.projects)
|
|
66
|
+
.filter((name) => {
|
|
67
|
+
const tags = this.projects[name].tags ?? [];
|
|
68
|
+
return tags.includes(generate_targets_1.GENERATE_OPENAPI_TAG) || tags.includes(generate_targets_1.GENERATE_DOCS_SITE_TAG);
|
|
69
|
+
})
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
/** The problems as ONE structured failure: each problem in the message, each fix as an Option. */
|
|
73
|
+
failure(problems) {
|
|
74
|
+
return new rules_config_1.RuleFailError('nx-wiring', 'A project generating API documents is not wired so that ^build generates them:\n' +
|
|
75
|
+
problems.map((each) => ` ${each.project}: ${each.problem}`).join('\n'), undefined, undefined, problems.map((each) => new rules_config_1.Option(`${each.project}: ${each.cure}`, true)));
|
|
76
|
+
}
|
|
77
|
+
/** Render {@link failure} through the one human renderer, as validate-nx-wiring's report. */
|
|
78
|
+
report(problems) {
|
|
79
|
+
console.error(`\n❌ ${(0, rules_config_1.renderRuleFailForHuman)(this.failure(problems))}\n`);
|
|
80
|
+
}
|
|
81
|
+
problems() {
|
|
82
|
+
const generating = this.generating();
|
|
83
|
+
const problems = this.handWritten(new Set(generating));
|
|
84
|
+
for (const name of generating)
|
|
85
|
+
problems.push(...this.shapeOf(name));
|
|
86
|
+
problems.push(...this.dependentsOf(new Set(generating)));
|
|
87
|
+
return problems;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* An UNTAGGED project naming an inferred executor by hand: a second way to opt in, which also slips
|
|
91
|
+
* past the shape check (and breaks the graph load the day the installed plugin lacks the executor).
|
|
92
|
+
*/
|
|
93
|
+
handWritten(generating) {
|
|
94
|
+
const problems = [];
|
|
95
|
+
for (const name of Object.keys(this.projects).sort()) {
|
|
96
|
+
if (generating.has(name))
|
|
97
|
+
continue;
|
|
98
|
+
const project = this.projects[name];
|
|
99
|
+
for (const [targetName, target] of Object.entries(project.targets ?? {})) {
|
|
100
|
+
if (target.executor === undefined || !INFERRED_EXECUTORS.includes(target.executor))
|
|
101
|
+
continue;
|
|
102
|
+
problems.push(new GenerateWiringProblem(name, `targets.${targetName} names the executor ${target.executor} by hand, and the project has no ` +
|
|
103
|
+
`"${generate_targets_1.GENERATE_OPENAPI_TAG}" / "${generate_targets_1.GENERATE_DOCS_SITE_TAG}" tag. Opting in is the tag; the plugin ` +
|
|
104
|
+
'infers the executor.', `Add "${targetName === core_util_1.GeneratedApiDocsLayout.DOCS_TARGET ? generate_targets_1.GENERATE_DOCS_SITE_TAG : generate_targets_1.GENERATE_OPENAPI_TAG}" ` +
|
|
105
|
+
`to "tags" in ${project.root}/project.json and delete the "executor" line of targets.${targetName}.`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
shapeOf(name) {
|
|
111
|
+
const project = this.projects[name];
|
|
112
|
+
const where = `${project.root}/project.json`;
|
|
113
|
+
const targets = project.targets ?? {};
|
|
114
|
+
const lookup = new core_util_1.GeneratedApiDocsLayout(project.root, name, targets).outputTarget();
|
|
115
|
+
if (lookup.found === undefined) {
|
|
116
|
+
return [new GenerateWiringProblem(name, lookup.problem.problem, lookup.problem.cure)];
|
|
117
|
+
}
|
|
118
|
+
const compileName = lookup.found.targetName;
|
|
119
|
+
const problems = [];
|
|
120
|
+
if (!this.names(targets[compileName], '^build')) {
|
|
121
|
+
problems.push(new GenerateWiringProblem(name, `${name}:${compileName} (the target ${core_util_1.GeneratedApiDocsLayout.OPENAPI_TARGET} writes into) does ` +
|
|
122
|
+
'not dependsOn "^build", so upstream libraries are not built before it compiles.', `Add "dependsOn": ["^build"] to targets.${compileName} in ${where}.`));
|
|
123
|
+
}
|
|
124
|
+
const build = targets['build'];
|
|
125
|
+
const buildOk = build !== undefined && build.executor === 'nx:noop' &&
|
|
126
|
+
this.names(build, compileName) && this.names(build, core_util_1.GeneratedApiDocsLayout.OPENAPI_TARGET);
|
|
127
|
+
if (!buildOk) {
|
|
128
|
+
problems.push(new GenerateWiringProblem(name, `${name}:build must be an nx:noop over "${compileName}" and "${core_util_1.GeneratedApiDocsLayout.OPENAPI_TARGET}" ` +
|
|
129
|
+
`— it is what every consumer's ^build asks for, so it is what pulls generation in — and it is ` +
|
|
130
|
+
`${build === undefined ? 'missing' : `${build.executor ?? '(no executor)'} dependsOn ${JSON.stringify(build.dependsOn ?? [])}`}.`, `Set targets.build in ${where} to ` +
|
|
131
|
+
`{ "executor": "nx:noop", "dependsOn": ["${compileName}", "${core_util_1.GeneratedApiDocsLayout.OPENAPI_TARGET}"] }, ` +
|
|
132
|
+
`moving the @nx/js:tsc options to targets.${compileName}.`));
|
|
133
|
+
}
|
|
134
|
+
return problems;
|
|
135
|
+
}
|
|
136
|
+
dependentsOf(generating) {
|
|
137
|
+
const problems = [];
|
|
138
|
+
for (const name of Object.keys(this.projects).sort()) {
|
|
139
|
+
const upstream = (this.dependencies[name] ?? [])
|
|
140
|
+
.map((dependency) => dependency.target)
|
|
141
|
+
.filter((target) => generating.has(target) && target !== name);
|
|
142
|
+
const test = this.projects[name].targets?.['test'];
|
|
143
|
+
if (upstream.length === 0 || test === undefined || this.names(test, '^build'))
|
|
144
|
+
continue;
|
|
145
|
+
problems.push(new GenerateWiringProblem(name, `${name} depends on ${[...new Set(upstream)].sort().join(', ')}, which generate${upstream.length === 1 ? 's' : ''} ` +
|
|
146
|
+
'API documents and MCP tool catalogs into their build output, and its test target does not ' +
|
|
147
|
+
'dependsOn "^build" — a spec that boots the MCP server would read catalogs that were never built.', `Add "dependsOn": ["^build"] to targets.test in ${this.projects[name].root}/project.json.`));
|
|
148
|
+
}
|
|
149
|
+
return problems;
|
|
150
|
+
}
|
|
151
|
+
/** Whether `target` dependsOn `wanted` — `^x` for the upstream form, `x` for a sibling. */
|
|
152
|
+
names(target, wanted) {
|
|
153
|
+
const upstream = wanted.startsWith('^');
|
|
154
|
+
const name = upstream ? wanted.slice(1) : wanted;
|
|
155
|
+
return (target?.dependsOn ?? []).some((entry) => {
|
|
156
|
+
if (typeof entry === 'string')
|
|
157
|
+
return entry === wanted;
|
|
158
|
+
return entry.target === name && (entry.dependencies === true) === upstream;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.GenerateWiring = GenerateWiring;
|
|
163
|
+
//# sourceMappingURL=generate-wiring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-wiring.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/generate-wiring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The build SHAPE a project opting into generated API documents must have, checked by\n * `validate-nx-wiring` (#1021):\n *\n * ```\n * <api-lib>:compile @nx/js:tsc → its outputPath (dependsOn ^build)\n * <api-lib>:openapi-generate dependsOn [\"compile\"] → writes into compile's outputPath\n * <api-lib>:build nx:noop, dependsOn [\"compile\", \"openapi-generate\"]\n * ```\n *\n * Why that shape and no other: nx `dependsOn` only points BACKWARD, generation must run AFTER tsc\n * (whose cleanup wipes the outputPath), and every consumer only ever asks for `^build` — servers,\n * Dockerfiles (`nx run-many -t build -p lang-server`). With `build` as a noop over both, every existing\n * `^build` pulls generation in with zero consumer change.\n *\n * And one rule for the projects that DEPEND on such a library: their `test` must dependsOn `^build`.\n * A server spec that boots the MCP server reads the library's generated tool catalogs out of its build\n * output (`McpToolCatalog.fromPackages` on a workspace source directory), so a test run that did not\n * build the library first fails on a missing file — or, worse, passes against a stale one.\n */\n\nimport type { ProjectConfiguration, TargetConfiguration, TargetDependencyConfig } from '@nx/devkit';\nimport { GeneratedApiDocsLayout } from '@webpieces/core-util';\nimport { Option, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { GENERATE_DOCS_SITE_TAG, GENERATE_OPENAPI_TAG } from '../../generate-targets';\n\n/** One project's wiring defect, and the exact project.json edit that fixes it. Data-only. */\nexport class GenerateWiringProblem {\n constructor(\n readonly project: string,\n readonly problem: string,\n readonly cure: string,\n ) {}\n}\n\n/** The one field of an nx graph dependency this check reads. Data-only. */\nexport class ProjectDependency {\n constructor(readonly target: string) {}\n}\n\ntype DependsOnEntry = string | TargetDependencyConfig;\n\n/** The executors the plugin infers from a tag — a project.json naming one by hand is a second opt-in. */\nconst INFERRED_EXECUTORS: readonly string[] = [\n '@webpieces/nx-webpieces-rules:openapi-generate',\n '@webpieces/nx-webpieces-rules:docs-generate',\n];\n\nexport class GenerateWiring {\n constructor(\n /** nx's RESOLVED project configurations — targetDefaults and inferred targets merged in. */\n private readonly projects: Readonly<Record<string, ProjectConfiguration>>,\n /** The project graph's dependencies, by source project. */\n private readonly dependencies: Readonly<Record<string, readonly ProjectDependency[]>>,\n ) {}\n\n /** The projects tagged to generate API documents. */\n generating(): string[] {\n return Object.keys(this.projects)\n .filter((name: string) => {\n const tags = this.projects[name]!.tags ?? [];\n return tags.includes(GENERATE_OPENAPI_TAG) || tags.includes(GENERATE_DOCS_SITE_TAG);\n })\n .sort();\n }\n\n /** The problems as ONE structured failure: each problem in the message, each fix as an Option. */\n failure(problems: readonly GenerateWiringProblem[]): RuleFailError {\n return new RuleFailError(\n 'nx-wiring',\n 'A project generating API documents is not wired so that ^build generates them:\\n' +\n problems.map((each: GenerateWiringProblem) => ` ${each.project}: ${each.problem}`).join('\\n'),\n undefined,\n undefined,\n problems.map((each: GenerateWiringProblem) => new Option(`${each.project}: ${each.cure}`, true)),\n );\n }\n\n /** Render {@link failure} through the one human renderer, as validate-nx-wiring's report. */\n report(problems: readonly GenerateWiringProblem[]): void {\n console.error(`\\n❌ ${renderRuleFailForHuman(this.failure(problems))}\\n`);\n }\n\n problems(): GenerateWiringProblem[] {\n const generating = this.generating();\n const problems: GenerateWiringProblem[] = this.handWritten(new Set(generating));\n for (const name of generating) problems.push(...this.shapeOf(name));\n problems.push(...this.dependentsOf(new Set(generating)));\n return problems;\n }\n\n /**\n * An UNTAGGED project naming an inferred executor by hand: a second way to opt in, which also slips\n * past the shape check (and breaks the graph load the day the installed plugin lacks the executor).\n */\n private handWritten(generating: ReadonlySet<string>): GenerateWiringProblem[] {\n const problems: GenerateWiringProblem[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (generating.has(name)) continue;\n const project = this.projects[name]!;\n for (const [targetName, target] of Object.entries(project.targets ?? {})) {\n if (target.executor === undefined || !INFERRED_EXECUTORS.includes(target.executor)) continue;\n problems.push(new GenerateWiringProblem(\n name,\n `targets.${targetName} names the executor ${target.executor} by hand, and the project has no ` +\n `\"${GENERATE_OPENAPI_TAG}\" / \"${GENERATE_DOCS_SITE_TAG}\" tag. Opting in is the tag; the plugin ` +\n 'infers the executor.',\n `Add \"${targetName === GeneratedApiDocsLayout.DOCS_TARGET ? GENERATE_DOCS_SITE_TAG : GENERATE_OPENAPI_TAG}\" ` +\n `to \"tags\" in ${project.root}/project.json and delete the \"executor\" line of targets.${targetName}.`,\n ));\n }\n }\n return problems;\n }\n\n private shapeOf(name: string): GenerateWiringProblem[] {\n const project = this.projects[name]!;\n const where = `${project.root}/project.json`;\n const targets = project.targets ?? {};\n const lookup = new GeneratedApiDocsLayout(project.root, name, targets).outputTarget();\n if (lookup.found === undefined) {\n return [new GenerateWiringProblem(name, lookup.problem!.problem, lookup.problem!.cure)];\n }\n const compileName = lookup.found.targetName;\n const problems: GenerateWiringProblem[] = [];\n if (!this.names(targets[compileName], '^build')) {\n problems.push(new GenerateWiringProblem(\n name,\n `${name}:${compileName} (the target ${GeneratedApiDocsLayout.OPENAPI_TARGET} writes into) does ` +\n 'not dependsOn \"^build\", so upstream libraries are not built before it compiles.',\n `Add \"dependsOn\": [\"^build\"] to targets.${compileName} in ${where}.`,\n ));\n }\n const build = targets['build'];\n const buildOk = build !== undefined && build.executor === 'nx:noop' &&\n this.names(build, compileName) && this.names(build, GeneratedApiDocsLayout.OPENAPI_TARGET);\n if (!buildOk) {\n problems.push(new GenerateWiringProblem(\n name,\n `${name}:build must be an nx:noop over \"${compileName}\" and \"${GeneratedApiDocsLayout.OPENAPI_TARGET}\" ` +\n `— it is what every consumer's ^build asks for, so it is what pulls generation in — and it is ` +\n `${build === undefined ? 'missing' : `${build.executor ?? '(no executor)'} dependsOn ${JSON.stringify(build.dependsOn ?? [])}`}.`,\n `Set targets.build in ${where} to ` +\n `{ \"executor\": \"nx:noop\", \"dependsOn\": [\"${compileName}\", \"${GeneratedApiDocsLayout.OPENAPI_TARGET}\"] }, ` +\n `moving the @nx/js:tsc options to targets.${compileName}.`,\n ));\n }\n return problems;\n }\n\n private dependentsOf(generating: ReadonlySet<string>): GenerateWiringProblem[] {\n const problems: GenerateWiringProblem[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n const upstream = (this.dependencies[name] ?? [])\n .map((dependency: ProjectDependency) => dependency.target)\n .filter((target: string) => generating.has(target) && target !== name);\n const test = this.projects[name]!.targets?.['test'];\n if (upstream.length === 0 || test === undefined || this.names(test, '^build')) continue;\n problems.push(new GenerateWiringProblem(\n name,\n `${name} depends on ${[...new Set(upstream)].sort().join(', ')}, which generate${upstream.length === 1 ? 's' : ''} ` +\n 'API documents and MCP tool catalogs into their build output, and its test target does not ' +\n 'dependsOn \"^build\" — a spec that boots the MCP server would read catalogs that were never built.',\n `Add \"dependsOn\": [\"^build\"] to targets.test in ${this.projects[name]!.root}/project.json.`,\n ));\n }\n return problems;\n }\n\n /** Whether `target` dependsOn `wanted` — `^x` for the upstream form, `x` for a sibling. */\n private names(target: TargetConfiguration | undefined, wanted: string): boolean {\n const upstream = wanted.startsWith('^');\n const name = upstream ? wanted.slice(1) : wanted;\n return (target?.dependsOn ?? []).some((entry: DependsOnEntry) => {\n if (typeof entry === 'string') return entry === wanted;\n return entry.target === name && (entry.dependencies === true) === upstream;\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generator the `openapi-generate` / `docs-generate` executors RUN but never bundle, and the oldest
|
|
3
|
+
* release of it that has every capability the executor relies on. Data-only.
|
|
4
|
+
*
|
|
5
|
+
* `minimumVersion` is the version handshake. A capability ships in the generator (the server stream)
|
|
6
|
+
* first; the executor that relies on it follows, and raises this number in the same change. Keep it the
|
|
7
|
+
* FIRST release carrying the capability, not the latest one — raising it further forces a consumer bump
|
|
8
|
+
* that buys nothing.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GeneratorPackage {
|
|
11
|
+
readonly packageName: string;
|
|
12
|
+
readonly binName: string;
|
|
13
|
+
readonly minimumVersion: string;
|
|
14
|
+
constructor(packageName: string, binName: string, minimumVersion: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `wp-openapi` with `--manifest/--out/--format`, writing ONE MCP tool catalog PER CONTRACT
|
|
18
|
+
* (`mcp-<ContractClass>-tools.json`, #1021). 0.4.812 is the first release carrying that layout; an
|
|
19
|
+
* older generator writes the single `mcp-tools.json` no server of this release reads.
|
|
20
|
+
*/
|
|
21
|
+
export declare const OPENAPI_GENERATOR: GeneratorPackage;
|
|
22
|
+
/** `wp-docs-site --spec/--prose/--out`: 0.4.807 is the first release carrying the docs site (#1007). */
|
|
23
|
+
export declare const DOCS_SITE: GeneratorPackage;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOCS_SITE = exports.OPENAPI_GENERATOR = exports.GeneratorPackage = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A generator the `openapi-generate` / `docs-generate` executors RUN but never bundle, and the oldest
|
|
6
|
+
* release of it that has every capability the executor relies on. Data-only.
|
|
7
|
+
*
|
|
8
|
+
* `minimumVersion` is the version handshake. A capability ships in the generator (the server stream)
|
|
9
|
+
* first; the executor that relies on it follows, and raises this number in the same change. Keep it the
|
|
10
|
+
* FIRST release carrying the capability, not the latest one — raising it further forces a consumer bump
|
|
11
|
+
* that buys nothing.
|
|
12
|
+
*/
|
|
13
|
+
class GeneratorPackage {
|
|
14
|
+
packageName;
|
|
15
|
+
binName;
|
|
16
|
+
minimumVersion;
|
|
17
|
+
constructor(packageName, binName, minimumVersion) {
|
|
18
|
+
this.packageName = packageName;
|
|
19
|
+
this.binName = binName;
|
|
20
|
+
this.minimumVersion = minimumVersion;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.GeneratorPackage = GeneratorPackage;
|
|
24
|
+
/**
|
|
25
|
+
* `wp-openapi` with `--manifest/--out/--format`, writing ONE MCP tool catalog PER CONTRACT
|
|
26
|
+
* (`mcp-<ContractClass>-tools.json`, #1021). 0.4.812 is the first release carrying that layout; an
|
|
27
|
+
* older generator writes the single `mcp-tools.json` no server of this release reads.
|
|
28
|
+
*/
|
|
29
|
+
exports.OPENAPI_GENERATOR = new GeneratorPackage('@webpieces/openapi-generator', 'wp-openapi', '0.4.812');
|
|
30
|
+
/** `wp-docs-site --spec/--prose/--out`: 0.4.807 is the first release carrying the docs site (#1007). */
|
|
31
|
+
exports.DOCS_SITE = new GeneratorPackage('@webpieces/docs-site', 'wp-docs-site', '0.4.807');
|
|
32
|
+
//# sourceMappingURL=generator-package.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator-package.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/generator-package.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,gBAAgB;IAEZ;IACA;IACA;IAHb,YACa,WAAmB,EACnB,OAAe,EACf,cAAsB;QAFtB,gBAAW,GAAX,WAAW,CAAQ;QACnB,YAAO,GAAP,OAAO,CAAQ;QACf,mBAAc,GAAd,cAAc,CAAQ;IAChC,CAAC;CACP;AAND,4CAMC;AAED;;;;GAIG;AACU,QAAA,iBAAiB,GAAG,IAAI,gBAAgB,CAAC,8BAA8B,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;AAE/G,wGAAwG;AAC3F,QAAA,SAAS,GAAG,IAAI,gBAAgB,CAAC,sBAAsB,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC","sourcesContent":["/**\n * A generator the `openapi-generate` / `docs-generate` executors RUN but never bundle, and the oldest\n * release of it that has every capability the executor relies on. Data-only.\n *\n * `minimumVersion` is the version handshake. A capability ships in the generator (the server stream)\n * first; the executor that relies on it follows, and raises this number in the same change. Keep it the\n * FIRST release carrying the capability, not the latest one — raising it further forces a consumer bump\n * that buys nothing.\n */\nexport class GeneratorPackage {\n constructor(\n readonly packageName: string,\n readonly binName: string,\n readonly minimumVersion: string,\n ) {}\n}\n\n/**\n * `wp-openapi` with `--manifest/--out/--format`, writing ONE MCP tool catalog PER CONTRACT\n * (`mcp-<ContractClass>-tools.json`, #1021). 0.4.812 is the first release carrying that layout; an\n * older generator writes the single `mcp-tools.json` no server of this release reads.\n */\nexport const OPENAPI_GENERATOR = new GeneratorPackage('@webpieces/openapi-generator', 'wp-openapi', '0.4.812');\n\n/** `wp-docs-site --spec/--prose/--out`: 0.4.807 is the first release carrying the docs site (#1007). */\nexport const DOCS_SITE = new GeneratorPackage('@webpieces/docs-site', 'wp-docs-site', '0.4.807');\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ConsumerBin } from './consumer-bin-resolver';
|
|
2
|
+
/** One finished generator process. Data-only. */
|
|
3
|
+
export declare class GeneratorRun {
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
/** stdout and stderr, in that order — the generator reports its refusals on stdout. */
|
|
6
|
+
readonly output: string;
|
|
7
|
+
constructor(exitCode: number,
|
|
8
|
+
/** stdout and stderr, in that order — the generator reports its refusals on stdout. */
|
|
9
|
+
output: string);
|
|
10
|
+
get ok(): boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs a resolved {@link ConsumerBin} with the node that is running US, so the generator gets the
|
|
14
|
+
* consumer's `node_modules` (it lives there) without depending on a `.bin` shim or on `PATH`.
|
|
15
|
+
*
|
|
16
|
+
* It REPORTS the outcome rather than throwing on a non-zero exit, so the executor that ran it decides
|
|
17
|
+
* how to word the refusal (it names the manifest or the document it was rendering).
|
|
18
|
+
*/
|
|
19
|
+
export declare class GeneratorRunner {
|
|
20
|
+
run(bin: ConsumerBin, args: readonly string[], cwd: string): GeneratorRun;
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GeneratorRunner = exports.GeneratorRun = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
/** One finished generator process. Data-only. */
|
|
6
|
+
class GeneratorRun {
|
|
7
|
+
exitCode;
|
|
8
|
+
output;
|
|
9
|
+
constructor(exitCode,
|
|
10
|
+
/** stdout and stderr, in that order — the generator reports its refusals on stdout. */
|
|
11
|
+
output) {
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
this.output = output;
|
|
14
|
+
}
|
|
15
|
+
get ok() {
|
|
16
|
+
return this.exitCode === 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.GeneratorRun = GeneratorRun;
|
|
20
|
+
/**
|
|
21
|
+
* Runs a resolved {@link ConsumerBin} with the node that is running US, so the generator gets the
|
|
22
|
+
* consumer's `node_modules` (it lives there) without depending on a `.bin` shim or on `PATH`.
|
|
23
|
+
*
|
|
24
|
+
* It REPORTS the outcome rather than throwing on a non-zero exit, so the executor that ran it decides
|
|
25
|
+
* how to word the refusal (it names the manifest or the document it was rendering).
|
|
26
|
+
*/
|
|
27
|
+
class GeneratorRunner {
|
|
28
|
+
run(bin, args, cwd) {
|
|
29
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [bin.binPath, ...args], { cwd, encoding: 'utf8' });
|
|
30
|
+
const output = [result.stdout ?? '', result.stderr ?? '', result.error?.message ?? '']
|
|
31
|
+
.filter((part) => part.trim() !== '')
|
|
32
|
+
.join('\n')
|
|
33
|
+
.trim();
|
|
34
|
+
return new GeneratorRun(result.status ?? 1, output);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.GeneratorRunner = GeneratorRunner;
|
|
38
|
+
//# sourceMappingURL=generator-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator-runner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/generator-runner.ts"],"names":[],"mappings":";;;AAAA,iDAA0C;AAG1C,iDAAiD;AACjD,MAAa,YAAY;IAER;IAEA;IAHb,YACa,QAAgB;IACzB,uFAAuF;IAC9E,MAAc;QAFd,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,WAAM,GAAN,MAAM,CAAQ;IACxB,CAAC;IAEJ,IAAI,EAAE;QACF,OAAO,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;IAC/B,CAAC;CACJ;AAVD,oCAUC;AAED;;;;;;GAMG;AACH,MAAa,eAAe;IACxB,GAAG,CAAC,GAAgB,EAAE,IAAuB,EAAE,GAAW;QACtD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9F,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,EAAE,CAAC;aACjF,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;aAC5C,IAAI,CAAC,IAAI,CAAC;aACV,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACxD,CAAC;CACJ;AATD,0CASC","sourcesContent":["import { spawnSync } from 'child_process';\nimport { ConsumerBin } from './consumer-bin-resolver';\n\n/** One finished generator process. Data-only. */\nexport class GeneratorRun {\n constructor(\n readonly exitCode: number,\n /** stdout and stderr, in that order — the generator reports its refusals on stdout. */\n readonly output: string,\n ) {}\n\n get ok(): boolean {\n return this.exitCode === 0;\n }\n}\n\n/**\n * Runs a resolved {@link ConsumerBin} with the node that is running US, so the generator gets the\n * consumer's `node_modules` (it lives there) without depending on a `.bin` shim or on `PATH`.\n *\n * It REPORTS the outcome rather than throwing on a non-zero exit, so the executor that ran it decides\n * how to word the refusal (it names the manifest or the document it was rendering).\n */\nexport class GeneratorRunner {\n run(bin: ConsumerBin, args: readonly string[], cwd: string): GeneratorRun {\n const result = spawnSync(process.execPath, [bin.binPath, ...args], { cwd, encoding: 'utf8' });\n const output = [result.stdout ?? '', result.stderr ?? '', result.error?.message ?? '']\n .filter((part: string) => part.trim() !== '')\n .join('\\n')\n .trim();\n return new GeneratorRun(result.status ?? 1, output);\n }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 documents' directory is the `outputPath` of the target `openapi-generate` dependsOn — the api
|
|
8
|
+
* library's `compile` (tsc) step, which writes the directory the package is packed from, so the
|
|
9
|
+
* documents ship INSIDE the published package. It is ASKED of nx through `GeneratedApiDocsLayout`
|
|
10
|
+
* (`@webpieces/core-util`), the same lookup `McpToolCatalog.fromPackages` reads with, and never
|
|
11
|
+
* assumed: this repo builds into a workspace-root `dist/apps/...`, another consumer builds into a
|
|
12
|
+
* project-local `<project>/dist`, and hardcoding either one — or the target's NAME — generates the
|
|
13
|
+
* document somewhere the package is not packed from;
|
|
14
|
+
* - the ordering and the cache are the target's own `dependsOn` and `outputs`, which the executor
|
|
15
|
+
* checks rather than trusts, because both failures are silent: without the `dependsOn` edge, tsc's
|
|
16
|
+
* clean of `outputPath` races the write (the document vanishes and a dependent test dies on ENOENT,
|
|
17
|
+
* intermittently, only in CI), and `outputs` that miss a written file make every cache hit restore a
|
|
18
|
+
* package without that file.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExecutorContext, TargetConfiguration } from '@nx/devkit';
|
|
21
|
+
export declare class GeneratorTarget {
|
|
22
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
23
|
+
readonly ruleName: string;
|
|
24
|
+
readonly workspaceRoot: string;
|
|
25
|
+
readonly projectName: string;
|
|
26
|
+
/** Workspace-relative. */
|
|
27
|
+
readonly projectRoot: string;
|
|
28
|
+
readonly targetName: string;
|
|
29
|
+
readonly target: TargetConfiguration;
|
|
30
|
+
readonly targets: Record<string, TargetConfiguration>;
|
|
31
|
+
constructor(
|
|
32
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
33
|
+
ruleName: string, workspaceRoot: string, projectName: string,
|
|
34
|
+
/** Workspace-relative. */
|
|
35
|
+
projectRoot: string, targetName: string, target: TargetConfiguration, targets: Record<string, TargetConfiguration>);
|
|
36
|
+
static of(ruleName: string, context: ExecutorContext): GeneratorTarget;
|
|
37
|
+
/**
|
|
38
|
+
* Absolute path to the directory the documents live in: the `outputPath` of the target
|
|
39
|
+
* `openapi-generate` dependsOn — the directory the package is packed from.
|
|
40
|
+
*/
|
|
41
|
+
documentsDir(): string;
|
|
42
|
+
/** Absolute `<projectRoot>/<dir>`, refusing anything that is not strictly inside the project. */
|
|
43
|
+
insideProject(dir: string, optionName: string): string;
|
|
44
|
+
/** Refuse unless this target `dependsOn` the sibling `required` target. */
|
|
45
|
+
assertDependsOn(required: string): void;
|
|
46
|
+
/** Refuse unless every written file is covered by the target's declared `outputs`, so a cache hit restores it. */
|
|
47
|
+
assertOutputsCover(writtenAbs: readonly string[]): void;
|
|
48
|
+
/** A workspace-relative path the consumer stated, from `options`, required and never defaulted. */
|
|
49
|
+
requiredOption(value: string | undefined, name: string): string;
|
|
50
|
+
private namesSibling;
|
|
51
|
+
/** nx's own tokens, so a path written the way nx accepts it resolves the way nx resolves it. */
|
|
52
|
+
private interpolate;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate into a STAGING directory, then publish into the output directory — so the executor knows
|
|
56
|
+
* exactly which files the run produced (for {@link GeneratorTarget.assertOutputsCover}) without
|
|
57
|
+
* parsing a generator's console output, and a failed run leaves the output directory untouched.
|
|
58
|
+
*/
|
|
59
|
+
export declare class StagedOutput {
|
|
60
|
+
publish(stagingDir: string, destDir: string): string[];
|
|
61
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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 documents' directory is the `outputPath` of the target `openapi-generate` dependsOn — the api
|
|
9
|
+
* library's `compile` (tsc) step, which writes the directory the package is packed from, so the
|
|
10
|
+
* documents ship INSIDE the published package. It is ASKED of nx through `GeneratedApiDocsLayout`
|
|
11
|
+
* (`@webpieces/core-util`), the same lookup `McpToolCatalog.fromPackages` reads with, and never
|
|
12
|
+
* assumed: this repo builds into a workspace-root `dist/apps/...`, another consumer builds into a
|
|
13
|
+
* project-local `<project>/dist`, and hardcoding either one — or the target's NAME — generates the
|
|
14
|
+
* document somewhere the package is not packed from;
|
|
15
|
+
* - the ordering and the cache are the target's own `dependsOn` and `outputs`, which the executor
|
|
16
|
+
* checks rather than trusts, because both failures are silent: without the `dependsOn` edge, tsc's
|
|
17
|
+
* clean of `outputPath` races the write (the document vanishes and a dependent test dies on ENOENT,
|
|
18
|
+
* intermittently, only in CI), and `outputs` that miss a written file make every cache hit restore a
|
|
19
|
+
* package without that file.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.StagedOutput = exports.GeneratorTarget = void 0;
|
|
23
|
+
const tslib_1 = require("tslib");
|
|
24
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
25
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
26
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
27
|
+
const path = tslib_1.__importStar(require("path"));
|
|
28
|
+
class GeneratorTarget {
|
|
29
|
+
ruleName;
|
|
30
|
+
workspaceRoot;
|
|
31
|
+
projectName;
|
|
32
|
+
projectRoot;
|
|
33
|
+
targetName;
|
|
34
|
+
target;
|
|
35
|
+
targets;
|
|
36
|
+
constructor(
|
|
37
|
+
/** The executor's name, which is what every refusal is reported under. */
|
|
38
|
+
ruleName, workspaceRoot, projectName,
|
|
39
|
+
/** Workspace-relative. */
|
|
40
|
+
projectRoot, targetName, target, targets) {
|
|
41
|
+
this.ruleName = ruleName;
|
|
42
|
+
this.workspaceRoot = workspaceRoot;
|
|
43
|
+
this.projectName = projectName;
|
|
44
|
+
this.projectRoot = projectRoot;
|
|
45
|
+
this.targetName = targetName;
|
|
46
|
+
this.target = target;
|
|
47
|
+
this.targets = targets;
|
|
48
|
+
}
|
|
49
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
50
|
+
static of(ruleName, context) {
|
|
51
|
+
const projectName = context.projectName ?? '';
|
|
52
|
+
const project = context.projectsConfigurations?.projects[projectName];
|
|
53
|
+
const targetName = context.targetName ?? '';
|
|
54
|
+
const target = project?.targets?.[targetName];
|
|
55
|
+
if (project === undefined || target === undefined) {
|
|
56
|
+
// nx hands every executor its own project and target; their absence is our bug, not the consumer's.
|
|
57
|
+
throw new Error(`${ruleName}: nx did not describe project '${projectName}' target '${targetName}'`);
|
|
58
|
+
}
|
|
59
|
+
return new GeneratorTarget(ruleName, context.root, projectName, project.root, targetName, target, project.targets ?? {});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Absolute path to the directory the documents live in: the `outputPath` of the target
|
|
63
|
+
* `openapi-generate` dependsOn — the directory the package is packed from.
|
|
64
|
+
*/
|
|
65
|
+
documentsDir() {
|
|
66
|
+
const lookup = new core_util_1.GeneratedApiDocsLayout(this.projectRoot, this.projectName, this.targets).outputTarget();
|
|
67
|
+
if (lookup.found === undefined) {
|
|
68
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${lookup.problem.problem} The documents go into the build's own output directory — the ` +
|
|
69
|
+
'one the package is packed from — and are never assumed to be ./dist.', undefined, undefined, [new rules_config_1.Option(lookup.problem.cure, true)]);
|
|
70
|
+
}
|
|
71
|
+
return path.resolve(this.workspaceRoot, lookup.found.outputPath);
|
|
72
|
+
}
|
|
73
|
+
/** Absolute `<projectRoot>/<dir>`, refusing anything that is not strictly inside the project. */
|
|
74
|
+
insideProject(dir, optionName) {
|
|
75
|
+
const projectAbs = path.resolve(this.workspaceRoot, this.projectRoot);
|
|
76
|
+
const resolved = path.resolve(projectAbs, dir);
|
|
77
|
+
const relative = path.relative(projectAbs, resolved);
|
|
78
|
+
if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative))
|
|
79
|
+
return resolved;
|
|
80
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} options.${optionName} is '${dir}', which is not a directory ` +
|
|
81
|
+
`strictly inside ${this.projectRoot}. It is emptied and rewritten on every run, so it may only ` +
|
|
82
|
+
'name a directory of the project\'s own.', undefined, undefined, [new rules_config_1.Option(`Set options.${optionName} to a project-relative directory, e.g. "generated-docs"`, true)]);
|
|
83
|
+
}
|
|
84
|
+
/** Refuse unless this target `dependsOn` the sibling `required` target. */
|
|
85
|
+
assertDependsOn(required) {
|
|
86
|
+
const entries = this.target.dependsOn ?? [];
|
|
87
|
+
if (entries.some((entry) => this.namesSibling(entry, required)))
|
|
88
|
+
return;
|
|
89
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} does not declare dependsOn "${required}". It reads what ` +
|
|
90
|
+
`${required} writes, so without that edge nx may run it before or alongside ${required} and ` +
|
|
91
|
+
`render a stale or missing document. That fails intermittently and mostly in CI, which is ` +
|
|
92
|
+
`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)]);
|
|
93
|
+
}
|
|
94
|
+
/** Refuse unless every written file is covered by the target's declared `outputs`, so a cache hit restores it. */
|
|
95
|
+
assertOutputsCover(writtenAbs) {
|
|
96
|
+
const patterns = (this.target.outputs ?? []).map((output) => this.interpolate(output));
|
|
97
|
+
const uncovered = writtenAbs
|
|
98
|
+
.map((file) => path.relative(this.workspaceRoot, file).split(path.sep).join('/'))
|
|
99
|
+
.filter((file) => !(0, rules_config_1.matchesAnyGlob)(file, patterns));
|
|
100
|
+
if (uncovered.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
throw new rules_config_1.RuleFailError(this.ruleName, `${this.projectName}:${this.targetName} wrote files its declared outputs do not cover, so a ` +
|
|
103
|
+
`cache hit would restore a package WITHOUT them:\n` +
|
|
104
|
+
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, ` +
|
|
105
|
+
`e.g. "{workspaceRoot}/${path.posix.dirname(uncovered[0])}/<the files>"`, true)]);
|
|
106
|
+
}
|
|
107
|
+
/** A workspace-relative path the consumer stated, from `options`, required and never defaulted. */
|
|
108
|
+
requiredOption(value, name) {
|
|
109
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
110
|
+
return value;
|
|
111
|
+
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)]);
|
|
112
|
+
}
|
|
113
|
+
namesSibling(entry, required) {
|
|
114
|
+
if (typeof entry === 'string')
|
|
115
|
+
return entry === required;
|
|
116
|
+
const projects = entry.projects;
|
|
117
|
+
const self = projects === undefined || projects === 'self' ||
|
|
118
|
+
(Array.isArray(projects) && projects.length === 1 && projects[0] === this.projectName);
|
|
119
|
+
return entry.target === required && entry.dependencies !== true && self;
|
|
120
|
+
}
|
|
121
|
+
/** nx's own tokens, so a path written the way nx accepts it resolves the way nx resolves it. */
|
|
122
|
+
interpolate(text) {
|
|
123
|
+
return text
|
|
124
|
+
.replace(/\{workspaceRoot\}\/?/g, '')
|
|
125
|
+
.replace(/\{projectRoot\}/g, this.projectRoot)
|
|
126
|
+
.replace(/\{projectName\}/g, this.projectName)
|
|
127
|
+
.replace(/\{options\.([A-Za-z0-9_]+)\}/g, (whole, key) => {
|
|
128
|
+
const value = this.target.options?.[key];
|
|
129
|
+
return typeof value === 'string' ? value : whole;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.GeneratorTarget = GeneratorTarget;
|
|
134
|
+
/**
|
|
135
|
+
* Generate into a STAGING directory, then publish into the output directory — so the executor knows
|
|
136
|
+
* exactly which files the run produced (for {@link GeneratorTarget.assertOutputsCover}) without
|
|
137
|
+
* parsing a generator's console output, and a failed run leaves the output directory untouched.
|
|
138
|
+
*/
|
|
139
|
+
class StagedOutput {
|
|
140
|
+
publish(stagingDir, destDir) {
|
|
141
|
+
const written = [];
|
|
142
|
+
const copy = (from, to) => {
|
|
143
|
+
fs.mkdirSync(to, { recursive: true });
|
|
144
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
145
|
+
const source = path.join(from, entry.name);
|
|
146
|
+
const target = path.join(to, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
copy(source, target);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
fs.copyFileSync(source, target);
|
|
152
|
+
written.push(target);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
copy(stagingDir, destDir);
|
|
157
|
+
return written.sort();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.StagedOutput = StagedOutput;
|
|
161
|
+
//# sourceMappingURL=generator-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator-target.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-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 documents' directory is the `outputPath` of the target `openapi-generate` dependsOn — the api\n * library's `compile` (tsc) step, which writes the directory the package is packed from, so the\n * documents ship INSIDE the published package. It is ASKED of nx through `GeneratedApiDocsLayout`\n * (`@webpieces/core-util`), the same lookup `McpToolCatalog.fromPackages` reads with, and never\n * assumed: this repo builds into a workspace-root `dist/apps/...`, another consumer builds into a\n * project-local `<project>/dist`, and hardcoding either one — or the target's NAME — 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: without the `dependsOn` edge, tsc's\n * clean of `outputPath` races the write (the document vanishes and a dependent test dies on ENOENT,\n * intermittently, only in CI), and `outputs` that miss a written file make every cache hit restore a\n * package without that file.\n */\n\nimport type { ExecutorContext, TargetConfiguration, TargetDependencyConfig } from '@nx/devkit';\nimport { GeneratedApiDocsLayout } from '@webpieces/core-util';\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 targets: Record<string, TargetConfiguration>,\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 ?? {});\n }\n\n /**\n * Absolute path to the directory the documents live in: the `outputPath` of the target\n * `openapi-generate` dependsOn — the directory the package is packed from.\n */\n documentsDir(): string {\n const lookup = new GeneratedApiDocsLayout(this.projectRoot, this.projectName, this.targets).outputTarget();\n if (lookup.found === undefined) {\n throw new RuleFailError(\n this.ruleName,\n `${lookup.problem!.problem} The documents go into the build's own output directory — the ` +\n 'one the package is packed from — and are never assumed to be ./dist.',\n undefined,\n undefined,\n [new Option(lookup.problem!.cure, true)],\n );\n }\n return path.resolve(this.workspaceRoot, lookup.found.outputPath);\n }\n\n /** Absolute `<projectRoot>/<dir>`, refusing anything that is not strictly inside the project. */\n insideProject(dir: string, optionName: string): string {\n const projectAbs = path.resolve(this.workspaceRoot, this.projectRoot);\n const resolved = path.resolve(projectAbs, dir);\n const relative = path.relative(projectAbs, resolved);\n if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)) return resolved;\n throw new RuleFailError(\n this.ruleName,\n `${this.projectName}:${this.targetName} options.${optionName} is '${dir}', which is not a directory ` +\n `strictly inside ${this.projectRoot}. It is emptied and rewritten on every run, so it may only ` +\n 'name a directory of the project\\'s own.',\n undefined,\n undefined,\n [new Option(`Set options.${optionName} to a project-relative directory, e.g. \"generated-docs\"`, true)],\n );\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 reads what ` +\n `${required} writes, so without that edge nx may run it before or alongside ${required} and ` +\n `render a stale or missing document. That fails intermittently and mostly in CI, which is ` +\n `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"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Everything a JSON document can hold — a parsed OpenAPI document or package.json is exactly this. */
|
|
2
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
|
|
3
|
+
/** The object case of {@link JsonValue}, named so the recursive alias can refer to it. */
|
|
4
|
+
export type JsonObject = {
|
|
5
|
+
[key: string]: JsonValue;
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-value.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/json-value.ts"],"names":[],"mappings":"","sourcesContent":["/** Everything a JSON document can hold — a parsed OpenAPI document or package.json is exactly this. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;\n\n/** The object case of {@link JsonValue}, named so the recursive alias can refer to it. */\nexport type JsonObject = { [key: string]: JsonValue };\n"]}
|
package/src/plugin.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* This plugin automatically creates targets for:
|
|
5
5
|
* 1. Workspace-level architecture validation (generate, visualize, validate-*)
|
|
6
6
|
* 2. Per-project circular dependency checking
|
|
7
|
+
* 3. Per-project API documents, opted into with a TAG: `generate:openapi` infers `openapi-generate`,
|
|
8
|
+
* `generate:docs-site` infers `openapi-generate` + `docs-generate` (see generate-targets.ts)
|
|
7
9
|
*
|
|
8
10
|
* Install with: nx add @webpieces/nx-webpieces-rules
|
|
9
11
|
*
|