@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/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.bundlePath(target, kind, stagingRoot);
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.bundlePath(target, kind))} with ${KEY}`);
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.bundlePath(target, kind, root)).split(path.sep).join("/");
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.bundlePath(target, kind))} with ${KEY}`);
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) && kind === "asyncapi") {
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 !== "asyncapi") {
274
- throw new BuildError(
275
- `target '${target}': aggregate is only supported for asyncapi; ` +
276
- `an OpenAPI portfolio is a hand-written table of contents of $refs`
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 = { build, prepare, stage, substituteTree, bundle, bundleWithFragmentPaths, lint, distribute, BuildError };
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.bundlePath(target, kind);
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`