@arc-e-tect/api-only-publisher 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +18 -0
- package/package.json +1 -1
- package/src/cli.js +42 -6
- package/src/config.js +16 -0
- package/src/index.js +1 -0
- package/src/unreferenced.js +53 -0
package/README.adoc
CHANGED
|
@@ -138,6 +138,7 @@ stands. This is how the *conventions* become reusable, as opposed to the code.
|
|
|
138
138
|
|
|
139
139
|
|`lint`
|
|
140
140
|
|Lint what is already built, without rebuilding, for fast local feedback.
|
|
141
|
+
Without `--target`, it also fails on a fragment no target reaches, as <<unreferenced,A fragment no target reaches fails the lint §>> describes.
|
|
141
142
|
|
|
142
143
|
|`targets`
|
|
143
144
|
|List the declared targets, what each one builds, and whether it is distributed.
|
|
@@ -420,6 +421,8 @@ build:
|
|
|
420
421
|
dist: dist
|
|
421
422
|
reports:
|
|
422
423
|
lint: build/reports/lint
|
|
424
|
+
lint:
|
|
425
|
+
unreferenced: error # a fragment no target reaches: error, warn or off
|
|
423
426
|
|
|
424
427
|
toolchain:
|
|
425
428
|
redocly: "@redocly/cli@2.52.0"
|
|
@@ -473,6 +476,21 @@ from the directory it was told to use, so a snippet one level up was invisible
|
|
|
473
476
|
and the workaround was to move the Markdown files. The search now starts at the
|
|
474
477
|
staged source root.
|
|
475
478
|
|
|
479
|
+
[[unreferenced]]
|
|
480
|
+
=== A fragment no target reaches fails the lint
|
|
481
|
+
|
|
482
|
+
A linter checks documents, and a fragment reaches a document only through a `$ref`.
|
|
483
|
+
A fragment that no target references is therefore never linted, however wrong it is, until the day a target starts to use it.
|
|
484
|
+
|
|
485
|
+
So `lint`, run without `--target`, also lists every YAML file under the source root that no target's closure reaches, in `build/reports/lint/unreferenced.txt`, and fails when there is one.
|
|
486
|
+
Every target counts, `publish: false` ones included, because they are linted too.
|
|
487
|
+
That reports a fragment that does not comply before anything uses it, and finds a definition left behind by a change that stopped using it.
|
|
488
|
+
|
|
489
|
+
`lint.unreferenced` in `apionly.yaml` sets what happens: `error` by default, `warn` to report without failing, or `off` not to look.
|
|
490
|
+
`lint --target` does not look, because an unreferenced fragment belongs to no target.
|
|
491
|
+
|
|
492
|
+
`lint` lints every document even when one of them fails, and names every failure at the end, so a single run reports all of them.
|
|
493
|
+
|
|
476
494
|
=== Version stamping edits one scalar
|
|
477
495
|
|
|
478
496
|
`info.version` is located structurally and that one scalar is spliced. It is
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const { split, SplitError } = require("./split");
|
|
|
15
15
|
const { publish, ChannelError } = require("./channels");
|
|
16
16
|
const { VersionError: PolicyError, describe } = require("./version-policy");
|
|
17
17
|
const { versionOf, BundleVersionError } = require("./bundle-version");
|
|
18
|
+
const { unreferenced } = require("./unreferenced");
|
|
18
19
|
|
|
19
20
|
const USAGE = `api-only-publisher -- build and distribute API description documents
|
|
20
21
|
|
|
@@ -162,26 +163,61 @@ async function main(argv) {
|
|
|
162
163
|
}
|
|
163
164
|
case "lint": {
|
|
164
165
|
// Lint without rebuilding, for fast local feedback on what is already
|
|
165
|
-
// in dist/.
|
|
166
|
+
// in dist/. Every selected document is linted even when one fails, so
|
|
167
|
+
// one run reports every failure rather than only the first.
|
|
166
168
|
const fs = require("fs");
|
|
167
169
|
const { lint } = require("./pipeline");
|
|
170
|
+
const failures = [];
|
|
168
171
|
let linted = 0;
|
|
169
172
|
for (const kind of ["openapi", "asyncapi"]) {
|
|
170
173
|
for (const target of config.targetsFor(kind)) {
|
|
171
174
|
if (targets && !targets.includes(target)) continue;
|
|
172
175
|
const file = path.join(config.distDir(target), config.outputName(kind));
|
|
173
176
|
if (!fs.existsSync(file)) {
|
|
174
|
-
|
|
177
|
+
failures.push(`${target} (${kind}): ${file} does not exist; run 'build' first`);
|
|
178
|
+
continue;
|
|
175
179
|
}
|
|
176
180
|
log(`=== ${target} (${kind}) ===`);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
try {
|
|
182
|
+
lint(config, kind, file, log, {
|
|
183
|
+
report: true,
|
|
184
|
+
reportFile: config.lintReport(target, kind),
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!(error instanceof BuildError)) throw error;
|
|
188
|
+
console.error(error.message);
|
|
189
|
+
failures.push(`${target} (${kind})`);
|
|
190
|
+
}
|
|
181
191
|
linted += 1;
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
194
|
log(`\nLinted ${linted} document(s).`);
|
|
195
|
+
|
|
196
|
+
// A fragment no target reaches is never linted, so it is looked for
|
|
197
|
+
// here -- only when every target is linted, since it belongs to none.
|
|
198
|
+
const mode = config.lintUnreferenced();
|
|
199
|
+
let orphaned = null;
|
|
200
|
+
if (!targets && mode !== "off") {
|
|
201
|
+
prepare(config);
|
|
202
|
+
const orphans = unreferenced(config);
|
|
203
|
+
const reportFile = config.unreferencedReport();
|
|
204
|
+
fs.mkdirSync(path.dirname(reportFile), { recursive: true });
|
|
205
|
+
fs.writeFileSync(reportFile, orphans.map((file) => `${file}\n`).join(""));
|
|
206
|
+
if (orphans.length > 0) {
|
|
207
|
+
orphaned =
|
|
208
|
+
`${orphans.length} fragment(s) not reachable from any target, so nothing lints them:\n` +
|
|
209
|
+
orphans.map((file) => ` ${file}`).join("\n") +
|
|
210
|
+
"\nReference each one from a target's bundle, or delete it.";
|
|
211
|
+
if (mode === "warn") log(orphaned);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const problems = [];
|
|
216
|
+
if (failures.length > 0) {
|
|
217
|
+
problems.push(`lint failed for ${failures.length} document(s): ${failures.join("; ")}`);
|
|
218
|
+
}
|
|
219
|
+
if (orphaned && mode === "error") problems.push(orphaned);
|
|
220
|
+
if (problems.length > 0) throw new BuildError(problems.join("\n\n"));
|
|
185
221
|
return 0;
|
|
186
222
|
}
|
|
187
223
|
case "closure": {
|
package/src/config.js
CHANGED
|
@@ -93,6 +93,7 @@ function load(configPath) {
|
|
|
93
93
|
toolchain: parsed.toolchain || {},
|
|
94
94
|
build: parsed.build || {},
|
|
95
95
|
reports: parsed.reports || {},
|
|
96
|
+
lint: parsed.lint || {},
|
|
96
97
|
distribution: parsed.distribution || null,
|
|
97
98
|
channels: parsed.channels || {},
|
|
98
99
|
targets,
|
|
@@ -126,6 +127,21 @@ function load(configPath) {
|
|
|
126
127
|
const reports = this.reports.lint || "build/reports/lint";
|
|
127
128
|
return path.resolve(this.root, reports, target, `${kind}.txt`);
|
|
128
129
|
},
|
|
130
|
+
// Where lint lists the fragments no target reaches.
|
|
131
|
+
unreferencedReport() {
|
|
132
|
+
const reports = this.reports.lint || "build/reports/lint";
|
|
133
|
+
return path.resolve(this.root, reports, "unreferenced.txt");
|
|
134
|
+
},
|
|
135
|
+
// What lint does about a fragment no target reaches: `error`, the default,
|
|
136
|
+
// fails the lint; `warn` reports it; `off` does not look.
|
|
137
|
+
lintUnreferenced() {
|
|
138
|
+
const configured = this.lint.unreferenced;
|
|
139
|
+
const mode = configured === undefined ? "error" : configured === false ? "off" : configured;
|
|
140
|
+
if (!["error", "warn", "off"].includes(mode)) {
|
|
141
|
+
throw new ConfigError(`lint.unreferenced must be error, warn or off, not ${JSON.stringify(configured)}`);
|
|
142
|
+
}
|
|
143
|
+
return mode;
|
|
144
|
+
},
|
|
129
145
|
tool(name) {
|
|
130
146
|
return requireString(this.toolchain[name], `toolchain.${name}`);
|
|
131
147
|
},
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Fragments no target reaches.
|
|
4
|
+
//
|
|
5
|
+
// A linter checks documents, and a fragment reaches a document only through a
|
|
6
|
+
// $ref. A fragment that no target references is therefore never linted, however
|
|
7
|
+
// wrong it is, until the day a target starts to use it. Looking for unreferenced
|
|
8
|
+
// fragments is how that is caught before then -- and how a definition left behind
|
|
9
|
+
// by a change that stopped using it is found at all.
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
|
|
14
|
+
const { forTargets } = require("./closure");
|
|
15
|
+
|
|
16
|
+
function yamlFiles(dir) {
|
|
17
|
+
const files = [];
|
|
18
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
19
|
+
const full = path.join(dir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) files.push(...yamlFiles(full));
|
|
21
|
+
else if (entry.isFile() && /\.ya?ml$/.test(entry.name)) files.push(full);
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const portable = (file) => file.split(path.sep).join("/");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every YAML file under the source root that no target's closure reaches,
|
|
30
|
+
* relative to the source root and sorted.
|
|
31
|
+
*
|
|
32
|
+
* Every target counts, `publish: false` ones included: a documentation view is
|
|
33
|
+
* linted like any other target, so whatever it reaches is linted too. The staged
|
|
34
|
+
* tree must already exist, as for forTargets.
|
|
35
|
+
*
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
function unreferenced(config) {
|
|
39
|
+
const reached = new Set();
|
|
40
|
+
for (const [, entry] of forTargets(config)) {
|
|
41
|
+
for (const [kind, files] of Object.entries(entry.byKind)) {
|
|
42
|
+
const stagingRoot = config.stagingRoot(kind);
|
|
43
|
+
for (const file of files) reached.add(portable(path.relative(stagingRoot, file)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const sourceRoot = config.sourceRoot();
|
|
47
|
+
return yamlFiles(sourceRoot)
|
|
48
|
+
.map((file) => portable(path.relative(sourceRoot, file)))
|
|
49
|
+
.filter((file) => !reached.has(file))
|
|
50
|
+
.sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { unreferenced };
|