x-openapi-flow 1.2.0 → 1.2.2

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.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # x-openapi-flow
2
2
 
3
- CLI and specification for validating the `x-openapi-flow` extension field in OpenAPI documents.
3
+ CLI and extension contract for documenting and validating resource lifecycle workflows in OpenAPI using `x-openapi-flow`.
4
+
5
+ ## Overview
6
+
7
+ `x-openapi-flow` validates:
8
+
9
+ - Extension schema correctness
10
+ - Lifecycle graph consistency
11
+ - Optional quality checks for transitions and references
12
+
13
+ It also supports a sidecar workflow (`init` + `apply`) to preserve lifecycle metadata when OpenAPI files are regenerated.
4
14
 
5
15
  ## Installation
6
16
 
@@ -23,15 +33,21 @@ If authentication is required, include this in your `.npmrc`:
23
33
 
24
34
  Use a GitHub PAT with `read:packages` (install) and `write:packages` (publish).
25
35
 
26
- ## Quick Usage
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ npx x-openapi-flow init openapi.yaml
40
+ npx x-openapi-flow apply openapi.yaml
41
+ ```
42
+
43
+ Optional checks:
27
44
 
28
45
  ```bash
29
- x-openapi-flow validate openapi.yaml
30
- x-openapi-flow graph openapi.yaml
31
- x-openapi-flow doctor
46
+ npx x-openapi-flow validate openapi.yaml --profile strict
47
+ npx x-openapi-flow graph openapi.yaml
32
48
  ```
33
49
 
34
- ## Commands
50
+ ## CLI Commands
35
51
 
36
52
  ```bash
37
53
  x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
@@ -41,20 +57,22 @@ x-openapi-flow graph <openapi-file> [--format mermaid|json]
41
57
  x-openapi-flow doctor [--config path]
42
58
  ```
43
59
 
60
+ ## Sidecar Workflow
61
+
44
62
  `init` always works on an existing OpenAPI file in your repository.
45
- `init` creates/synchronizes `x-openapi-flow.flows.yaml` as a persistent sidecar for your `x-openapi-flow` data.
63
+ `init` creates/synchronizes `{context}-openapi-flow.(json|yaml)` as a persistent sidecar for your `x-openapi-flow` data.
46
64
  Use `apply` to inject sidecar flows back into regenerated OpenAPI files.
47
65
  If no OpenAPI/Swagger file exists yet, generate one first with your framework's official OpenAPI/Swagger tooling.
48
66
 
49
- ## Recommended Workflow
67
+ ### Recommended Sequence
50
68
 
51
69
  ```bash
52
70
  x-openapi-flow init openapi.yaml
53
- # edit x-openapi-flow.flows.yaml
71
+ # edit {context}-openapi-flow.(json|yaml)
54
72
  x-openapi-flow apply openapi.yaml
55
73
  ```
56
74
 
57
- ## Optional Configuration
75
+ ## Configuration
58
76
 
59
77
  Create `x-openapi-flow.config.json` in your project directory:
60
78
 
@@ -66,12 +84,12 @@ Create `x-openapi-flow.config.json` in your project directory:
66
84
  }
67
85
  ```
68
86
 
69
- ## File Compatibility
87
+ ## Compatibility
70
88
 
71
89
  - OpenAPI input in `.yaml`, `.yml`, and `.json`
72
90
  - Validation processes OAS content with the `x-openapi-flow` extension
73
91
 
74
- ### Optional Transition Guidance Fields
92
+ ## Transition Guidance Fields
75
93
 
76
94
  - `next_operation_id`: operationId usually called for the next state transition
77
95
  - `prerequisite_operation_ids`: operationIds expected before a transition
@@ -83,21 +101,25 @@ Field reference format:
83
101
  - `operationId:request.body.field`
84
102
  - `operationId:response.<status>.body.field`
85
103
 
86
- ## Swagger UI
104
+ ## Visualization
105
+
106
+ ### Swagger UI
87
107
 
88
108
  - There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
89
109
  - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
90
110
  - A ready HTML example is available at `examples/swagger-ui/index.html`.
111
+ - The plugin renders a global **Flow Overview** (Mermaid image) near the top of the docs, plus operation-level flow cards.
91
112
 
92
113
  ![Swagger UI integration result](../docs/assets/swagger-ui-integration-result-v2.svg)
93
114
 
94
- ## Graph Output Example
115
+ ### Graph Output Example
95
116
 
96
117
  `x-openapi-flow graph` includes transition guidance labels in Mermaid output when present (`next_operation_id`, `prerequisite_operation_ids`).
118
+ The `graph` command accepts both full OpenAPI files and sidecar files (`{context}-openapi-flow.(json|yaml)`).
97
119
 
98
120
  ![Guided graph example](../docs/assets/graph-order-guided.svg)
99
121
 
100
- ## Repository and Full Documentation
122
+ ## Repository and Documentation
101
123
 
102
124
  - Repository: https://github.com/tiago-marques/x-openapi-flow
103
125
  - Full guide and changelog are available in the root repository.
@@ -53,7 +53,7 @@ Examples:
53
53
  x-openapi-flow validate examples/order-api.yaml
54
54
  x-openapi-flow validate examples/order-api.yaml --profile relaxed
55
55
  x-openapi-flow validate examples/order-api.yaml --strict-quality
56
- x-openapi-flow init openapi.yaml --flows x-openapi-flow.flows.yaml
56
+ x-openapi-flow init openapi.yaml --flows openapi-openapi-flow.yaml
57
57
  x-openapi-flow init
58
58
  x-openapi-flow apply openapi.yaml
59
59
  x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
@@ -386,7 +386,10 @@ function resolveFlowsPath(openApiFile, customFlowsPath) {
386
386
  }
387
387
 
388
388
  if (openApiFile) {
389
- return path.join(path.dirname(openApiFile), DEFAULT_FLOWS_FILE);
389
+ const parsed = path.parse(openApiFile);
390
+ const extension = parsed.ext.toLowerCase() === ".json" ? ".json" : ".yaml";
391
+ const fileName = `${parsed.name}-openapi-flow${extension}`;
392
+ return path.join(path.dirname(openApiFile), fileName);
390
393
  }
391
394
 
392
395
  return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
@@ -406,6 +409,15 @@ function saveOpenApi(filePath, api) {
406
409
  fs.writeFileSync(filePath, content, "utf8");
407
410
  }
408
411
 
412
+ function buildFallbackOperationId(method, pathKey) {
413
+ const raw = `${method}_${pathKey}`.toLowerCase();
414
+ const sanitized = raw
415
+ .replace(/[^a-z0-9]+/g, "_")
416
+ .replace(/^_+|_+$/g, "");
417
+
418
+ return sanitized || "operation";
419
+ }
420
+
409
421
  function extractOperationEntries(api) {
410
422
  const entries = [];
411
423
  const paths = (api && api.paths) || {};
@@ -419,13 +431,16 @@ function extractOperationEntries(api) {
419
431
  }
420
432
 
421
433
  const operationId = operation.operationId;
434
+ const resolvedOperationId = operationId || buildFallbackOperationId(method, pathKey);
422
435
  const key = operationId ? `operationId:${operationId}` : `${method.toUpperCase()} ${pathKey}`;
423
436
 
424
437
  entries.push({
425
438
  key,
426
439
  operationId,
440
+ resolvedOperationId,
427
441
  method,
428
442
  path: pathKey,
443
+ operation,
429
444
  });
430
445
  }
431
446
  }
@@ -456,10 +471,22 @@ function readFlowsFile(flowsPath) {
456
471
 
457
472
  function writeFlowsFile(flowsPath, flowsDoc) {
458
473
  fs.mkdirSync(path.dirname(flowsPath), { recursive: true });
459
- const content = yaml.dump(flowsDoc, { noRefs: true, lineWidth: -1 });
474
+ const content = flowsPath.endsWith(".json")
475
+ ? `${JSON.stringify(flowsDoc, null, 2)}\n`
476
+ : yaml.dump(flowsDoc, { noRefs: true, lineWidth: -1 });
460
477
  fs.writeFileSync(flowsPath, content, "utf8");
461
478
  }
462
479
 
480
+ function buildFlowTemplate(operationId) {
481
+ const safeOperationId = operationId || "operation";
482
+ return {
483
+ version: "1.0",
484
+ id: `${safeOperationId}_FLOW_ID`,
485
+ current_state: `${safeOperationId}_STATE`,
486
+ transitions: [],
487
+ };
488
+ }
489
+
463
490
  function buildOperationLookup(api) {
464
491
  const lookupByKey = new Map();
465
492
  const lookupByOperationId = new Map();
@@ -467,8 +494,8 @@ function buildOperationLookup(api) {
467
494
 
468
495
  for (const entry of entries) {
469
496
  lookupByKey.set(entry.key, entry);
470
- if (entry.operationId) {
471
- lookupByOperationId.set(entry.operationId, entry);
497
+ if (entry.resolvedOperationId) {
498
+ lookupByOperationId.set(entry.resolvedOperationId, entry);
472
499
  }
473
500
  }
474
501
 
@@ -476,51 +503,41 @@ function buildOperationLookup(api) {
476
503
  }
477
504
 
478
505
  function mergeFlowsWithOpenApi(api, flowsDoc) {
479
- const { entries, lookupByKey, lookupByOperationId } = buildOperationLookup(api);
506
+ const { entries, lookupByKey } = buildOperationLookup(api);
480
507
 
508
+ const existingByOperationId = new Map();
481
509
  const existingByKey = new Map();
482
510
  for (const entry of flowsDoc.operations) {
483
- const entryKey = entry.key || (entry.operationId ? `operationId:${entry.operationId}` : null);
484
- if (entryKey) {
485
- existingByKey.set(entryKey, entry);
511
+ if (entry && entry.operationId) {
512
+ existingByOperationId.set(entry.operationId, entry);
513
+ }
514
+
515
+ const legacyKey = entry && entry.key ? entry.key : null;
516
+ if (legacyKey) {
517
+ existingByKey.set(legacyKey, entry);
486
518
  }
487
519
  }
488
520
 
489
521
  const mergedOperations = [];
490
522
 
491
523
  for (const op of entries) {
492
- const existing = existingByKey.get(op.key);
493
- if (existing) {
494
- mergedOperations.push({
495
- ...existing,
496
- key: op.key,
497
- operationId: op.operationId,
498
- method: op.method,
499
- path: op.path,
500
- missing_in_openapi: false,
501
- });
502
- } else {
503
- mergedOperations.push({
504
- key: op.key,
505
- operationId: op.operationId,
506
- method: op.method,
507
- path: op.path,
508
- "x-openapi-flow": null,
509
- missing_in_openapi: false,
510
- });
511
- }
512
- }
524
+ const existing = (op.resolvedOperationId && existingByOperationId.get(op.resolvedOperationId))
525
+ || existingByKey.get(op.key);
513
526
 
514
- for (const existing of flowsDoc.operations) {
515
- const existingKey = existing.key || (existing.operationId ? `operationId:${existing.operationId}` : null);
516
- const found = existingKey && lookupByKey.has(existingKey);
527
+ const openApiFlow =
528
+ op.operation && typeof op.operation["x-openapi-flow"] === "object"
529
+ ? op.operation["x-openapi-flow"]
530
+ : null;
517
531
 
518
- if (!found) {
519
- mergedOperations.push({
520
- ...existing,
521
- missing_in_openapi: true,
522
- });
523
- }
532
+ const sidecarFlow =
533
+ existing && typeof existing["x-openapi-flow"] === "object"
534
+ ? existing["x-openapi-flow"]
535
+ : null;
536
+
537
+ mergedOperations.push({
538
+ operationId: op.resolvedOperationId,
539
+ "x-openapi-flow": sidecarFlow || openApiFlow || buildFlowTemplate(op.resolvedOperationId),
540
+ });
524
541
  }
525
542
 
526
543
  return {
@@ -535,12 +552,12 @@ function applyFlowsToOpenApi(api, flowsDoc) {
535
552
  const { lookupByKey, lookupByOperationId } = buildOperationLookup(api);
536
553
 
537
554
  for (const flowEntry of flowsDoc.operations || []) {
538
- if (!flowEntry || flowEntry.missing_in_openapi === true) {
555
+ if (!flowEntry) {
539
556
  continue;
540
557
  }
541
558
 
542
559
  const flowValue = flowEntry["x-openapi-flow"];
543
- if (!flowValue || typeof flowValue !== "object") {
560
+ if (!flowValue || typeof flowValue !== "object" || Object.keys(flowValue).length === 0) {
544
561
  continue;
545
562
  }
546
563
 
@@ -595,19 +612,12 @@ function runInit(parsed) {
595
612
 
596
613
  const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
597
614
  writeFlowsFile(flowsPath, mergedFlows);
598
- const appliedCount = applyFlowsToOpenApi(api, mergedFlows);
599
- saveOpenApi(targetOpenApiFile, api);
600
-
601
- const trackedCount = mergedFlows.operations.filter((entry) => !entry.missing_in_openapi).length;
602
- const orphanCount = mergedFlows.operations.filter((entry) => entry.missing_in_openapi).length;
615
+ const trackedCount = mergedFlows.operations.length;
603
616
 
604
617
  console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
605
618
  console.log(`Flows sidecar synced: ${flowsPath}`);
606
619
  console.log(`Tracked operations: ${trackedCount}`);
607
- if (orphanCount > 0) {
608
- console.log(`Orphan flow entries kept in sidecar: ${orphanCount}`);
609
- }
610
- console.log(`Applied x-openapi-flow entries to OpenAPI: ${appliedCount}`);
620
+ console.log("OpenAPI source unchanged. Edit the sidecar and run apply to generate the full spec.");
611
621
 
612
622
  console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
613
623
  return 0;
@@ -699,8 +709,11 @@ function runDoctor(parsed) {
699
709
  }
700
710
 
701
711
  function buildMermaidGraph(filePath) {
702
- const api = loadApi(filePath);
703
- const flows = extractFlows(api);
712
+ const flows = extractFlowsForGraph(filePath);
713
+ if (flows.length === 0) {
714
+ throw new Error("No x-openapi-flow definitions found in OpenAPI or sidecar file");
715
+ }
716
+
704
717
  const lines = ["stateDiagram-v2"];
705
718
  const nodes = new Set();
706
719
  const edges = [];
@@ -760,6 +773,51 @@ function buildMermaidGraph(filePath) {
760
773
  };
761
774
  }
762
775
 
776
+ function extractFlowsForGraph(filePath) {
777
+ let flows = [];
778
+
779
+ try {
780
+ const api = loadApi(filePath);
781
+ flows = extractFlows(api);
782
+ } catch (_err) {
783
+ flows = [];
784
+ }
785
+
786
+ if (flows.length > 0) {
787
+ return flows;
788
+ }
789
+
790
+ const content = fs.readFileSync(filePath, "utf8");
791
+ const parsed = yaml.load(content);
792
+
793
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.operations)) {
794
+ return [];
795
+ }
796
+
797
+ const sidecarFlows = [];
798
+ for (const operationEntry of parsed.operations) {
799
+ if (!operationEntry || typeof operationEntry !== "object") {
800
+ continue;
801
+ }
802
+
803
+ const flow = operationEntry["x-openapi-flow"];
804
+ if (!flow || typeof flow !== "object") {
805
+ continue;
806
+ }
807
+
808
+ if (!flow.current_state) {
809
+ continue;
810
+ }
811
+
812
+ sidecarFlows.push({
813
+ endpoint: operationEntry.operationId || operationEntry.key || "sidecar-operation",
814
+ flow,
815
+ });
816
+ }
817
+
818
+ return sidecarFlows;
819
+ }
820
+
763
821
  function runGraph(parsed) {
764
822
  try {
765
823
  const graphResult = buildMermaidGraph(parsed.filePath);
@@ -160,6 +160,9 @@ window.XOpenApiFlowPlugin = function () {
160
160
  .xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
161
161
  .xof-edge { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
162
162
  .xof-empty { opacity: 0.85; font-style: italic; }
163
+ .xof-overview { margin: 10px 0 16px; }
164
+ .xof-overview img { width: 100%; max-width: 760px; border: 1px solid rgba(255,255,255,0.3); border-radius: 6px; background: #fff; }
165
+ .xof-overview-code { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 11px; opacity: 0.9; white-space: pre-wrap; }
163
166
  `;
164
167
 
165
168
  document.head.appendChild(style);
@@ -215,6 +218,174 @@ window.XOpenApiFlowPlugin = function () {
215
218
  `;
216
219
  }
217
220
 
221
+ function getSpecFromUi() {
222
+ try {
223
+ if (!window.ui || !window.ui.specSelectors || !window.ui.specSelectors.specJson) {
224
+ return null;
225
+ }
226
+
227
+ const spec = window.ui.specSelectors.specJson();
228
+ return spec && spec.toJS ? spec.toJS() : spec;
229
+ } catch (_error) {
230
+ return null;
231
+ }
232
+ }
233
+
234
+ function extractFlowsFromSpec(spec) {
235
+ const result = [];
236
+ const paths = (spec && spec.paths) || {};
237
+ const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
238
+
239
+ Object.entries(paths).forEach(([pathKey, pathItem]) => {
240
+ if (!pathItem || typeof pathItem !== 'object') return;
241
+
242
+ methods.forEach((method) => {
243
+ const operation = pathItem[method];
244
+ if (!operation || typeof operation !== 'object') return;
245
+
246
+ const flow = operation['x-openapi-flow'];
247
+ if (!flow || typeof flow !== 'object' || !flow.current_state) return;
248
+
249
+ result.push({
250
+ operationId: operation.operationId || `${method}_${pathKey}`,
251
+ flow,
252
+ });
253
+ });
254
+ });
255
+
256
+ return result;
257
+ }
258
+
259
+ function buildOverviewMermaid(flows) {
260
+ const lines = ['stateDiagram-v2'];
261
+ const states = new Set();
262
+ const seen = new Set();
263
+
264
+ flows.forEach(({ flow }) => {
265
+ const current = flow.current_state;
266
+ if (!current) return;
267
+
268
+ states.add(current);
269
+ const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
270
+ transitions.forEach((transition) => {
271
+ const target = transition.target_state;
272
+ if (!target) return;
273
+ states.add(target);
274
+
275
+ const labelParts = [];
276
+ if (transition.next_operation_id) {
277
+ labelParts.push(`next:${text(transition.next_operation_id)}`);
278
+ }
279
+ if (Array.isArray(transition.prerequisite_operation_ids) && transition.prerequisite_operation_ids.length) {
280
+ labelParts.push(`requires:${transition.prerequisite_operation_ids.join(',')}`);
281
+ }
282
+ const label = labelParts.join(' | ');
283
+ const key = `${current}::${target}::${label}`;
284
+ if (seen.has(key)) return;
285
+ seen.add(key);
286
+ lines.push(` ${current} --> ${target}${label ? `: ${label}` : ''}`);
287
+ });
288
+ });
289
+
290
+ Array.from(states)
291
+ .sort()
292
+ .forEach((state) => {
293
+ lines.splice(1, 0, ` state ${state}`);
294
+ });
295
+
296
+ return lines.join('\n');
297
+ }
298
+
299
+ let mermaidLoaderPromise = null;
300
+ function ensureMermaid() {
301
+ if (window.mermaid) {
302
+ return Promise.resolve(window.mermaid);
303
+ }
304
+
305
+ if (mermaidLoaderPromise) {
306
+ return mermaidLoaderPromise;
307
+ }
308
+
309
+ mermaidLoaderPromise = new Promise((resolve, reject) => {
310
+ const script = document.createElement('script');
311
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
312
+ script.async = true;
313
+ script.onload = () => {
314
+ if (window.mermaid) {
315
+ window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
316
+ resolve(window.mermaid);
317
+ } else {
318
+ reject(new Error('Mermaid library not available after load'));
319
+ }
320
+ };
321
+ script.onerror = () => reject(new Error('Could not load Mermaid library'));
322
+ document.head.appendChild(script);
323
+ });
324
+
325
+ return mermaidLoaderPromise;
326
+ }
327
+
328
+ function svgToDataUri(svg) {
329
+ const encoded = window.btoa(unescape(encodeURIComponent(svg)));
330
+ return `data:image/svg+xml;base64,${encoded}`;
331
+ }
332
+
333
+ let overviewRenderedHash = null;
334
+ let overviewRenderInProgress = false;
335
+ let overviewPendingHash = null;
336
+ async function renderOverview() {
337
+ const spec = getSpecFromUi();
338
+ const flows = extractFlowsFromSpec(spec);
339
+ if (!flows.length) return;
340
+
341
+ const mermaid = buildOverviewMermaid(flows);
342
+ const currentHash = `${flows.length}:${mermaid}`;
343
+ if (overviewRenderedHash === currentHash) return;
344
+ if (overviewRenderInProgress && overviewPendingHash === currentHash) return;
345
+
346
+ const infoContainer = document.querySelector('.swagger-ui .information-container');
347
+ if (!infoContainer) return;
348
+
349
+ let holder = document.getElementById('xof-overview-holder');
350
+ if (!holder) {
351
+ holder = document.createElement('div');
352
+ holder.id = 'xof-overview-holder';
353
+ holder.className = 'xof-overview xof-card';
354
+ infoContainer.parentNode.insertBefore(holder, infoContainer.nextSibling);
355
+ }
356
+
357
+ holder.innerHTML = '<div class="xof-title">x-openapi-flow — Flow Overview</div><div class="xof-empty">Rendering Mermaid graph...</div>';
358
+ overviewRenderInProgress = true;
359
+ overviewPendingHash = currentHash;
360
+
361
+ try {
362
+ const mermaidLib = await ensureMermaid();
363
+ const renderId = `xof-overview-${Date.now()}`;
364
+ const renderResult = await mermaidLib.render(renderId, mermaid);
365
+ const svg = renderResult && renderResult.svg ? renderResult.svg : renderResult;
366
+ const dataUri = svgToDataUri(svg);
367
+
368
+ holder.innerHTML = `
369
+ <div class="xof-title">x-openapi-flow — Flow Overview</div>
370
+ <img src="${dataUri}" alt="x-openapi-flow overview graph" />
371
+ <details style="margin-top:8px;">
372
+ <summary style="cursor:pointer;">Mermaid source</summary>
373
+ <div class="xof-overview-code">${mermaid.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
374
+ </details>
375
+ `;
376
+ } catch (_error) {
377
+ holder.innerHTML = `
378
+ <div class="xof-title">x-openapi-flow — Flow Overview</div>
379
+ <div class="xof-empty">Could not render Mermaid image in this environment.</div>
380
+ <div class="xof-overview-code">${mermaid.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
381
+ `;
382
+ } finally {
383
+ overviewRenderInProgress = false;
384
+ }
385
+
386
+ overviewRenderedHash = currentHash;
387
+ }
388
+
218
389
  function findXOpenApiFlowValueCell(opblock) {
219
390
  const rows = opblock.querySelectorAll('tr');
220
391
  for (const row of rows) {
@@ -249,14 +420,27 @@ window.XOpenApiFlowPlugin = function () {
249
420
  injectStyles();
250
421
  const opblocks = document.querySelectorAll('.opblock');
251
422
  opblocks.forEach((opblock) => enhanceOperation(opblock));
423
+ renderOverview().catch(() => {
424
+ // keep plugin resilient in environments where async rendering fails
425
+ });
426
+ }
427
+
428
+ let enhanceScheduled = false;
429
+ function scheduleEnhance() {
430
+ if (enhanceScheduled) return;
431
+ enhanceScheduled = true;
432
+ window.requestAnimationFrame(() => {
433
+ enhanceScheduled = false;
434
+ enhanceAll();
435
+ });
252
436
  }
253
437
 
254
438
  const observer = new MutationObserver(() => {
255
- enhanceAll();
439
+ scheduleEnhance();
256
440
  });
257
441
 
258
442
  window.addEventListener('load', () => {
259
- enhanceAll();
443
+ scheduleEnhance();
260
444
  observer.observe(document.body, { childList: true, subtree: true });
261
445
  });
262
446
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-openapi-flow",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "OpenAPI extension for resource workflow and lifecycle management",
5
5
  "main": "lib/validator.js",
6
6
  "repository": {