@arc-e-tect/api-only-publisher 0.7.1 → 0.9.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 +215 -18
- package/package.json +1 -1
- package/src/aggregate.js +274 -50
- package/src/cli.js +101 -6
- package/src/closure.js +1 -1
- package/src/config-command.js +100 -0
- package/src/config.js +226 -7
- package/src/examples.js +172 -0
- package/src/init-config.js +27 -7
- package/src/init.js +10 -4
- package/src/pipeline.js +60 -13
- package/src/split.js +1 -1
package/src/init.js
CHANGED
|
@@ -322,17 +322,19 @@ const CONFIG_FILE = "apionly.yaml";
|
|
|
322
322
|
* completion is measured against; without it, apionly.yaml is compared as every
|
|
323
323
|
* other file is.
|
|
324
324
|
*
|
|
325
|
+
* @param {{paths: string, location: string}} [portfolio] the portfolio section to
|
|
326
|
+
* add when apionly.yaml lacks one; see addMissingConfig for what it does with it
|
|
325
327
|
* @returns {{rel: string, status: "missing"|"identical"|"differs", added?: string[], text?: string}[]}
|
|
326
328
|
* `added` and `text` -- the completed content -- are there only for apionly.yaml,
|
|
327
329
|
* and only when something was missing from it.
|
|
328
330
|
*/
|
|
329
|
-
function plan(targetDir, files, values) {
|
|
331
|
+
function plan(targetDir, files, values, portfolio) {
|
|
330
332
|
return Object.entries(files).map(([rel, content]) => {
|
|
331
333
|
const file = path.join(targetDir, rel);
|
|
332
334
|
if (!fs.existsSync(file)) return { rel, status: "missing" };
|
|
333
335
|
const onDisk = fs.readFileSync(file, "utf8");
|
|
334
336
|
if (rel === CONFIG_FILE && values) {
|
|
335
|
-
const completed = addMissingConfig(onDisk, { ...DEFAULTS, ...values });
|
|
337
|
+
const completed = addMissingConfig(onDisk, { ...DEFAULTS, ...values }, portfolio);
|
|
336
338
|
if (completed.added.length > 0) {
|
|
337
339
|
const status = normalised(completed.text) === normalised(content) ? "identical" : "differs";
|
|
338
340
|
return { rel, status, added: completed.added, text: completed.text };
|
|
@@ -356,13 +358,17 @@ function plan(targetDir, files, values) {
|
|
|
356
358
|
* comment already there exactly as it was. A kind already declared, however it
|
|
357
359
|
* reads, is never touched -- what is already there is not changed, only what is
|
|
358
360
|
* not there is added.
|
|
361
|
+
*
|
|
362
|
+
* `portfolio`, given, is added the same way, as one more slot: whether the
|
|
363
|
+
* library is about to have more than one target -- and so whether this is worth
|
|
364
|
+
* asking about at all -- is decided before init is called, not here.
|
|
359
365
|
*/
|
|
360
|
-
function init(targetDir, { values = DEFAULTS, force = false, log = () => {} } = {}) {
|
|
366
|
+
function init(targetDir, { values = DEFAULTS, force = false, log = () => {}, portfolio = null } = {}) {
|
|
361
367
|
const files = scaffold(values);
|
|
362
368
|
const report = { created: [], overwritten: [], identical: [], differing: [], updated: [] };
|
|
363
369
|
const updates = [];
|
|
364
370
|
|
|
365
|
-
for (const { rel, status, added, text } of plan(targetDir, files, values)) {
|
|
371
|
+
for (const { rel, status, added, text } of plan(targetDir, files, values, portfolio)) {
|
|
366
372
|
if (added && !force) {
|
|
367
373
|
fs.writeFileSync(path.join(targetDir, rel), text);
|
|
368
374
|
report.updated.push(rel);
|
package/src/pipeline.js
CHANGED
|
@@ -9,8 +9,11 @@ const YAML = require("yaml");
|
|
|
9
9
|
|
|
10
10
|
const { substituteFile } = require("./placeholders");
|
|
11
11
|
const { stampFile } = require("./version");
|
|
12
|
-
const { generateAsyncApi, isAggregate } = require("./aggregate");
|
|
12
|
+
const { generateAsyncApi, generateOpenApi, openapiPushDown, isAggregate } = require("./aggregate");
|
|
13
13
|
const { stampFiles, componentPaths, strayPaths, fragmentStamps, unresolvedStamps, FragmentPathError, KEY } = require("./fragment-paths");
|
|
14
|
+
const {
|
|
15
|
+
asyncapiOperationsWithoutExamples, openapiOperationsWithoutExamples, asyncapiMessage, openapiMessage,
|
|
16
|
+
} = require("./examples");
|
|
14
17
|
|
|
15
18
|
class BuildError extends Error {}
|
|
16
19
|
|
|
@@ -85,7 +88,7 @@ function substituteTree(config, kind, log) {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
function bundle(config, target, kind, outFile, log, { stagingRoot } = {}) {
|
|
88
|
-
const source = config.
|
|
91
|
+
const source = config.bundleRootPath(target, kind, stagingRoot);
|
|
89
92
|
if (!fs.existsSync(source)) {
|
|
90
93
|
throw new BuildError(`target '${target}': bundle root not found at ${source}`);
|
|
91
94
|
}
|
|
@@ -130,7 +133,7 @@ function bundleWithFragmentPaths(config, target, kind, outFile, log) {
|
|
|
130
133
|
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
131
134
|
};
|
|
132
135
|
|
|
133
|
-
log(`-- Bundling ${path.basename(config.
|
|
136
|
+
log(`-- Bundling ${path.basename(config.bundleRootPath(target, kind))} with ${KEY}`);
|
|
134
137
|
const discovered = componentPaths(pass("discover", null));
|
|
135
138
|
const fragments = new Set([...discovered.values()].filter(Boolean));
|
|
136
139
|
const document = pass("stamp", fragments);
|
|
@@ -169,7 +172,7 @@ function bundleInlinedWithFragmentPaths(config, target, kind, outFile, log) {
|
|
|
169
172
|
const root = path.join(scratch, "stamp");
|
|
170
173
|
fs.cpSync(config.stagingRoot(kind), root, { recursive: true });
|
|
171
174
|
|
|
172
|
-
const bundleRoot = path.relative(root, config.
|
|
175
|
+
const bundleRoot = path.relative(root, config.bundleRootPath(target, kind, root)).split(path.sep).join("/");
|
|
173
176
|
try {
|
|
174
177
|
stampFiles(root, { except: new Set([bundleRoot]) });
|
|
175
178
|
} catch (error) {
|
|
@@ -177,7 +180,7 @@ function bundleInlinedWithFragmentPaths(config, target, kind, outFile, log) {
|
|
|
177
180
|
throw error;
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
log(`-- Bundling ${path.basename(config.
|
|
183
|
+
log(`-- Bundling ${path.basename(config.bundleRootPath(target, kind))} with ${KEY}`);
|
|
181
184
|
const file = path.join(scratch, "stamp.yaml");
|
|
182
185
|
bundle(config, target, kind, file, () => {}, { stagingRoot: root });
|
|
183
186
|
const document = YAML.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -205,6 +208,33 @@ function lint(config, kind, file, log, { report = false, reportFile = null } = {
|
|
|
205
208
|
if (report && output.trim()) log(output.trimEnd());
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Reports operations of a built document whose example this kind's toolchain
|
|
213
|
+
* could use, and does not have -- separately from lint's own findings, since
|
|
214
|
+
* "your contract is wrong" and "your contract limits what you can do with it
|
|
215
|
+
* downstream" are different claims and read worse conflated.
|
|
216
|
+
*
|
|
217
|
+
* Governed by `lint.examples.<kind>`: `warn`, the default, logs each finding and
|
|
218
|
+
* continues; `error` logs them and fails the build; `off` does not look.
|
|
219
|
+
*/
|
|
220
|
+
function checkExamples(config, kind, file, log) {
|
|
221
|
+
const mode = config.lintExamples(kind);
|
|
222
|
+
if (mode === "off") return;
|
|
223
|
+
|
|
224
|
+
const document = YAML.parse(fs.readFileSync(file, "utf8"));
|
|
225
|
+
const findings = kind === "asyncapi"
|
|
226
|
+
? asyncapiOperationsWithoutExamples(document).map(asyncapiMessage)
|
|
227
|
+
: openapiOperationsWithoutExamples(document).map(openapiMessage);
|
|
228
|
+
if (findings.length === 0) return;
|
|
229
|
+
|
|
230
|
+
for (const message of findings) log(`-- ${message}`);
|
|
231
|
+
if (mode === "error") {
|
|
232
|
+
throw new BuildError(
|
|
233
|
+
`${findings.length} operation(s) in ${path.basename(file)} have no example ` +
|
|
234
|
+
"(lint.examples." + kind + " is error):\n" + findings.map((m) => ` ${m}`).join("\n"));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
208
238
|
/**
|
|
209
239
|
* Copy a built document to the project that implements the target.
|
|
210
240
|
*
|
|
@@ -240,8 +270,11 @@ function prepare(config, { kinds = ["openapi", "asyncapi"], log = () => {} } = {
|
|
|
240
270
|
stage(config, kind, log);
|
|
241
271
|
substituteTree(config, kind, log);
|
|
242
272
|
for (const target of targets) {
|
|
243
|
-
if (isAggregate(config, target, kind)
|
|
273
|
+
if (!isAggregate(config, target, kind)) continue;
|
|
274
|
+
if (kind === "asyncapi") {
|
|
244
275
|
generateAsyncApi(config, target, { log });
|
|
276
|
+
} else {
|
|
277
|
+
generateOpenApi(config, target, { log });
|
|
245
278
|
}
|
|
246
279
|
}
|
|
247
280
|
}
|
|
@@ -269,14 +302,13 @@ function build(config, { targets, versionOf = () => null, kinds = ["openapi", "a
|
|
|
269
302
|
log(`\n=== ${target} (${kind}) ===`);
|
|
270
303
|
// An aggregate has no hand-written bundle root; it is synthesised from
|
|
271
304
|
// its members into the staged tree, so it can never fall behind them.
|
|
305
|
+
let openapiOwners = null;
|
|
272
306
|
if (isAggregate(config, target, kind)) {
|
|
273
|
-
if (kind
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
307
|
+
if (kind === "asyncapi") {
|
|
308
|
+
generateAsyncApi(config, target, { log });
|
|
309
|
+
} else {
|
|
310
|
+
openapiOwners = generateOpenApi(config, target, { log }).owners;
|
|
278
311
|
}
|
|
279
|
-
generateAsyncApi(config, target, { log });
|
|
280
312
|
}
|
|
281
313
|
const outFile = path.join(config.distDir(target), config.outputName(kind));
|
|
282
314
|
if (config.fragmentPaths(kind)) {
|
|
@@ -284,12 +316,25 @@ function build(config, { targets, versionOf = () => null, kinds = ["openapi", "a
|
|
|
284
316
|
} else {
|
|
285
317
|
bundle(config, target, kind, outFile, log);
|
|
286
318
|
}
|
|
319
|
+
if (openapiOwners) {
|
|
320
|
+
// Pass 2: only once bundling has resolved every $ref does an
|
|
321
|
+
// operation's id, or whether it already sets its own security,
|
|
322
|
+
// become visible at all.
|
|
323
|
+
const { text, reconciled } = openapiPushDown(fs.readFileSync(outFile, "utf8"), openapiOwners, {
|
|
324
|
+
operationIdStrategy: config.portfolioOperationIdStrategy(),
|
|
325
|
+
});
|
|
326
|
+
fs.writeFileSync(outFile, text);
|
|
327
|
+
for (const r of reconciled) {
|
|
328
|
+
log(`-- Reconciled tag '${r.key}': two members describe it differently; kept the first's`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
287
331
|
const version = versionOf(target);
|
|
288
332
|
if (version) {
|
|
289
333
|
log(`-- Stamping version '${version}'`);
|
|
290
334
|
stampFile(outFile, version);
|
|
291
335
|
}
|
|
292
336
|
lint(config, kind, outFile, log);
|
|
337
|
+
checkExamples(config, kind, outFile, log);
|
|
293
338
|
|
|
294
339
|
let distributed = null;
|
|
295
340
|
if (config.isPublished(target)) {
|
|
@@ -303,4 +348,6 @@ function build(config, { targets, versionOf = () => null, kinds = ["openapi", "a
|
|
|
303
348
|
return results;
|
|
304
349
|
}
|
|
305
350
|
|
|
306
|
-
module.exports = {
|
|
351
|
+
module.exports = {
|
|
352
|
+
build, prepare, stage, substituteTree, bundle, bundleWithFragmentPaths, lint, checkExamples, distribute, BuildError,
|
|
353
|
+
};
|
package/src/split.js
CHANGED
|
@@ -111,7 +111,7 @@ function split(config, { by = "kind", outDir, log = () => {} } = {}) {
|
|
|
111
111
|
// Union of every member's closure.
|
|
112
112
|
const files = new Set();
|
|
113
113
|
for (const { target, kind } of members) {
|
|
114
|
-
const entry = config.
|
|
114
|
+
const entry = config.bundleRootPath(target, kind);
|
|
115
115
|
if (!fs.existsSync(entry)) {
|
|
116
116
|
throw new SplitError(
|
|
117
117
|
`target '${target}': ${kind} bundle root not found at ${entry}; build first so the tree is staged`
|