@arc-e-tect/api-only-publisher 0.2.0 → 0.4.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 +273 -257
- package/package.json +1 -1
- package/src/config.js +26 -5
- package/src/fragment-paths.js +107 -0
- package/src/index.js +1 -0
- package/src/pipeline.js +64 -10
- package/src/placeholders.js +12 -17
- package/src/split.js +4 -4
- package/src/unreferenced.js +20 -1
- package/src/version.js +4 -6
package/README.adoc
CHANGED
|
@@ -11,64 +11,63 @@ image:https://img.shields.io/github/v/release/Arc-E-Tect/SoftwareEngineeringDone
|
|
|
11
11
|
image:https://img.shields.io/badge/Node-22%2B-339933?logo=node.js[Node 22+,link=https://nodejs.org]
|
|
12
12
|
image:https://img.shields.io/badge/License-MIT-blue[MIT,link=LICENSE]
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
[NOTE]
|
|
15
|
+
.Where the links in this document go
|
|
16
|
+
====
|
|
17
|
+
Every link ends with a marker saying where it leads, so you can tell before you follow it:
|
|
18
|
+
|
|
19
|
+
[horizontal]
|
|
20
|
+
`↗`:: An *external* site, outside this repository.
|
|
21
|
+
`→`:: *Another file* in this repository.
|
|
22
|
+
`§`:: *A section of this same file.*
|
|
23
|
+
====
|
|
15
24
|
|
|
16
25
|
// Remove this notice when 1.0.0 is released.
|
|
17
26
|
[IMPORTANT]
|
|
18
27
|
====
|
|
19
28
|
Until 1.0.0 is released, any release may change the command line, `apionly.yaml` or the published artifacts incompatibly, without a major version bump.
|
|
20
|
-
|
|
29
|
+
The interface is still being settled; 1.0.0 is the first release whose interface is kept stable.
|
|
21
30
|
Pin an exact version, and read the changelog before upgrading.
|
|
22
31
|
====
|
|
23
32
|
|
|
33
|
+
[#what-it-is]
|
|
24
34
|
== What it is
|
|
25
35
|
|
|
26
|
-
`api-only-publisher` turns a library of small, reusable API description
|
|
27
|
-
fragments into one complete, self-contained document per target, and distributes
|
|
28
|
-
each to whoever needs it.
|
|
29
|
-
|
|
30
|
-
It is the producer half of a pair. The consumer half, `API-Only Subscriber`,
|
|
31
|
-
fetches a published document into an implementation project and verifies that it
|
|
32
|
-
has not drifted. The two are coupled only by the published artifact and its
|
|
33
|
-
manifest -- neither knows anything else about the other.
|
|
34
|
-
|
|
35
|
-
A *target* is whatever gets its own contract document: a microservice, a modular
|
|
36
|
-
monolith, a backend-for-frontend, or a documentation aggregate spanning all of
|
|
37
|
-
them. It need not be a deployable, which is why this is not called `services`.
|
|
36
|
+
`api-only-publisher` turns a library of small, reusable API description fragments into one complete, self-contained document per target, and distributes each to whoever needs it.
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
It is the producer half of a pair.
|
|
39
|
+
The consumer half, the link:../api-only-subscriber/README.adoc[API-Only Subscriber →], fetches a published document into an implementation project and verifies that it has not drifted.
|
|
40
|
+
The two are coupled only by the published artifact and its manifest; neither knows anything else about the other.
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
A *target* is whatever gets its own contract document: a microservice, a modular monolith, a backend-for-frontend, or a documentation aggregate spanning all of them.
|
|
43
|
+
It need not be a deployable, which is why this is not called `services`.
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
a write-to-a-temp-file-and-move dance to work around the difference between BSD
|
|
46
|
-
and GNU `sed -i`. And neither could parse YAML, which is why setting a version
|
|
47
|
-
was a regular expression that silently did nothing when it failed to match.
|
|
48
|
-
|
|
49
|
-
Node was never an optional dependency either: `@redocly/cli` and `@asyncapi/cli`
|
|
50
|
-
are Node tools, so a Node runtime was already required to run the pipeline at all.
|
|
45
|
+
This document is the reference for the Publisher alone.
|
|
46
|
+
To set it up for your situation, start at link:../docs/README.adoc[the documentation →].
|
|
51
47
|
|
|
48
|
+
[#requirements]
|
|
52
49
|
== Requirements
|
|
53
50
|
|
|
54
51
|
[cols="1,1",options="header"]
|
|
55
52
|
|===
|
|
56
53
|
| Requirement | Version
|
|
57
|
-
| Node
|
|
58
|
-
| npm |
|
|
54
|
+
| Node.js | `^22.14.0`, `^24.10.0` or `>=26.0.0`
|
|
55
|
+
| npm | The one that comes with Node.js
|
|
59
56
|
|===
|
|
60
57
|
|
|
61
|
-
Nothing else.
|
|
62
|
-
pins, so they need no installation, and the only runtime dependency is `yaml`.
|
|
58
|
+
Nothing else.
|
|
59
|
+
The bundlers, https://redocly.com/docs/cli/[Redocly CLI ↗] and https://www.asyncapi.com/tools/cli[AsyncAPI CLI ↗], are fetched by `npx` at the versions `apionly.yaml` pins, so they need no installation, and the only runtime dependency is `yaml`.
|
|
63
60
|
|
|
61
|
+
[#installation]
|
|
64
62
|
== Installation
|
|
65
63
|
|
|
66
64
|
The Publisher behaves the same however it is started.
|
|
67
65
|
What differs is where its version is pinned, and what checks that pin.
|
|
68
66
|
|
|
67
|
+
[#as-a-development-dependency]
|
|
69
68
|
=== As a development dependency
|
|
70
69
|
|
|
71
|
-
The
|
|
70
|
+
The fit for a specification library, and for any build that runs the Publisher more than once.
|
|
72
71
|
|
|
73
72
|
[source,console]
|
|
74
73
|
----
|
|
@@ -93,7 +92,7 @@ npm install --save-dev --save-exact @arc-e-tect/api-only-publisher
|
|
|
93
92
|
----
|
|
94
93
|
npm ci
|
|
95
94
|
npm run apionly -- build
|
|
96
|
-
npm run apionly -- publish --target
|
|
95
|
+
npm run apionly -- publish --target orders
|
|
97
96
|
----
|
|
98
97
|
|
|
99
98
|
* The version is pinned in one place, `package.json`, and every command and every CI job runs that one.
|
|
@@ -102,9 +101,13 @@ npm run apionly -- publish --target user-account
|
|
|
102
101
|
* Renovate and Dependabot see the dependency, so an upgrade arrives as a pull request that moves the pin.
|
|
103
102
|
* The cost is a `package.json` and a `package-lock.json` in the library, and an `npm ci` before the first command.
|
|
104
103
|
|
|
104
|
+
Run it through the script, as `npm run apionly -- <command>`.
|
|
105
|
+
`npx api-only-publisher`, without the scope, is not this package: npx would look that name up on the public registry.
|
|
106
|
+
|
|
107
|
+
[#with-npx]
|
|
105
108
|
=== With `npx`, installing nothing
|
|
106
109
|
|
|
107
|
-
The
|
|
110
|
+
The fit for trying the tool, for scaffolding a library with `init`, and for a one-off command.
|
|
108
111
|
|
|
109
112
|
[source,console]
|
|
110
113
|
----
|
|
@@ -118,99 +121,91 @@ npx @arc-e-tect/api-only-publisher@<version> build -C my-api-library
|
|
|
118
121
|
* Nothing records an integrity hash, and nothing notices when a newer version is released.
|
|
119
122
|
|
|
120
123
|
Either way, pin an exact version rather than a range.
|
|
121
|
-
|
|
124
|
+
A published contract should change only when its fragments or its version change, and a floating tool version lets it change without either.
|
|
122
125
|
|
|
123
126
|
The bundlers are a separate matter.
|
|
124
127
|
However the Publisher itself is started, it fetches them with `npx`, at the versions `toolchain` in `apionly.yaml` pins.
|
|
125
128
|
|
|
129
|
+
[#commands]
|
|
126
130
|
== Commands
|
|
127
131
|
|
|
128
132
|
[cols="1,3", options="header"]
|
|
129
133
|
|===
|
|
130
134
|
|Command |Purpose
|
|
131
135
|
|
|
132
|
-
|`init [dir]`
|
|
133
|
-
|Scaffold `apionly.yaml` and a reference directory layout that builds as it
|
|
134
|
-
stands. This is how the *conventions* become reusable, as opposed to the code.
|
|
136
|
+
|`init [dir] [--force]`
|
|
137
|
+
|Scaffold `apionly.yaml` and a reference directory layout that builds as it stands.
|
|
135
138
|
|
|
136
|
-
|`build`
|
|
137
|
-
|Stage, substitute placeholders, bundle, stamp each published target's version,
|
|
139
|
+
|`build [--target <t>]... [--pre-release <ids>] [--openapi\|--asyncapi]`
|
|
140
|
+
|Stage, substitute placeholders, bundle, stamp each published target's version, and lint.
|
|
138
141
|
|
|
139
|
-
|`lint
|
|
140
|
-
|Lint what is already built, without rebuilding,
|
|
142
|
+
|`lint [--target <t>]...`
|
|
143
|
+
|Lint what is already built, without rebuilding, and write a report per document.
|
|
141
144
|
Without `--target`, it also fails on a fragment no target reaches, as <<unreferenced,A fragment no target reaches fails the lint §>> describes.
|
|
142
145
|
|
|
143
146
|
|`targets`
|
|
144
|
-
|List the declared targets, what each one builds, and whether it is
|
|
147
|
+
|List the declared targets, what each one builds, and whether it is published.
|
|
145
148
|
|
|
146
|
-
|`closure
|
|
147
|
-
|Show each target's dependency closure: how many files it reaches, and the hash
|
|
148
|
-
of their content.
|
|
149
|
+
|`closure [--target <t>]...`
|
|
150
|
+
|Show each target's dependency closure: how many files it reaches, and the hash of their content.
|
|
149
151
|
|
|
150
152
|
|`changed --since <ref>`
|
|
151
|
-
|Report which targets' closures
|
|
152
|
-
|
|
153
|
-
much else in the repository moved.
|
|
153
|
+
|Report which targets' closures changed between a git ref and the working tree.
|
|
154
|
+
A target whose closure is untouched needs no new version, however much else in the repository moved.
|
|
154
155
|
|
|
155
|
-
|`pack`
|
|
156
|
+
|`pack [--pre-release <ids>] [--target <t>]... [--out <dir>]`
|
|
156
157
|
|Archive each built, published target with a `manifest.json`, at the version its version file declares.
|
|
157
158
|
|
|
158
|
-
|`publish`
|
|
159
|
-
|Pack once, then ship those same bytes to every configured channel
|
|
159
|
+
|`publish [--pre-release <ids>] [--target <t>]... [--channel <c>]... [--out <dir>]`
|
|
160
|
+
|Pack once, then ship those same bytes to every configured channel, or to each `--channel`.
|
|
160
161
|
|
|
161
|
-
|`split --out <dir
|
|
162
|
+
|`split --out <dir> [--by kind\|target]`
|
|
162
163
|
|Make the library safe to break into separate repositories.
|
|
163
164
|
|===
|
|
164
165
|
|
|
166
|
+
`--target` and `--channel` may be repeated.
|
|
167
|
+
`-C <dir>` runs as if started in `<dir>`; without it the configuration is found by walking up from the current directory.
|
|
168
|
+
`-q` or `--quiet` reports only errors, which is what a script that reads the output wants.
|
|
169
|
+
`--pre-release <ids>` appends pre-release identifiers to each target's version, as <<versions,Versions §>> describes.
|
|
170
|
+
|
|
165
171
|
[source,console]
|
|
166
172
|
----
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
api-only-publisher publish --target user-account --pre-release rc.1
|
|
171
|
-
api-only-publisher targets
|
|
173
|
+
npm run apionly -- build --target orders --openapi
|
|
174
|
+
npm run apionly -- publish --target orders --pre-release rc.1
|
|
175
|
+
npm run apionly -- changed --since origin/main --quiet
|
|
172
176
|
----
|
|
173
177
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
`--pre-release <ids>` appends pre-release identifiers to each target's version, as <<versions,Versions §>> describes.
|
|
177
|
-
|
|
178
|
-
== What `build` actually does
|
|
178
|
+
[#what-build-does]
|
|
179
|
+
== What `build` does
|
|
179
180
|
|
|
180
|
-
. **Stage.** The whole source root is copied to
|
|
181
|
-
after this point touches the hand-authored tree.
|
|
181
|
+
. **Stage.** The whole source root is copied to `<build.staging>/<kind>/`.
|
|
182
|
+
Nothing after this point touches the hand-authored tree.
|
|
182
183
|
+
|
|
183
|
-
The *whole* root, not just one specification
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
those references. Each type gets *its own* staging root, so building one never
|
|
187
|
-
invalidates the other's staged tree.
|
|
184
|
+
The *whole* root, not just one specification kind's subtree, because the trees `$ref` each other: an event schema can reuse an OpenAPI component schema, so a field means the same thing in an event as in an HTTP response.
|
|
185
|
+
A partial copy breaks those references.
|
|
186
|
+
Each kind gets *its own* staging root, so building one never invalidates the other's staged tree.
|
|
188
187
|
|
|
189
|
-
. **Substitute placeholders**, in place, across the staged copy.
|
|
190
|
-
is replaced with the contents of `<token>.md`, found by searching the staged
|
|
191
|
-
|
|
192
|
-
indented YAML scalar.
|
|
188
|
+
. **Substitute placeholders**, in place, across the staged copy.
|
|
189
|
+
A `{{token}}` is replaced with the contents of `<token>.md`, found by searching the staged source root.
|
|
190
|
+
Indentation is preserved, so multi-line Markdown stays valid inside an indented YAML scalar.
|
|
193
191
|
|
|
194
|
-
. **Bundle.** `@redocly/cli bundle` or `@asyncapi/cli bundle` resolves every
|
|
195
|
-
|
|
192
|
+
. **Bundle.** `@redocly/cli bundle` or `@asyncapi/cli bundle` resolves every `$ref` into one flat, self-contained document under `<build.dist>/<target>/`.
|
|
193
|
+
An OpenAPI document's components each carry the path of the fragment they came from, as <<fragment-paths,`x-fragment-path` §>> describes.
|
|
196
194
|
|
|
197
195
|
. **Stamp the version** of each published target on its *finished* document: the version its version file declares, with any `--pre-release` identifiers appended.
|
|
198
196
|
A `publish: false` target keeps the version its source declares.
|
|
199
197
|
|
|
200
|
-
. **Lint** the finished document, so a broken fragment surfaces against the
|
|
201
|
-
target it actually affects rather than at deploy time.
|
|
202
|
-
|
|
203
|
-
. **Distribute**, unless the target is `publish: false`.
|
|
198
|
+
. **Lint** the finished document, so a broken fragment surfaces against the target it actually affects.
|
|
204
199
|
|
|
205
|
-
[
|
|
200
|
+
[#versions]
|
|
206
201
|
== Versions
|
|
207
202
|
|
|
208
203
|
Every published target has a version of its own, and it is kept in the library, beside the fragments it describes.
|
|
209
204
|
|
|
210
|
-
.specs/openapi/bundles/
|
|
205
|
+
.specs/openapi/bundles/orders.bundle.properties
|
|
211
206
|
[source,properties]
|
|
212
207
|
----
|
|
213
|
-
# The version of the
|
|
208
|
+
# The version of the orders contract, for every document it builds.
|
|
214
209
|
# Semantic: major for a breaking change, minor for an additive one, patch for
|
|
215
210
|
# anything else. Change it in the same commit as the fragments it describes.
|
|
216
211
|
version=2.1.0
|
|
@@ -236,79 +231,61 @@ A version names one set of documents.
|
|
|
236
231
|
Publishing different documents under a version that was published before, after editing a fragment without changing the version file, is refused by the API-Only Subscriber in every project that locked the earlier ones.
|
|
237
232
|
Change the version in the same commit as the fragments instead.
|
|
238
233
|
|
|
234
|
+
[#closure]
|
|
239
235
|
== Independent versioning, and the dependency closure
|
|
240
236
|
|
|
241
|
-
Giving each target its own version has one non-obvious consequence, and the
|
|
242
|
-
`closure` and `changed` commands are what pay for it.
|
|
237
|
+
Giving each target its own version has one non-obvious consequence, and the `closure` and `changed` commands are what pay for it.
|
|
243
238
|
|
|
244
|
-
A change under `components/common/` affects every target that reaches it.
|
|
245
|
-
change under one
|
|
246
|
-
repository-wide versioning the distinction is invisible, because everything bumps
|
|
247
|
-
together; under per-target versioning the release job has to know the difference,
|
|
248
|
-
or one service's edit releases every other service and the version numbers stop
|
|
249
|
-
meaning anything.
|
|
239
|
+
A change under `components/common/` affects every target that reaches it.
|
|
240
|
+
A change under one target's own fragments affects only that one.
|
|
241
|
+
Under repository-wide versioning the distinction is invisible, because everything bumps together; under per-target versioning a release has to know the difference, or one target's edit releases every other target and the version numbers stop meaning anything.
|
|
250
242
|
|
|
251
|
-
A target's *closure* is every file reachable from its bundle root by following
|
|
252
|
-
|
|
253
|
-
which targets a commit actually touched:
|
|
243
|
+
A target's *closure* is every file reachable from its bundle root by following `$ref`s transitively.
|
|
244
|
+
`changed --since <ref>` resolves each closure and reports which targets a change actually touched:
|
|
254
245
|
|
|
255
246
|
[source,console]
|
|
256
247
|
----
|
|
257
|
-
|
|
248
|
+
npm run apionly -- changed --since origin/main
|
|
258
249
|
----
|
|
259
250
|
|
|
260
|
-
Two things make this correct rather than approximate.
|
|
261
|
-
over the *staged, substituted* tree, so a change to a Markdown snippet
|
|
262
|
-
|
|
263
|
-
of a closure always means editing a file already inside it, so a bundle gaining
|
|
264
|
-
or losing a `$ref` is caught without diffing membership separately.
|
|
251
|
+
Two things make this correct rather than approximate.
|
|
252
|
+
The closure is computed over the *staged, substituted* tree, so a change to a Markdown snippet, which genuinely changes the published document, counts.
|
|
253
|
+
And a change to the *shape* of a closure always means editing a file already inside it, so a bundle gaining or losing a `$ref` is caught without diffing membership separately.
|
|
265
254
|
|
|
266
|
-
The closure hash is recorded in each release's manifest as `closureSha256`, which
|
|
267
|
-
is what lets a later commit compare against what was actually published.
|
|
255
|
+
The closure hash is recorded in each release's manifest as `closureSha256`, which is what lets a later commit compare against what was actually published.
|
|
268
256
|
|
|
257
|
+
[#splitting]
|
|
269
258
|
== Splitting the library
|
|
270
259
|
|
|
271
|
-
A fragment library is one `$ref` graph, and that graph does not respect the
|
|
272
|
-
|
|
273
|
-
schemas reuse `openapi/components/common/schemas/UsernameV1.yaml` -- a username
|
|
274
|
-
means the same thing over Kafka as over HTTP. Move the two trees into separate
|
|
275
|
-
repositories as they stand and every one of those references dangles.
|
|
260
|
+
A fragment library is one `$ref` graph, and that graph does not respect the directory boundaries a repository split would follow.
|
|
261
|
+
When the AsyncAPI documents reuse `openapi/components/common/schemas/`, moving the two trees into separate repositories as they stand leaves every one of those references dangling.
|
|
276
262
|
|
|
277
263
|
[source,console]
|
|
278
264
|
----
|
|
279
|
-
|
|
265
|
+
npm run apionly -- split --out build/split --by kind
|
|
280
266
|
----
|
|
281
267
|
|
|
282
|
-
Each part is written with its own subtree *plus* a copy of every foreign file it
|
|
283
|
-
|
|
284
|
-
The copies keep their original path relative to the shared source root, which is
|
|
285
|
-
what makes this safe: every relative `$ref` keeps resolving exactly as it did, so
|
|
286
|
-
nothing is rewritten and no reference can break because a rewrite got the depth
|
|
287
|
-
wrong.
|
|
268
|
+
Each part is written with its own subtree *plus* a copy of every foreign file it reaches, so that afterwards each side is self-contained and builds on its own.
|
|
269
|
+
The copies keep their original path relative to the shared source root, which is what makes this safe: every relative `$ref` keeps resolving exactly as it did, so nothing is rewritten and no reference can break because a rewrite got the depth wrong.
|
|
288
270
|
|
|
289
|
-
`--by kind` splits along the specification
|
|
290
|
-
target its own self-contained tree, which is what you want before splitting into
|
|
291
|
-
per-service repositories.
|
|
271
|
+
`--by kind` splits along the specification kinds.
|
|
272
|
+
`--by target` gives every target its own self-contained tree, which is what you want before splitting into per-target repositories.
|
|
292
273
|
|
|
293
|
-
Any part that received copies gets an `IMPORTED.adoc` listing them and saying
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
agree today. Where that matters, promote the fragment to a contract of its own,
|
|
297
|
-
published and consumed like any other, rather than copied.
|
|
274
|
+
Any part that received copies gets an `IMPORTED.adoc` listing them and saying plainly what it costs: those copies can now drift, and nothing will notice.
|
|
275
|
+
A shared schema that two repositories both define is two schemas that happen to agree today.
|
|
276
|
+
Where that matters, promote the fragment to a contract of its own, published and consumed like any other, rather than copied.
|
|
298
277
|
|
|
278
|
+
[#aggregates]
|
|
299
279
|
== Aggregates
|
|
300
280
|
|
|
301
|
-
A whole-landscape OpenAPI view is just another bundle root: every path body
|
|
302
|
-
already lives in a `$ref`'d fragment, so a portfolio costs one hand-written
|
|
303
|
-
table of contents and duplicates no contract text.
|
|
281
|
+
A whole-landscape OpenAPI view is just another bundle root: every path body already lives in a `$ref`'d fragment, so a portfolio costs one hand-written table of contents and duplicates no contract text.
|
|
304
282
|
|
|
305
|
-
AsyncAPI cannot be written that way.
|
|
306
|
-
(`channel: {$ref: '#/channels/
|
|
307
|
-
|
|
308
|
-
async portfolio would have to copy every operation verbatim, and that copy would
|
|
309
|
-
start rotting the moment a member changed.
|
|
283
|
+
AsyncAPI cannot be written that way.
|
|
284
|
+
Its operations use document-root pointers (`channel: {$ref: '#/channels/orderPlacedV1'}`), and `#` resolves against whichever file contains it, so moving an operation into a fragment breaks it.
|
|
285
|
+
A hand-written async portfolio would have to copy every operation verbatim, and that copy would start rotting the moment a member changed.
|
|
310
286
|
|
|
311
|
-
So it is generated instead.
|
|
287
|
+
So it is generated instead.
|
|
288
|
+
Declare the members and the aggregate's own identity:
|
|
312
289
|
|
|
313
290
|
[source,yaml]
|
|
314
291
|
----
|
|
@@ -317,17 +294,17 @@ portfolio:
|
|
|
317
294
|
asyncapi:
|
|
318
295
|
bundle: portfolio_asyncapi_structure.yaml
|
|
319
296
|
aggregate:
|
|
320
|
-
-
|
|
297
|
+
- orders
|
|
298
|
+
- payments
|
|
321
299
|
info:
|
|
322
300
|
title: Everything, together
|
|
323
301
|
version: 0.0.0
|
|
324
302
|
----
|
|
325
303
|
|
|
326
|
-
The bundle root is synthesised into the staging tree immediately before bundling,
|
|
327
|
-
|
|
328
|
-
members. Two members contributing the same channel or operation key is an error
|
|
329
|
-
rather than a silent overwrite.
|
|
304
|
+
The bundle root is synthesised into the staging tree immediately before bundling, so nothing is duplicated in the source and the view cannot fall behind its members.
|
|
305
|
+
Two members contributing the same channel or operation key is an error rather than a silent overwrite.
|
|
330
306
|
|
|
307
|
+
[#channels]
|
|
331
308
|
== Channels
|
|
332
309
|
|
|
333
310
|
[cols="1,3", options="header"]
|
|
@@ -336,74 +313,63 @@ rather than a silent overwrite.
|
|
|
336
313
|
|
|
337
314
|
|`file`
|
|
338
315
|
|A local directory, laid out as `<directory>/<target>/<version>/`.
|
|
339
|
-
Not a distribution mechanism between repositories: it
|
|
316
|
+
Not a distribution mechanism between repositories: it makes the pipeline testable end to end with no infrastructure, lets a library share one build with the projects that implement it, and is a local-iteration escape hatch.
|
|
340
317
|
Publishing a version again replaces that version and keeps the others; with `clean: true` only the version being published is kept, for a directory that is a build output rather than a record of releases.
|
|
341
318
|
|
|
342
319
|
|`maven`
|
|
343
|
-
|A path publishes into a repository layout on disk; an `http(s)` URL deploys to a
|
|
344
|
-
|
|
345
|
-
conflict handling come free, which is what lets the Subscriber carry no HTTP
|
|
346
|
-
client of its own.
|
|
320
|
+
|A path publishes into a repository layout on disk; an `http(s)` URL deploys to a remote repository with an HTTP `PUT` per file and a bearer token.
|
|
321
|
+
Native for Gradle consumers: version resolution, caching and conflict handling come free, which is what lets the Subscriber carry no HTTP client of its own.
|
|
347
322
|
|
|
348
323
|
|`npm`
|
|
349
|
-
|Real semver, integrity hashes
|
|
350
|
-
toolchain here is already Node.
|
|
324
|
+
|Real semver, integrity hashes, private scopes, and a toolchain that is already Node.js.
|
|
351
325
|
|
|
352
326
|
|`github-release`
|
|
353
|
-
|The language-neutral floor: immutable per tag, works for private repositories
|
|
354
|
-
|
|
355
|
-
dependency-resolution semantics and no update notification, which is why it is
|
|
356
|
-
the fallback rather than the default.
|
|
327
|
+
|The language-neutral floor: immutable per tag, works for private repositories with a token, readable by a consumer with no JVM and no Node.js.
|
|
328
|
+
No dependency-resolution semantics and no update notification, which is why it is the fallback rather than the default.
|
|
357
329
|
|===
|
|
358
330
|
|
|
359
331
|
Credentials are read from the environment, never from configuration.
|
|
360
|
-
`channels.maven.tokenEnv` names the variable; a configuration file gets
|
|
361
|
-
|
|
332
|
+
`channels.maven.tokenEnv` names the variable, `MAVEN_TOKEN` by default; a configuration file gets committed, and a credential in a committed file is a credential that has leaked.
|
|
333
|
+
|
|
334
|
+
link:../docs/reference/how-publisher-and-subscriber-work-together.adoc#channels[How the Publisher and the Subscriber work together →] lists each channel's layout and options.
|
|
362
335
|
|
|
336
|
+
[#why-registries]
|
|
363
337
|
=== Why npm and Maven rather than release assets alone
|
|
364
338
|
|
|
365
|
-
Publishing is passive.
|
|
366
|
-
implementation repository until somebody looks.
|
|
339
|
+
Publishing is passive.
|
|
340
|
+
The producer releases `2.1.0` and nothing happens in an implementation repository until somebody looks.
|
|
367
341
|
|
|
368
|
-
Renovate and Dependabot understand npm and Maven natively, so a contract bump
|
|
369
|
-
|
|
370
|
-
practical argument for preferring them, and the reason to design the coordinates
|
|
371
|
-
so that standard update bots can read them.
|
|
342
|
+
Renovate and Dependabot understand npm and Maven natively, so a contract bump arrives downstream as a pull request without any bespoke machinery.
|
|
343
|
+
That is the practical argument for preferring them, and the reason to design the coordinates so that standard update bots can read them.
|
|
372
344
|
|
|
345
|
+
[#pre-releases]
|
|
373
346
|
== Pre-releases
|
|
374
347
|
|
|
375
|
-
API-
|
|
376
|
-
|
|
377
|
-
work around the tool by cloning the specification repository -- which is exactly
|
|
378
|
-
the broad read access that publishing artifacts exists to avoid, with none of the
|
|
379
|
-
guarantees.
|
|
348
|
+
API-first design means implementation starts against a contract that is not finished.
|
|
349
|
+
If the only way to obtain a bundle were a final release, teams would work around the tool by cloning the specification repository, which is exactly the broad read access that publishing artifacts exists to avoid, with none of the guarantees.
|
|
380
350
|
|
|
381
|
-
So a pre-release version is first-class: `2.1.0-rc.1`, or Maven's `-SNAPSHOT`
|
|
382
|
-
spelling, which is not semver-legal but is what Maven consumers expect.
|
|
351
|
+
So a pre-release version is first-class: `2.1.0-rc.1`, or Maven's `-SNAPSHOT` spelling, which is not semver-legal but is what Maven consumers expect.
|
|
383
352
|
It is cut from the version in the target's version file: `publish --pre-release rc.1` publishes `2.1.0-rc.1` when the file declares `2.1.0`.
|
|
384
353
|
|
|
385
|
-
The hard rule is the other half of it.
|
|
386
|
-
production build:
|
|
354
|
+
The hard rule is the other half of it.
|
|
355
|
+
A pre-release must never quietly satisfy a production build:
|
|
387
356
|
|
|
388
|
-
* npm publishes it under the `next` dist-tag, never `latest`, so `npm install`
|
|
389
|
-
|
|
390
|
-
*
|
|
391
|
-
to an unfinished contract.
|
|
392
|
-
* The API-Only Subscriber refuses a pre-release version outright unless the
|
|
393
|
-
subscription sets `allowPrerelease = true` -- a visible, reviewable line in a
|
|
394
|
-
build file rather than a default.
|
|
357
|
+
* npm publishes it under the `next` dist-tag, never `latest`, so `npm install` cannot pick one up by accident.
|
|
358
|
+
* A GitHub release is marked as a pre-release, so "latest release" never resolves to an unfinished contract.
|
|
359
|
+
* The API-Only Subscriber refuses a pre-release version outright unless the subscription sets `allowPrerelease = true`: a visible, reviewable line in a build file rather than a default.
|
|
395
360
|
|
|
361
|
+
[#configuration]
|
|
396
362
|
== Configuration
|
|
397
363
|
|
|
398
|
-
Everything project-specific lives in `apionly.yaml`, not in the code.
|
|
399
|
-
the tool to another project means editing configuration.
|
|
364
|
+
Everything project-specific lives in `apionly.yaml`, not in the code.
|
|
365
|
+
Adapting the tool to another project means editing configuration.
|
|
400
366
|
|
|
401
367
|
[source,yaml]
|
|
402
368
|
----
|
|
403
369
|
schemaVersion: 1
|
|
404
370
|
|
|
405
371
|
sources:
|
|
406
|
-
root: specs # staged wholesale; the
|
|
372
|
+
root: specs # staged wholesale; the kinds share fragments
|
|
407
373
|
openapi: openapi
|
|
408
374
|
asyncapi: asyncapi
|
|
409
375
|
|
|
@@ -411,6 +377,7 @@ defaults:
|
|
|
411
377
|
openapi:
|
|
412
378
|
lint: .redocly.yaml
|
|
413
379
|
outputName: openapi.yaml
|
|
380
|
+
fragmentPaths: true # x-fragment-path on every component; the default
|
|
414
381
|
asyncapi:
|
|
415
382
|
outputName: asyncapi.yaml
|
|
416
383
|
placeholders:
|
|
@@ -418,7 +385,7 @@ defaults:
|
|
|
418
385
|
|
|
419
386
|
build:
|
|
420
387
|
staging: build/staging
|
|
421
|
-
dist: dist
|
|
388
|
+
dist: build/dist
|
|
422
389
|
reports:
|
|
423
390
|
lint: build/reports/lint
|
|
424
391
|
lint:
|
|
@@ -434,83 +401,145 @@ channels:
|
|
|
434
401
|
clean: true # keep only the version being published
|
|
435
402
|
|
|
436
403
|
targets:
|
|
437
|
-
|
|
438
|
-
# versionFile: versions/
|
|
439
|
-
# default:
|
|
404
|
+
orders:
|
|
405
|
+
# versionFile: versions/orders.properties
|
|
406
|
+
# default: orders.bundle.properties beside the first bundle root
|
|
440
407
|
openapi:
|
|
441
|
-
bundle: bundles/
|
|
408
|
+
bundle: bundles/orders.yaml
|
|
442
409
|
asyncapi:
|
|
443
|
-
bundle:
|
|
410
|
+
bundle: orders_asyncapi.yaml
|
|
444
411
|
portfolio:
|
|
445
|
-
publish: false # built and linted, never
|
|
412
|
+
publish: false # built and linted, never published
|
|
446
413
|
openapi:
|
|
447
|
-
bundle: bundles/
|
|
414
|
+
bundle: bundles/portfolio.yaml
|
|
448
415
|
----
|
|
449
416
|
|
|
450
|
-
`targets` is the single source of truth for what gets built.
|
|
451
|
-
per-script target arrays and the `apis:` map in `redocly.yaml`, which said
|
|
452
|
-
overlapping things in two places under two different names.
|
|
417
|
+
`targets` is the single source of truth for what gets built.
|
|
453
418
|
|
|
419
|
+
[#pinning-the-bundlers]
|
|
454
420
|
=== Pinning the bundlers
|
|
455
421
|
|
|
456
|
-
`toolchain` pins the bundler versions.
|
|
457
|
-
could change because an upstream release happened, with nothing in the library
|
|
458
|
-
|
|
459
|
-
unfalsifiable. An upgrade should be a deliberate commit that moves the pin and
|
|
460
|
-
the expected output together.
|
|
422
|
+
`toolchain` pins the bundler versions.
|
|
423
|
+
Unpinned, the documents this tool produces could change because an upstream release happened, with nothing in the library having changed.
|
|
424
|
+
An upgrade should be a deliberate commit that moves the pin, reviewed with whatever it changes in the published documents.
|
|
461
425
|
|
|
426
|
+
[#behaviour]
|
|
462
427
|
== Behaviour worth knowing about
|
|
463
428
|
|
|
429
|
+
[#unresolved-placeholders]
|
|
464
430
|
=== Unresolved placeholders fail the build
|
|
465
431
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
green build. An unresolved `{{token}}` is now an error. Set
|
|
469
|
-
`defaults.placeholders.strict: false` to get the old leniency, in which case the
|
|
470
|
-
token is left visible rather than replaced with a marker.
|
|
432
|
+
An unresolved `{{token}}` is an error, so a broken contract cannot ship from a green build.
|
|
433
|
+
Set `defaults.placeholders.strict: false` to leave the token visible in the document instead.
|
|
471
434
|
|
|
435
|
+
[#placeholder-search]
|
|
472
436
|
=== Placeholder snippets are found from the source root
|
|
473
437
|
|
|
474
|
-
|
|
475
|
-
from the directory it was told to use, so a snippet one level up was invisible
|
|
476
|
-
and the workaround was to move the Markdown files. The search now starts at the
|
|
477
|
-
staged source root.
|
|
438
|
+
`<token>.md` is searched for from the staged source root downwards, so a snippet can live anywhere under it.
|
|
478
439
|
|
|
479
|
-
[
|
|
440
|
+
[#unreferenced]
|
|
480
441
|
=== A fragment no target reaches fails the lint
|
|
481
442
|
|
|
482
443
|
A linter checks documents, and a fragment reaches a document only through a `$ref`.
|
|
483
444
|
A fragment that no target references is therefore never linted, however wrong it is, until the day a target starts to use it.
|
|
484
445
|
|
|
485
|
-
So `lint`, run without `--target`, also lists every YAML file under the source root that no target's closure reaches, in
|
|
446
|
+
So `lint`, run without `--target`, also lists every YAML file under the source root that no target's closure reaches, in `<reports.lint>/unreferenced.txt`, and fails when there is one.
|
|
486
447
|
Every target counts, `publish: false` ones included, because they are linted too.
|
|
487
448
|
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
449
|
|
|
450
|
+
Configuration is not a fragment, so it may sit anywhere, `sources.root` included.
|
|
451
|
+
The check skips the well-known configuration files of lint tools, wherever they are: `redocly.yaml`, `.redocly.yaml`, `.redocly.lint-ignore.yaml`, `.spectral.yaml`, `.spectral.yml`, and `apionly.yaml`.
|
|
452
|
+
It also skips the lint configurations `defaults.<kind>.lint` names, whatever they are called.
|
|
453
|
+
Any other YAML file under the source root counts.
|
|
454
|
+
|
|
489
455
|
`lint.unreferenced` in `apionly.yaml` sets what happens: `error` by default, `warn` to report without failing, or `off` not to look.
|
|
490
456
|
`lint --target` does not look, because an unreferenced fragment belongs to no target.
|
|
491
457
|
|
|
492
458
|
`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
459
|
|
|
460
|
+
[#version-stamping]
|
|
494
461
|
=== Version stamping edits one scalar
|
|
495
462
|
|
|
496
|
-
`info.version` is located structurally and that one scalar is spliced.
|
|
497
|
-
deliberately not a re-serialisation: re-emitting a document reformats everything
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
463
|
+
`info.version` is located structurally and that one scalar is spliced.
|
|
464
|
+
It is deliberately not a re-serialisation: re-emitting a document reformats everything around the edit, because the AsyncAPI CLI wraps long descriptions at a width no YAML emitter reproduces.
|
|
465
|
+
Splicing keeps every other byte exactly as the bundler wrote it.
|
|
466
|
+
A document with no `info` block, or no version inside it, is an error.
|
|
467
|
+
|
|
468
|
+
[#fragment-paths]
|
|
469
|
+
=== Every component says which fragment it came from
|
|
501
470
|
|
|
471
|
+
Bundling flattens the library's directories into one `components` namespace, and the bundler names each component after its file.
|
|
472
|
+
Two fragments with the same file name in different directories become `UserV1` and `UserV1-2`, and nothing in the document says which is which.
|
|
473
|
+
|
|
474
|
+
So every component the bundler builds from a fragment carries that fragment's path, relative to `sources.root`, as its first key:
|
|
475
|
+
|
|
476
|
+
[source,yaml]
|
|
477
|
+
----
|
|
478
|
+
components:
|
|
479
|
+
schemas:
|
|
480
|
+
UsernameV1:
|
|
481
|
+
x-fragment-path: openapi/components/common/schemas/UsernameV1.yaml
|
|
482
|
+
type: string
|
|
483
|
+
----
|
|
484
|
+
|
|
485
|
+
The path always uses forward slashes, and it names a file in the library.
|
|
486
|
+
Other OpenAPI tooling ignores the key, as it ignores any `x-` extension.
|
|
487
|
+
It is how the API-Only TranscriberJ ties generated code to the fragment it came from, instead of to a component name the bundler picked.
|
|
488
|
+
|
|
489
|
+
It is on by default, for OpenAPI documents only.
|
|
490
|
+
`defaults.openapi.fragmentPaths: false` turns it off.
|
|
491
|
+
|
|
492
|
+
How it is done::
|
|
493
|
+
The bundler is not told anything; it is shown stamped fragments, in copies of the staged tree under `<build.staging>/fragment-paths/<target>/`.
|
|
494
|
+
The first bundle is built with every fragment stamped, to learn which fragments become components.
|
|
495
|
+
The second, the one that is published, is built with only those stamped.
|
|
496
|
+
So a document differs from one built with `fragmentPaths: false` by exactly one line per component, and in no other byte.
|
|
497
|
+
The cost is a second bundler run per OpenAPI target.
|
|
498
|
+
|
|
499
|
+
What stops the build::
|
|
500
|
+
A fragment that writes `x-fragment-path` itself: the Publisher sets that key and nothing else may.
|
|
501
|
+
A component's fragment that is also inlined somewhere else in the same document, since the inlined copy would carry the key as well; the error names where.
|
|
502
|
+
|
|
503
|
+
A component written directly in a bundle root, rather than `$ref`'d from a fragment, has no fragment and carries no path.
|
|
504
|
+
|
|
505
|
+
[#fragment-path-versions]
|
|
506
|
+
==== What a change to `x-fragment-path` means for a contract's version
|
|
507
|
+
|
|
508
|
+
Semantic versioning describes the contract, so the version follows from what a change does to the contract's promises:
|
|
509
|
+
|
|
510
|
+
[cols="2,1,3", options="header"]
|
|
511
|
+
|===
|
|
512
|
+
|Change |Version |Why
|
|
513
|
+
|
|
514
|
+
|The key appears for the first time
|
|
515
|
+
|MINOR
|
|
516
|
+
|A new feature of the published document.
|
|
517
|
+
|
|
518
|
+
|A fragment moves to another directory, changing the value
|
|
519
|
+
|PATCH
|
|
520
|
+
|The contract promises the same; only its provenance metadata differs.
|
|
521
|
+
|
|
522
|
+
|The key is removed, by `fragmentPaths: false` after it was published
|
|
523
|
+
|MAJOR
|
|
524
|
+
|Consumers relying on it lose provenance and collision disambiguation, and may stop generating code at all.
|
|
525
|
+
|===
|
|
526
|
+
|
|
527
|
+
Upgrading to a Publisher that stamps the key changes every OpenAPI document that has components built from fragments.
|
|
528
|
+
`changed` does not see that, because a target's closure hash covers the fragments, not the Publisher.
|
|
529
|
+
Bump each such target's MINOR version in the upgrade's commit, or set `fragmentPaths: false` until you do: publishing a changed document under a version already published is refused by every Subscriber that locked it (see <<versions,Versions §>>).
|
|
530
|
+
|
|
531
|
+
[#distribution]
|
|
502
532
|
=== `distribution` is transitional
|
|
503
533
|
|
|
504
|
-
A producer has no business knowing the directory layout of the projects that
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
Subscriber replaces it: each consuming project declares where its own copy lands,
|
|
508
|
-
and the block goes away.
|
|
534
|
+
A producer has no business knowing the directory layout of the projects that consume it.
|
|
535
|
+
The `distribution` block copies built documents into such a layout, so that a library can adopt this tool before its consumers adopt the API-Only Subscriber.
|
|
536
|
+
Once they subscribe, each consuming project declares where its own copy lands, and the block goes away.
|
|
509
537
|
|
|
538
|
+
[#adapting]
|
|
510
539
|
== Adapting it to another project
|
|
511
540
|
|
|
512
|
-
The tool has no dependency on any particular product, framework or directory
|
|
513
|
-
|
|
541
|
+
The tool has no dependency on any particular product, framework or directory layout.
|
|
542
|
+
In order of what usually needs changing:
|
|
514
543
|
|
|
515
544
|
[cols="2,3", options="header"]
|
|
516
545
|
|===
|
|
@@ -523,16 +552,19 @@ layout. In order of what usually needs changing:
|
|
|
523
552
|
|`sources`
|
|
524
553
|
|
|
525
554
|
|What each document is called
|
|
526
|
-
|`defaults.<
|
|
555
|
+
|`defaults.<kind>.outputName`
|
|
527
556
|
|
|
528
557
|
|The lint rules
|
|
529
558
|
|`defaults.openapi.lint`
|
|
530
559
|
|
|
560
|
+
|Whether components carry `x-fragment-path`
|
|
561
|
+
|`defaults.openapi.fragmentPaths`, defaulting to `true`
|
|
562
|
+
|
|
531
563
|
|Where lint reports go
|
|
532
564
|
|`reports.lint`, defaulting to `build/reports/lint/<target>/<kind>.txt`
|
|
533
565
|
|
|
534
566
|
|Where built documents go
|
|
535
|
-
|`build.dist
|
|
567
|
+
|`build.dist`
|
|
536
568
|
|
|
537
569
|
|Where artifacts are published
|
|
538
570
|
|`channels`
|
|
@@ -541,37 +573,30 @@ layout. In order of what usually needs changing:
|
|
|
541
573
|
|Its version file, or `targets.<target>.versionFile`
|
|
542
574
|
|===
|
|
543
575
|
|
|
544
|
-
Beyond that
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
and hand-author one complete document. The tool only needs one bundle root file
|
|
548
|
-
per target; what that file `$ref`s, or whether it `$ref`s anything at all, is
|
|
549
|
-
entirely up to the library.
|
|
576
|
+
Beyond that, a `components/common/` directory beside per-target directories is one convention for organising a multi-target fragment library, not a requirement.
|
|
577
|
+
A single-target project can keep every fragment in one flat directory, or skip fragments entirely and hand-author one complete document.
|
|
578
|
+
The tool only needs one bundle root file per target; what that file `$ref`s, or whether it `$ref`s anything at all, is up to the library.
|
|
550
579
|
|
|
551
|
-
Placeholders are optional too.
|
|
552
|
-
nothing substituted.
|
|
580
|
+
Placeholders are optional too.
|
|
581
|
+
A library with no `{{token}}` anywhere simply has nothing substituted.
|
|
553
582
|
|
|
583
|
+
[#breaking-changes]
|
|
554
584
|
== Breaking changes
|
|
555
585
|
|
|
556
|
-
The tool does not police them.
|
|
557
|
-
commit altered, and every manifest records the closure hash of what was
|
|
558
|
-
published, so the hooks a diff gate would need already exist -- but the gate
|
|
559
|
-
itself is deliberately not built.
|
|
586
|
+
The tool does not police them.
|
|
587
|
+
`changed` tells a release which targets a commit altered, and every manifest records the closure hash of what was published, so the hooks a diff gate would need already exist, but the gate itself is deliberately not built.
|
|
560
588
|
|
|
561
589
|
A breaking change is declared where it is made: the major version in the target's version file changes in the same commit as the fragments it breaks.
|
|
590
|
+
A specification library should write down its own policy for what counts as breaking.
|
|
562
591
|
|
|
563
|
-
|
|
564
|
-
uses is at `usable-suspects/docs/breaking-changes.adoc`, and is a reasonable
|
|
565
|
-
starting point to copy.
|
|
566
|
-
|
|
592
|
+
[#as-a-library]
|
|
567
593
|
== Using it as a library
|
|
568
594
|
|
|
569
|
-
Everything the CLI does is available programmatically, so a release job that
|
|
570
|
-
wants to decide something for itself does not have to parse console output:
|
|
595
|
+
Everything the CLI does is available programmatically, so a release job that wants to decide something for itself does not have to parse console output:
|
|
571
596
|
|
|
572
597
|
[source,javascript]
|
|
573
598
|
----
|
|
574
|
-
const { loadFrom, prepare,
|
|
599
|
+
const { loadFrom, prepare, changedSince } =
|
|
575
600
|
require('@arc-e-tect/api-only-publisher');
|
|
576
601
|
|
|
577
602
|
const config = loadFrom('.');
|
|
@@ -584,19 +609,20 @@ for (const result of changedSince(config, 'origin/main')) {
|
|
|
584
609
|
}
|
|
585
610
|
----
|
|
586
611
|
|
|
612
|
+
[#exit-codes]
|
|
587
613
|
== Exit codes
|
|
588
614
|
|
|
589
615
|
[cols="1,3",options="header"]
|
|
590
616
|
|===
|
|
591
617
|
| Code | Meaning
|
|
592
618
|
| `0` | Success.
|
|
593
|
-
| `1` | A handled failure: bad configuration, an unresolved placeholder, a
|
|
594
|
-
|
|
595
|
-
refused the artifact. The message says which.
|
|
619
|
+
| `1` | A handled failure: bad configuration, an unresolved placeholder, a document that cannot be stamped, a dangling `$ref`, a lint failure, a channel that refused the artifact.
|
|
620
|
+
The message says which.
|
|
596
621
|
|===
|
|
597
622
|
|
|
598
623
|
Anything else is a bug; please report it.
|
|
599
624
|
|
|
625
|
+
[#tests]
|
|
600
626
|
== Tests
|
|
601
627
|
|
|
602
628
|
[source,console]
|
|
@@ -604,32 +630,22 @@ Anything else is a bug; please report it.
|
|
|
604
630
|
npm test
|
|
605
631
|
----
|
|
606
632
|
|
|
607
|
-
Covers placeholder substitution
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
The end-to-end guarantee lives elsewhere, in the specification library's own
|
|
611
|
-
golden fixtures: they assert that the documents this tool produces are unchanged,
|
|
612
|
-
byte for byte, from the ones the pipeline produced before it existed.
|
|
633
|
+
Covers every module and every command: placeholder substitution, version stamping, version files, configuration parsing, closures, packing, channels, splitting, and the scaffold.
|
|
634
|
+
`build.e2e.test.js` runs the real bundlers.
|
|
635
|
+
The run fails below 90% of lines and functions, or 80% of branches.
|
|
613
636
|
|
|
637
|
+
[#relationship]
|
|
614
638
|
== Relationship to the API-Only Subscriber
|
|
615
639
|
|
|
616
|
-
This half publishes; link:../api-only-subscriber/README.adoc[the Subscriber]
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
together in a real project, see link:../docs/README.adoc[the manuals], which are
|
|
623
|
-
organised by where the API descriptions live relative to the code that implements
|
|
624
|
-
them: link:../docs/in-the-implementation-project.adoc[in the implementation
|
|
625
|
-
project], link:../docs/elsewhere-in-the-repository.adoc[elsewhere in the same
|
|
626
|
-
repository], link:../docs/in-its-own-repository.adoc[in a repository of their
|
|
627
|
-
own]. The first uses only `build` and reads the result directly; the second
|
|
628
|
-
publishes to the `file` channel, and the third to a registry. If you are
|
|
629
|
-
replacing a script that copies a document into `src/main/resources`, start at
|
|
630
|
-
link:../docs/migrating-a-checked-in-document.adoc[Migrating a checked-in
|
|
631
|
-
document].
|
|
640
|
+
This half publishes; link:../api-only-subscriber/README.adoc[the Subscriber →] fetches and verifies.
|
|
641
|
+
They share one seam, an archive plus a `manifest.json`, and nothing else, which is why they can be versioned independently while living in one repository.
|
|
642
|
+
|
|
643
|
+
* link:../docs/README.adoc[The documentation →] helps you pick the use case that fits, with a tutorial and AI prompts for each.
|
|
644
|
+
* link:../docs/reference/how-publisher-and-subscriber-work-together.adoc[How the Publisher and the Subscriber work together →] maps every setting, command and check across the two tools, and says where a change to either belongs.
|
|
645
|
+
* link:../docs/reference/run-book.adoc[The run-book →] covers operating both, and every failure message.
|
|
632
646
|
|
|
647
|
+
[#license]
|
|
633
648
|
== License
|
|
634
649
|
|
|
635
|
-
MIT.
|
|
650
|
+
MIT.
|
|
651
|
+
See link:LICENSE[LICENSE →].
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// Reading and validating apionly.yaml.
|
|
4
4
|
//
|
|
5
|
-
// The configuration is the single source of truth for what
|
|
6
|
-
//
|
|
7
|
-
// overlapping thing under a different name.
|
|
5
|
+
// The configuration is the single source of truth for what a library builds:
|
|
6
|
+
// which targets exist, where their fragments live, and where each document goes.
|
|
8
7
|
|
|
9
8
|
const fs = require("fs");
|
|
10
9
|
const path = require("path");
|
|
@@ -154,8 +153,30 @@ function load(configPath) {
|
|
|
154
153
|
isPublished(target) {
|
|
155
154
|
return this.targets[target].publish !== false;
|
|
156
155
|
},
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
// A target's bundle root in a staged tree: the build's own, unless another
|
|
157
|
+
// staged copy is named.
|
|
158
|
+
bundlePath(target, kind, stagingRoot = this.stagingRoot(kind)) {
|
|
159
|
+
return path.join(
|
|
160
|
+
stagingRoot,
|
|
161
|
+
requireString(this.sources[kind], `sources.${kind}`),
|
|
162
|
+
this.targets[target][kind].bundle);
|
|
163
|
+
},
|
|
164
|
+
// Whether built documents of this kind carry x-fragment-path on each
|
|
165
|
+
// component. On unless turned off; OpenAPI only.
|
|
166
|
+
fragmentPaths(kind) {
|
|
167
|
+
if (kind !== "openapi") return false;
|
|
168
|
+
const configured = (this.defaults.openapi || {}).fragmentPaths;
|
|
169
|
+
if (configured === undefined) return true;
|
|
170
|
+
if (typeof configured !== "boolean") {
|
|
171
|
+
throw new ConfigError(
|
|
172
|
+
`defaults.openapi.fragmentPaths must be true or false, not ${JSON.stringify(configured)}`);
|
|
173
|
+
}
|
|
174
|
+
return configured;
|
|
175
|
+
},
|
|
176
|
+
// Where one target's stamped copies of the staged tree are built.
|
|
177
|
+
fragmentPathStaging(target) {
|
|
178
|
+
const staging = this.build.staging || "build/staging";
|
|
179
|
+
return path.resolve(this.root, staging, "fragment-paths", target);
|
|
159
180
|
},
|
|
160
181
|
// The file a target's version is read from: the target's own `versionFile`,
|
|
161
182
|
// relative to this configuration, or else <target>.bundle.properties beside
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// x-fragment-path: which fragment a bundled component came from.
|
|
4
|
+
//
|
|
5
|
+
// Bundling flattens the library's directory tree into one components namespace,
|
|
6
|
+
// renaming on collision (`UserV1`, `UserV1-2`). The extension puts the provenance
|
|
7
|
+
// back, so a consumer -- the TranscriberJ above all -- never has to read meaning
|
|
8
|
+
// into a bundler's key.
|
|
9
|
+
//
|
|
10
|
+
// The bundler is not told anything; it is shown stamped fragments. It copies a
|
|
11
|
+
// fragment's top-level keys into whatever it produces from that fragment, so a
|
|
12
|
+
// stamp at the top of a file arrives on the component built from it. But the same
|
|
13
|
+
// is true of every fragment the bundler inlines -- paths, info, tags -- so the
|
|
14
|
+
// build stamps in two passes: every fragment first, to learn which ones became
|
|
15
|
+
// components, then only those. See pipeline.bundleWithFragmentPaths.
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const YAML = require("yaml");
|
|
20
|
+
|
|
21
|
+
const KEY = "x-fragment-path";
|
|
22
|
+
|
|
23
|
+
class FragmentPathError extends Error {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stamp YAML fragments under `root`, in place, with their path from `root`.
|
|
27
|
+
*
|
|
28
|
+
* The path uses forward slashes on every platform. A file whose root is not a
|
|
29
|
+
* mapping -- a list of servers, say -- has nowhere to carry the key and is left
|
|
30
|
+
* alone; nothing that becomes a component looks like that.
|
|
31
|
+
*
|
|
32
|
+
* @param {Set<string>|null} only the root-relative paths to stamp; all when null
|
|
33
|
+
* @returns {string[]} the root-relative paths stamped, sorted
|
|
34
|
+
*/
|
|
35
|
+
function stampFiles(root, { only = null } = {}) {
|
|
36
|
+
const stamped = [];
|
|
37
|
+
const walk = (dir) => {
|
|
38
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
39
|
+
const full = path.join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
walk(full);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!/\.ya?ml$/.test(entry.name)) continue;
|
|
45
|
+
const rel = path.relative(root, full).split(path.sep).join("/");
|
|
46
|
+
if (only && !only.has(rel)) continue;
|
|
47
|
+
|
|
48
|
+
const doc = YAML.parseDocument(fs.readFileSync(full, "utf8"));
|
|
49
|
+
if (!YAML.isMap(doc.contents)) continue;
|
|
50
|
+
if (doc.contents.has(KEY)) {
|
|
51
|
+
throw new FragmentPathError(`${rel} declares ${KEY}; the Publisher sets that key and a fragment may not`);
|
|
52
|
+
}
|
|
53
|
+
doc.contents.items.unshift(doc.createPair(KEY, rel));
|
|
54
|
+
fs.writeFileSync(full, doc.toString());
|
|
55
|
+
stamped.push(rel);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
walk(root);
|
|
59
|
+
return stamped;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The stamp on every component of a parsed document, keyed `<type>/<name>`, in
|
|
64
|
+
* document order. A component with no stamp -- one written inline in the bundle
|
|
65
|
+
* root, say -- maps to null.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Map<string, string|null>}
|
|
68
|
+
*/
|
|
69
|
+
function componentPaths(document) {
|
|
70
|
+
const result = new Map();
|
|
71
|
+
for (const [type, entries] of Object.entries(document.components || {})) {
|
|
72
|
+
for (const [name, value] of Object.entries(entries || {})) {
|
|
73
|
+
const fragment = value && typeof value === "object" ? value[KEY] : undefined;
|
|
74
|
+
result.set(`${type}/${name}`, fragment === undefined ? null : fragment);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every stamp in a parsed document that is not directly on a component, with the
|
|
82
|
+
* JSON pointer of the object carrying it.
|
|
83
|
+
*
|
|
84
|
+
* @returns {Array<{at: string, path: string}>}
|
|
85
|
+
*/
|
|
86
|
+
function strayPaths(document) {
|
|
87
|
+
const stray = [];
|
|
88
|
+
const visit = (node, pointer) => {
|
|
89
|
+
if (Array.isArray(node)) {
|
|
90
|
+
node.forEach((item, i) => visit(item, `${pointer}/${i}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!node || typeof node !== "object") return;
|
|
94
|
+
const onComponent = /^\/components\/[^/]+\/[^/]+$/.test(pointer);
|
|
95
|
+
if (Object.hasOwn(node, KEY) && !onComponent) {
|
|
96
|
+
stray.push({ at: pointer || "/", path: node[KEY] });
|
|
97
|
+
}
|
|
98
|
+
for (const [key, value] of Object.entries(node)) {
|
|
99
|
+
if (key === KEY) continue;
|
|
100
|
+
visit(value, `${pointer}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
visit(document, "");
|
|
104
|
+
return stray;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { stampFiles, componentPaths, strayPaths, FragmentPathError, KEY };
|
package/src/index.js
CHANGED
package/src/pipeline.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execFileSync } = require("child_process");
|
|
8
|
+
const YAML = require("yaml");
|
|
8
9
|
|
|
9
10
|
const { substituteFile } = require("./placeholders");
|
|
10
11
|
const { stampFile } = require("./version");
|
|
11
12
|
const { generateAsyncApi, isAggregate } = require("./aggregate");
|
|
13
|
+
const { stampFiles, componentPaths, strayPaths, FragmentPathError, KEY } = require("./fragment-paths");
|
|
12
14
|
|
|
13
15
|
class BuildError extends Error {}
|
|
14
16
|
|
|
@@ -55,12 +57,8 @@ function stage(config, kind, log) {
|
|
|
55
57
|
/**
|
|
56
58
|
* Substitute placeholders across the staged tree, in place.
|
|
57
59
|
*
|
|
58
|
-
* Every YAML file is visited
|
|
59
|
-
*
|
|
60
|
-
* shared info block had to be preprocessed as a separate up-front step and why
|
|
61
|
-
* bundles had to $ref a generated merged_info.yaml instead of the file they
|
|
62
|
-
* meant. Visiting the staged tree removes that special case: a placeholder works
|
|
63
|
-
* wherever it is written.
|
|
60
|
+
* Every YAML file is visited, not only the bundle roots, so a placeholder works
|
|
61
|
+
* wherever it is written, and no file has to be preprocessed separately first.
|
|
64
62
|
*/
|
|
65
63
|
function substituteTree(config, kind, log) {
|
|
66
64
|
const stagingRoot = config.stagingRoot(kind);
|
|
@@ -86,8 +84,8 @@ function substituteTree(config, kind, log) {
|
|
|
86
84
|
log(`-- Substituted ${tokens} placeholder(s) across ${files} file(s)`);
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
function bundle(config, target, kind, outFile, log) {
|
|
90
|
-
const source = config.bundlePath(target, kind);
|
|
87
|
+
function bundle(config, target, kind, outFile, log, { stagingRoot } = {}) {
|
|
88
|
+
const source = config.bundlePath(target, kind, stagingRoot);
|
|
91
89
|
if (!fs.existsSync(source)) {
|
|
92
90
|
throw new BuildError(`target '${target}': bundle root not found at ${source}`);
|
|
93
91
|
}
|
|
@@ -100,6 +98,58 @@ function bundle(config, target, kind, outFile, log) {
|
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Bundle a target with x-fragment-path on every component built from a fragment.
|
|
103
|
+
*
|
|
104
|
+
* Two passes over two fresh copies of the staged tree, so the build's own staged
|
|
105
|
+
* tree -- which every other target bundles from -- is never stamped. The first
|
|
106
|
+
* stamps every fragment and learns which of them the bundler made components of.
|
|
107
|
+
* The second stamps only those, so the document differs from an unstamped one by
|
|
108
|
+
* exactly one line per such component.
|
|
109
|
+
*
|
|
110
|
+
* That holds as long as no component's fragment is also inlined somewhere else;
|
|
111
|
+
* if one is, the inlined copy would carry the stamp too, and the build fails
|
|
112
|
+
* rather than publish it.
|
|
113
|
+
*/
|
|
114
|
+
function bundleWithFragmentPaths(config, target, kind, outFile, log) {
|
|
115
|
+
const scratch = config.fragmentPathStaging(target);
|
|
116
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
117
|
+
|
|
118
|
+
const pass = (name, only) => {
|
|
119
|
+
const root = path.join(scratch, name);
|
|
120
|
+
fs.cpSync(config.stagingRoot(kind), root, { recursive: true });
|
|
121
|
+
try {
|
|
122
|
+
stampFiles(root, { only });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof FragmentPathError) throw new BuildError(`target '${target}': ${error.message}`);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const file = path.join(scratch, `${name}.yaml`);
|
|
128
|
+
bundle(config, target, kind, file, () => {}, { stagingRoot: root });
|
|
129
|
+
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
log(`-- Bundling ${path.basename(config.bundlePath(target, kind))} with ${KEY}`);
|
|
133
|
+
const discovered = componentPaths(pass("discover", null));
|
|
134
|
+
const fragments = new Set([...discovered.values()].filter(Boolean));
|
|
135
|
+
const document = pass("stamp", fragments);
|
|
136
|
+
|
|
137
|
+
const components = componentPaths(document);
|
|
138
|
+
if ([...components.keys()].join("\n") !== [...discovered.keys()].join("\n")) {
|
|
139
|
+
throw new BuildError(`target '${target}': stamping ${KEY} changed the bundler's component names`);
|
|
140
|
+
}
|
|
141
|
+
const stray = strayPaths(document);
|
|
142
|
+
if (stray.length > 0) {
|
|
143
|
+
throw new BuildError(
|
|
144
|
+
`target '${target}': a component's fragment is also inlined elsewhere, so ${KEY} would appear ` +
|
|
145
|
+
`off its component: ${stray.map((s) => `${s.at} (${s.path})`).join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
149
|
+
fs.copyFileSync(path.join(scratch, "stamp.yaml"), outFile);
|
|
150
|
+
log(`-- Stamped ${KEY} on ${[...components.values()].filter(Boolean).length} component(s)`);
|
|
151
|
+
}
|
|
152
|
+
|
|
103
153
|
function lint(config, kind, file, log, { report = false, reportFile = null } = {}) {
|
|
104
154
|
const tool = kind === "openapi" ? config.tool("redocly") : config.tool("asyncapi");
|
|
105
155
|
const args = kind === "openapi"
|
|
@@ -184,7 +234,11 @@ function build(config, { targets, versionOf = () => null, kinds = ["openapi", "a
|
|
|
184
234
|
generateAsyncApi(config, target, { log });
|
|
185
235
|
}
|
|
186
236
|
const outFile = path.join(config.distDir(target), config.outputName(kind));
|
|
187
|
-
|
|
237
|
+
if (config.fragmentPaths(kind)) {
|
|
238
|
+
bundleWithFragmentPaths(config, target, kind, outFile, log);
|
|
239
|
+
} else {
|
|
240
|
+
bundle(config, target, kind, outFile, log);
|
|
241
|
+
}
|
|
188
242
|
const version = versionOf(target);
|
|
189
243
|
if (version) {
|
|
190
244
|
log(`-- Stamping version '${version}'`);
|
|
@@ -204,4 +258,4 @@ function build(config, { targets, versionOf = () => null, kinds = ["openapi", "a
|
|
|
204
258
|
return results;
|
|
205
259
|
}
|
|
206
260
|
|
|
207
|
-
module.exports = { build, prepare, stage, substituteTree, bundle, lint, distribute, BuildError };
|
|
261
|
+
module.exports = { build, prepare, stage, substituteTree, bundle, bundleWithFragmentPaths, lint, distribute, BuildError };
|
package/src/placeholders.js
CHANGED
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// Placeholder substitution.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// A {{token}} in a staged YAML file is replaced by the contents of <token>.md.
|
|
6
|
+
// Three choices here are deliberate:
|
|
7
7
|
//
|
|
8
|
-
// 1. Search scope.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// a green build. Unresolved placeholders are now an error by default.
|
|
17
|
-
// 3. Token grammar. The pattern \{\{(\w+)\}\} silently excluded '-' and '.'
|
|
18
|
-
// from placeholder names, so {{status-codes}} was left in the output rather
|
|
19
|
-
// than reported. The grammar now admits them.
|
|
8
|
+
// 1. Search scope. <token>.md is searched for from the root the caller names,
|
|
9
|
+
// the staged source root by default, not from the directory of the file
|
|
10
|
+
// holding the token, so a snippet can live anywhere under that root.
|
|
11
|
+
// 2. Failure mode. An unresolved placeholder is an error by default. Leaving a
|
|
12
|
+
// marker in the document and carrying on would let a broken contract ship
|
|
13
|
+
// from a green build.
|
|
14
|
+
// 3. Token grammar. Names may contain '-' and '.', so {{status-codes}} is a
|
|
15
|
+
// placeholder like any other, never text silently left in the output.
|
|
20
16
|
|
|
21
17
|
const fs = require("fs");
|
|
22
18
|
const path = require("path");
|
|
@@ -103,9 +99,8 @@ function substitute(text, { searchRoot, strict = true, describeAs = "input" } =
|
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
/**
|
|
106
|
-
* Substitute a file in place.
|
|
107
|
-
*
|
|
108
|
-
* because it is always a staged copy.
|
|
102
|
+
* Substitute a file in place. The file is rewritten where it stands and nothing
|
|
103
|
+
* is written beside it, which is safe because it is always a staged copy.
|
|
109
104
|
*/
|
|
110
105
|
function substituteFile(file, options) {
|
|
111
106
|
const before = fs.readFileSync(file, "utf8");
|
package/src/split.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The problem it solves is real and easy to miss. A fragment library is one $ref
|
|
6
6
|
// graph, and that graph does not respect the directory boundaries a repository
|
|
7
|
-
// split would follow.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// repositories as they stand and every
|
|
7
|
+
// split would follow. An AsyncAPI event schema that reuses an OpenAPI common
|
|
8
|
+
// schema -- a username, say -- means the same thing over a message broker as over
|
|
9
|
+
// HTTP, and that reuse is a reference across the two trees. Move the trees into
|
|
10
|
+
// separate repositories as they stand and every such reference dangles.
|
|
11
11
|
//
|
|
12
12
|
// `split` resolves each part's dependency closure and materialises it whole:
|
|
13
13
|
// each part gets its own subtree *plus* a copy of every foreign file it reaches,
|
package/src/unreferenced.js
CHANGED
|
@@ -25,6 +25,18 @@ function yamlFiles(dir) {
|
|
|
25
25
|
|
|
26
26
|
const portable = (file) => file.split(path.sep).join("/");
|
|
27
27
|
|
|
28
|
+
// Configuration files of the tools an API library is linted with, and this tool's
|
|
29
|
+
// own. They are YAML, but they describe how to check an API, not the API, so they
|
|
30
|
+
// are never fragments, wherever they sit and whether or not apionly.yaml names them.
|
|
31
|
+
const TOOL_CONFIGS = new Set([
|
|
32
|
+
"apionly.yaml",
|
|
33
|
+
"redocly.yaml",
|
|
34
|
+
".redocly.yaml",
|
|
35
|
+
".redocly.lint-ignore.yaml",
|
|
36
|
+
".spectral.yaml",
|
|
37
|
+
".spectral.yml",
|
|
38
|
+
]);
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* Every YAML file under the source root that no target's closure reaches,
|
|
30
42
|
* relative to the source root and sorted.
|
|
@@ -33,6 +45,9 @@ const portable = (file) => file.split(path.sep).join("/");
|
|
|
33
45
|
* linted like any other target, so whatever it reaches is linted too. The staged
|
|
34
46
|
* tree must already exist, as for forTargets.
|
|
35
47
|
*
|
|
48
|
+
* Configuration is not a fragment: neither the well-known configuration files of
|
|
49
|
+
* lint tools, nor the lint configuration apionly.yaml names, whatever it is called.
|
|
50
|
+
*
|
|
36
51
|
* @returns {string[]}
|
|
37
52
|
*/
|
|
38
53
|
function unreferenced(config) {
|
|
@@ -44,9 +59,13 @@ function unreferenced(config) {
|
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
const sourceRoot = config.sourceRoot();
|
|
62
|
+
const own = new Set(["openapi", "asyncapi"]
|
|
63
|
+
.map((kind) => config.lintConfig(kind))
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((file) => portable(path.relative(sourceRoot, file))));
|
|
47
66
|
return yamlFiles(sourceRoot)
|
|
48
67
|
.map((file) => portable(path.relative(sourceRoot, file)))
|
|
49
|
-
.filter((file) => !reached.has(file))
|
|
68
|
+
.filter((file) => !reached.has(file) && !own.has(file) && !TOOL_CONFIGS.has(path.posix.basename(file)))
|
|
50
69
|
.sort();
|
|
51
70
|
}
|
|
52
71
|
|
package/src/version.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// Setting info.version on a bundled document.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// The field is located structurally and only that one scalar is spliced. It is
|
|
5
|
+
// A pattern-based substitution would have to differ per specification type,
|
|
6
|
+
// would silently do nothing when its pattern missed, and could match a
|
|
7
|
+
// `version:` field elsewhere in the document. So the field is located
|
|
8
|
+
// structurally, and only that one scalar is spliced. It is
|
|
11
9
|
// deliberately not a re-serialisation: re-emitting the document reformats
|
|
12
10
|
// everything around the edit, because the AsyncAPI CLI wraps long descriptions at
|
|
13
11
|
// a width no YAML emitter reproduces. Splicing keeps every other byte exactly as
|