@arc-e-tect/api-only-publisher 0.7.0 → 0.7.1
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 +32 -2
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/init-config.js +115 -0
- package/src/init.js +43 -7
package/README.adoc
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:source-highlighter: rouge
|
|
7
7
|
// The released version of this component: its snippets use it. The release
|
|
8
8
|
// workflow updates it; do not change it by hand.
|
|
9
|
-
:api-only-publisher-version: 0.7.
|
|
9
|
+
:api-only-publisher-version: 0.7.1
|
|
10
10
|
|
|
11
11
|
image:https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/actions/workflows/nvd-cache-refresh.yml/badge.svg[Vulnerability Scan,link=https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/actions/workflows/nvd-cache-refresh.yml]
|
|
12
12
|
image:https://img.shields.io/npm/v/@arc-e-tect/api-only-publisher[npm,link=https://www.npmjs.com/package/@arc-e-tect/api-only-publisher]
|
|
@@ -270,11 +270,41 @@ What it reports::
|
|
|
270
270
|
[horizontal]
|
|
271
271
|
`created`:: The file was not there, and is now.
|
|
272
272
|
`identical`:: The file was there, and matches what `init` would write, ignoring line endings and final newlines.
|
|
273
|
+
`updated`:: `apionly.yaml` was there, and lacked a kind's configuration; it does now.
|
|
274
|
+
The message names every slot added -- see <<init-additive,Adding a kind to an existing library §>>.
|
|
273
275
|
`differs`:: The file was there, and does not match: where the library has diverged from the scaffold, usually on purpose.
|
|
274
276
|
It is left alone.
|
|
275
277
|
`overwrote`:: The file differed, and `--force` replaced it.
|
|
276
278
|
+
|
|
277
|
-
At a terminal, `--force` first lists the files that differ and asks once whether to overwrite them; without a terminal, or with `--yes`, it overwrites them without asking.
|
|
279
|
+
At a terminal, `--force` first lists the files that still differ -- after any additive update apionly.yaml already got -- and asks once whether to overwrite them; without a terminal, or with `--yes`, it overwrites them without asking.
|
|
280
|
+
|
|
281
|
+
[#init-additive]
|
|
282
|
+
=== Adding a kind to an existing library
|
|
283
|
+
|
|
284
|
+
Running `init` again, with a kind the library did not have, is how a library grows: the fragments the new kind needs, and the configuration that makes the build see them.
|
|
285
|
+
|
|
286
|
+
*Files* stay create-only, as everywhere else in `init`: a fragment not written before is written now; one already there, however it now reads, is left alone -- even where the new kind changes what the scaffold would write for it, such as `specs/openapi/paths/example/ExamplesV1.yaml` once an AsyncAPI event comes to share its schema.
|
|
287
|
+
|
|
288
|
+
*Configuration* is additive: whichever of `sources.<kind>`, `defaults.<kind>`, the kind's pin in `toolchain`, and `targets.<target>.<kind>` `apionly.yaml` lacks is added to it, in the position a library scaffolded with both kinds from the start would have it in.
|
|
289
|
+
Every value, key and comment already in the file survives untouched, and a slot that is already there, however it reads, is never touched either -- not even to add something missing from *inside* it: a `defaults.openapi` with no `lint` is a choice, and `init` leaves it that way.
|
|
290
|
+
|
|
291
|
+
[source,console]
|
|
292
|
+
----
|
|
293
|
+
$ api-only-publisher init . --yes --openapi --target orders
|
|
294
|
+
created apionly.yaml
|
|
295
|
+
created specs/openapi/...
|
|
296
|
+
|
|
297
|
+
$ api-only-publisher init . --yes --openapi --asyncapi --target orders
|
|
298
|
+
created specs/asyncapi/...
|
|
299
|
+
updated apionly.yaml (added sources.asyncapi, defaults.asyncapi, toolchain.asyncapi, targets.orders.asyncapi)
|
|
300
|
+
differs specs/openapi/paths/example/ExamplesV1.yaml (left alone; --force overwrites)
|
|
301
|
+
----
|
|
302
|
+
|
|
303
|
+
Nothing is asked before completing `apionly.yaml`: unlike `--force`, an additive update never discards anything, so there is nothing to confirm.
|
|
304
|
+
Run `init` again with the same kinds, and it reports nothing left to add.
|
|
305
|
+
|
|
306
|
+
`--force` is the other tool for an apionly.yaml that differs, and it does the opposite of the additive update: it reinitialises the file wholesale, exactly as a first run with these values would write it -- any target or key it holds that these values would not produce is gone, additive or not.
|
|
307
|
+
Use it to discard a customisation on purpose; use a plain `init` to add a kind without touching anything else.
|
|
278
308
|
|
|
279
309
|
[#what-build-does]
|
|
280
310
|
== What `build` does
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -168,7 +168,7 @@ async function runInit(options, positional, io, log) {
|
|
|
168
168
|
if (terminal && force) {
|
|
169
169
|
// Asked, --force means "after showing me": it overwrites what differs only
|
|
170
170
|
// once the list has been seen and agreed to.
|
|
171
|
-
const differing = plan(dir, scaffold(values)).filter((entry) => entry.status === "differs");
|
|
171
|
+
const differing = plan(dir, scaffold(values), values).filter((entry) => entry.status === "differs");
|
|
172
172
|
if (differing.length > 0) {
|
|
173
173
|
output.write(`These files differ from the scaffold:\n${differing.map((d) => ` ${d.rel}\n`).join("")}`);
|
|
174
174
|
const answer = await terminal.question(`Overwrite these ${differing.length} file(s)? [y/N] `);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Additively completing an apionly.yaml that already exists.
|
|
4
|
+
//
|
|
5
|
+
// `init` creates a file that is not there; this is the same rule applied to
|
|
6
|
+
// configuration. When a kind's support is missing from an existing apionly.yaml --
|
|
7
|
+
// in whole, because the library never had it, or in part, because a run was
|
|
8
|
+
// interrupted between writing the fragments and writing the config for them -- the
|
|
9
|
+
// slots it needs are added. What is already there, however it reads, is never
|
|
10
|
+
// touched: a `defaults.openapi` block with no `lint` is a choice, not an absence.
|
|
11
|
+
//
|
|
12
|
+
// The unit added is a *slot*: one of the handful of places a kind's support in
|
|
13
|
+
// apionly.yaml lives -- never a key inside one, which is why `slots()` below stops
|
|
14
|
+
// at the map a kind occupies and goes no further into it.
|
|
15
|
+
|
|
16
|
+
const YAML = require("yaml");
|
|
17
|
+
|
|
18
|
+
/** Where a kind's tool is pinned in `toolchain`; the two kinds do not share a key name. */
|
|
19
|
+
const TOOLCHAIN_KEY = { openapi: "redocly", asyncapi: "asyncapi" };
|
|
20
|
+
const TOOLCHAIN_PIN = { openapi: "@redocly/cli@2.52.0", asyncapi: "@asyncapi/cli@6.0.2" };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The slots scaffold(v) would fill for one kind, in the order config(v) declares them,
|
|
24
|
+
* each naming the sibling keys -- in the order to prefer -- a slot goes before when one
|
|
25
|
+
* of them is already there. `openapi` always precedes `asyncapi`, in every section that
|
|
26
|
+
* holds both; `asyncapi` precedes `placeholders`, the one section-closing key that is
|
|
27
|
+
* always there.
|
|
28
|
+
*/
|
|
29
|
+
function slotsFor(kind, v) {
|
|
30
|
+
const target = v.target;
|
|
31
|
+
if (kind === "openapi") {
|
|
32
|
+
return [
|
|
33
|
+
{ path: ["sources"], key: "openapi", value: "openapi", before: ["asyncapi"] },
|
|
34
|
+
{
|
|
35
|
+
path: ["defaults"], key: "openapi", before: ["asyncapi", "placeholders"],
|
|
36
|
+
value: { lint: ".redocly.yaml", outputName: "openapi.yaml" },
|
|
37
|
+
},
|
|
38
|
+
{ path: ["toolchain"], key: "redocly", value: TOOLCHAIN_PIN.openapi, before: ["asyncapi"] },
|
|
39
|
+
{
|
|
40
|
+
path: ["targets", target], key: "openapi", before: ["asyncapi"],
|
|
41
|
+
value: { bundle: `bundles/${target}_openapi_structure.yaml` },
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
{ path: ["sources"], key: "asyncapi", value: "asyncapi", before: [] },
|
|
47
|
+
{ path: ["defaults"], key: "asyncapi", value: { outputName: "asyncapi.yaml" }, before: ["placeholders"] },
|
|
48
|
+
{ path: ["toolchain"], key: "asyncapi", value: TOOLCHAIN_PIN.asyncapi, before: [] },
|
|
49
|
+
{
|
|
50
|
+
path: ["targets", target], key: "asyncapi", before: [],
|
|
51
|
+
value: { bundle: `bundles/${target}_asyncapi_structure.yaml` },
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A slot's YAML path, dot-separated, as the report names it. */
|
|
57
|
+
function slotName(slot) {
|
|
58
|
+
return [...slot.path, slot.key].join(".");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sets `key: value` in the map at `parentPath`, before the first of `before` that is
|
|
63
|
+
* already a sibling there, or at the end when none is. Creates `parentPath` itself,
|
|
64
|
+
* as `Document#setIn` does, when it is not there yet -- there being nothing in a map
|
|
65
|
+
* that does not exist yet to come before.
|
|
66
|
+
*/
|
|
67
|
+
function place(doc, parentPath, key, value, before) {
|
|
68
|
+
const parent = doc.getIn(parentPath, true);
|
|
69
|
+
if (YAML.isMap(parent)) {
|
|
70
|
+
for (const candidate of before) {
|
|
71
|
+
const sibling = parent.items.find((pair) => String(pair.key) === candidate);
|
|
72
|
+
if (sibling) {
|
|
73
|
+
parent.items.splice(parent.items.indexOf(sibling), 0, doc.createPair(key, value));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
doc.setIn([...parentPath, key], value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Adds to `text` -- an existing apionly.yaml -- whatever configuration slot
|
|
83
|
+
* scaffold(values) would write and this file lacks. Every value, key and comment
|
|
84
|
+
* already there survives unchanged; a slot already present, however it reads, is
|
|
85
|
+
* never touched, and neither is anything inside it.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} text apionly.yaml, as it is on disk
|
|
88
|
+
* @param {object} v the values scaffold(values) was given, resolved: every value
|
|
89
|
+
* DEFAULTS has, `values`' own where it gives one -- the shape scaffold() itself
|
|
90
|
+
* works from, so a caller that already has that object need not rebuild it
|
|
91
|
+
* @returns {{text: string, added: string[]}} the edited text, and the slots added, in
|
|
92
|
+
* the order scaffold() declares them; `added` is empty, and `text` is `text`
|
|
93
|
+
* itself, when nothing was missing.
|
|
94
|
+
*/
|
|
95
|
+
function addMissingConfig(text, v) {
|
|
96
|
+
const doc = YAML.parseDocument(text);
|
|
97
|
+
const added = [];
|
|
98
|
+
|
|
99
|
+
// Not every file that differs is a library with a kind missing -- one that does not
|
|
100
|
+
// even declare sources.root is not an apionly.yaml this can complete, whatever it
|
|
101
|
+
// holds instead; force, or a person, is what that file needs.
|
|
102
|
+
if (!doc.hasIn(["sources", "root"])) return { text, added };
|
|
103
|
+
|
|
104
|
+
for (const kind of ["openapi", "asyncapi"].filter((k) => v.kinds.includes(k))) {
|
|
105
|
+
for (const slot of slotsFor(kind, v)) {
|
|
106
|
+
if (doc.hasIn([...slot.path, slot.key])) continue;
|
|
107
|
+
place(doc, slot.path, slot.key, slot.value, slot.before);
|
|
108
|
+
added.push(slotName(slot));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return added.length === 0 ? { text, added } : { text: doc.toString(), added };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { addMissingConfig };
|
package/src/init.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const fs = require("fs");
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const YAML = require("yaml");
|
|
18
|
+
const { addMissingConfig } = require("./init-config");
|
|
18
19
|
|
|
19
20
|
/** Every value the scaffold is written from, as it is when nobody chooses otherwise. */
|
|
20
21
|
const DEFAULTS = Object.freeze({
|
|
@@ -307,17 +308,37 @@ function normalised(text) {
|
|
|
307
308
|
return text.replace(/\r\n?/g, "\n").replace(/\n+$/, "");
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
const CONFIG_FILE = "apionly.yaml";
|
|
312
|
+
|
|
310
313
|
/**
|
|
311
314
|
* What writing these files would do to each: create it, leave it because it is
|
|
312
315
|
* identical, or find it different from what would be written.
|
|
313
316
|
*
|
|
314
|
-
*
|
|
317
|
+
* apionly.yaml gets one more thing tried, ahead of "differs": whatever slot
|
|
318
|
+
* scaffold(values) would write and it lacks is added, in place, before the
|
|
319
|
+
* comparison that decides "identical" or "differs" -- so a file that is missing only
|
|
320
|
+
* a kind it never had is never reported as differing, and neither is one that is
|
|
321
|
+
* missing nothing at all. `values`, resolved as scaffold() resolves it, is what that
|
|
322
|
+
* completion is measured against; without it, apionly.yaml is compared as every
|
|
323
|
+
* other file is.
|
|
324
|
+
*
|
|
325
|
+
* @returns {{rel: string, status: "missing"|"identical"|"differs", added?: string[], text?: string}[]}
|
|
326
|
+
* `added` and `text` -- the completed content -- are there only for apionly.yaml,
|
|
327
|
+
* and only when something was missing from it.
|
|
315
328
|
*/
|
|
316
|
-
function plan(targetDir, files) {
|
|
329
|
+
function plan(targetDir, files, values) {
|
|
317
330
|
return Object.entries(files).map(([rel, content]) => {
|
|
318
331
|
const file = path.join(targetDir, rel);
|
|
319
332
|
if (!fs.existsSync(file)) return { rel, status: "missing" };
|
|
320
|
-
const
|
|
333
|
+
const onDisk = fs.readFileSync(file, "utf8");
|
|
334
|
+
if (rel === CONFIG_FILE && values) {
|
|
335
|
+
const completed = addMissingConfig(onDisk, { ...DEFAULTS, ...values });
|
|
336
|
+
if (completed.added.length > 0) {
|
|
337
|
+
const status = normalised(completed.text) === normalised(content) ? "identical" : "differs";
|
|
338
|
+
return { rel, status, added: completed.added, text: completed.text };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const same = normalised(onDisk) === normalised(content);
|
|
321
342
|
return { rel, status: same ? "identical" : "differs" };
|
|
322
343
|
});
|
|
323
344
|
}
|
|
@@ -327,13 +348,27 @@ function plan(targetDir, files) {
|
|
|
327
348
|
*
|
|
328
349
|
* A file that is not there is created. One that is there already is left alone,
|
|
329
350
|
* reported as identical to what would have been written or as differing from it;
|
|
330
|
-
* with `force`, one that differs is overwritten
|
|
351
|
+
* with `force`, one that differs is overwritten, wholesale, as if this were the
|
|
352
|
+
* first time -- apionly.yaml included, whatever else it declares.
|
|
353
|
+
*
|
|
354
|
+
* Short of `force`, apionly.yaml gets one more chance: whatever configuration a
|
|
355
|
+
* missing kind needs and it lacks is added to it, leaving every value, key and
|
|
356
|
+
* comment already there exactly as it was. A kind already declared, however it
|
|
357
|
+
* reads, is never touched -- what is already there is not changed, only what is
|
|
358
|
+
* not there is added.
|
|
331
359
|
*/
|
|
332
360
|
function init(targetDir, { values = DEFAULTS, force = false, log = () => {} } = {}) {
|
|
333
361
|
const files = scaffold(values);
|
|
334
|
-
const report = { created: [], overwritten: [], identical: [], differing: [] };
|
|
335
|
-
|
|
336
|
-
|
|
362
|
+
const report = { created: [], overwritten: [], identical: [], differing: [], updated: [] };
|
|
363
|
+
const updates = [];
|
|
364
|
+
|
|
365
|
+
for (const { rel, status, added, text } of plan(targetDir, files, values)) {
|
|
366
|
+
if (added && !force) {
|
|
367
|
+
fs.writeFileSync(path.join(targetDir, rel), text);
|
|
368
|
+
report.updated.push(rel);
|
|
369
|
+
updates.push({ rel, added });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
337
372
|
if (status === "identical") {
|
|
338
373
|
report.identical.push(rel);
|
|
339
374
|
continue;
|
|
@@ -350,6 +385,7 @@ function init(targetDir, { values = DEFAULTS, force = false, log = () => {} } =
|
|
|
350
385
|
|
|
351
386
|
for (const rel of report.created) log(` created ${rel}`);
|
|
352
387
|
for (const rel of report.overwritten) log(` overwrote ${rel}`);
|
|
388
|
+
for (const { rel, added } of updates) log(` updated ${rel} (added ${added.join(", ")})`);
|
|
353
389
|
for (const rel of report.identical) log(` identical ${rel}`);
|
|
354
390
|
for (const rel of report.differing) log(` differs ${rel} (left alone; --force overwrites)`);
|
|
355
391
|
return { ...report, skipped: [...report.identical, ...report.differing] };
|