@arc-e-tect/api-only-publisher 0.2.0 → 0.3.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 +205 -257
- package/package.json +1 -1
- package/src/config.js +2 -3
- package/src/pipeline.js +2 -6
- 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,90 @@ 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
|
-
`$ref` into one flat, self-contained document under `dist/<target>/`.
|
|
192
|
+
. **Bundle.** `@redocly/cli bundle` or `@asyncapi/cli bundle` resolves every `$ref` into one flat, self-contained document under `<build.dist>/<target>/`.
|
|
196
193
|
|
|
197
194
|
. **Stamp the version** of each published target on its *finished* document: the version its version file declares, with any `--pre-release` identifiers appended.
|
|
198
195
|
A `publish: false` target keeps the version its source declares.
|
|
199
196
|
|
|
200
|
-
. **Lint** the finished document, so a broken fragment surfaces against the
|
|
201
|
-
target it actually affects rather than at deploy time.
|
|
197
|
+
. **Lint** the finished document, so a broken fragment surfaces against the target it actually affects.
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
[[versions]]
|
|
199
|
+
[#versions]
|
|
206
200
|
== Versions
|
|
207
201
|
|
|
208
202
|
Every published target has a version of its own, and it is kept in the library, beside the fragments it describes.
|
|
209
203
|
|
|
210
|
-
.specs/openapi/bundles/
|
|
204
|
+
.specs/openapi/bundles/orders.bundle.properties
|
|
211
205
|
[source,properties]
|
|
212
206
|
----
|
|
213
|
-
# The version of the
|
|
207
|
+
# The version of the orders contract, for every document it builds.
|
|
214
208
|
# Semantic: major for a breaking change, minor for an additive one, patch for
|
|
215
209
|
# anything else. Change it in the same commit as the fragments it describes.
|
|
216
210
|
version=2.1.0
|
|
@@ -236,79 +230,61 @@ A version names one set of documents.
|
|
|
236
230
|
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
231
|
Change the version in the same commit as the fragments instead.
|
|
238
232
|
|
|
233
|
+
[#closure]
|
|
239
234
|
== Independent versioning, and the dependency closure
|
|
240
235
|
|
|
241
|
-
Giving each target its own version has one non-obvious consequence, and the
|
|
242
|
-
`closure` and `changed` commands are what pay for it.
|
|
236
|
+
Giving each target its own version has one non-obvious consequence, and the `closure` and `changed` commands are what pay for it.
|
|
243
237
|
|
|
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.
|
|
238
|
+
A change under `components/common/` affects every target that reaches it.
|
|
239
|
+
A change under one target's own fragments affects only that one.
|
|
240
|
+
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
241
|
|
|
251
|
-
A target's *closure* is every file reachable from its bundle root by following
|
|
252
|
-
|
|
253
|
-
which targets a commit actually touched:
|
|
242
|
+
A target's *closure* is every file reachable from its bundle root by following `$ref`s transitively.
|
|
243
|
+
`changed --since <ref>` resolves each closure and reports which targets a change actually touched:
|
|
254
244
|
|
|
255
245
|
[source,console]
|
|
256
246
|
----
|
|
257
|
-
|
|
247
|
+
npm run apionly -- changed --since origin/main
|
|
258
248
|
----
|
|
259
249
|
|
|
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.
|
|
250
|
+
Two things make this correct rather than approximate.
|
|
251
|
+
The closure is computed over the *staged, substituted* tree, so a change to a Markdown snippet, which genuinely changes the published document, counts.
|
|
252
|
+
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
253
|
|
|
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.
|
|
254
|
+
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
255
|
|
|
256
|
+
[#splitting]
|
|
269
257
|
== Splitting the library
|
|
270
258
|
|
|
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.
|
|
259
|
+
A fragment library is one `$ref` graph, and that graph does not respect the directory boundaries a repository split would follow.
|
|
260
|
+
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
261
|
|
|
277
262
|
[source,console]
|
|
278
263
|
----
|
|
279
|
-
|
|
264
|
+
npm run apionly -- split --out build/split --by kind
|
|
280
265
|
----
|
|
281
266
|
|
|
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.
|
|
267
|
+
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.
|
|
268
|
+
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
269
|
|
|
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.
|
|
270
|
+
`--by kind` splits along the specification kinds.
|
|
271
|
+
`--by target` gives every target its own self-contained tree, which is what you want before splitting into per-target repositories.
|
|
292
272
|
|
|
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.
|
|
273
|
+
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.
|
|
274
|
+
A shared schema that two repositories both define is two schemas that happen to agree today.
|
|
275
|
+
Where that matters, promote the fragment to a contract of its own, published and consumed like any other, rather than copied.
|
|
298
276
|
|
|
277
|
+
[#aggregates]
|
|
299
278
|
== Aggregates
|
|
300
279
|
|
|
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.
|
|
280
|
+
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
281
|
|
|
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.
|
|
282
|
+
AsyncAPI cannot be written that way.
|
|
283
|
+
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.
|
|
284
|
+
A hand-written async portfolio would have to copy every operation verbatim, and that copy would start rotting the moment a member changed.
|
|
310
285
|
|
|
311
|
-
So it is generated instead.
|
|
286
|
+
So it is generated instead.
|
|
287
|
+
Declare the members and the aggregate's own identity:
|
|
312
288
|
|
|
313
289
|
[source,yaml]
|
|
314
290
|
----
|
|
@@ -317,17 +293,17 @@ portfolio:
|
|
|
317
293
|
asyncapi:
|
|
318
294
|
bundle: portfolio_asyncapi_structure.yaml
|
|
319
295
|
aggregate:
|
|
320
|
-
-
|
|
296
|
+
- orders
|
|
297
|
+
- payments
|
|
321
298
|
info:
|
|
322
299
|
title: Everything, together
|
|
323
300
|
version: 0.0.0
|
|
324
301
|
----
|
|
325
302
|
|
|
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.
|
|
303
|
+
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.
|
|
304
|
+
Two members contributing the same channel or operation key is an error rather than a silent overwrite.
|
|
330
305
|
|
|
306
|
+
[#channels]
|
|
331
307
|
== Channels
|
|
332
308
|
|
|
333
309
|
[cols="1,3", options="header"]
|
|
@@ -336,74 +312,63 @@ rather than a silent overwrite.
|
|
|
336
312
|
|
|
337
313
|
|`file`
|
|
338
314
|
|A local directory, laid out as `<directory>/<target>/<version>/`.
|
|
339
|
-
Not a distribution mechanism between repositories: it
|
|
315
|
+
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
316
|
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
317
|
|
|
342
318
|
|`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.
|
|
319
|
+
|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.
|
|
320
|
+
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
321
|
|
|
348
322
|
|`npm`
|
|
349
|
-
|Real semver, integrity hashes
|
|
350
|
-
toolchain here is already Node.
|
|
323
|
+
|Real semver, integrity hashes, private scopes, and a toolchain that is already Node.js.
|
|
351
324
|
|
|
352
325
|
|`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.
|
|
326
|
+
|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.
|
|
327
|
+
No dependency-resolution semantics and no update notification, which is why it is the fallback rather than the default.
|
|
357
328
|
|===
|
|
358
329
|
|
|
359
330
|
Credentials are read from the environment, never from configuration.
|
|
360
|
-
`channels.maven.tokenEnv` names the variable; a configuration file gets
|
|
361
|
-
|
|
331
|
+
`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.
|
|
332
|
+
|
|
333
|
+
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
334
|
|
|
335
|
+
[#why-registries]
|
|
363
336
|
=== Why npm and Maven rather than release assets alone
|
|
364
337
|
|
|
365
|
-
Publishing is passive.
|
|
366
|
-
implementation repository until somebody looks.
|
|
338
|
+
Publishing is passive.
|
|
339
|
+
The producer releases `2.1.0` and nothing happens in an implementation repository until somebody looks.
|
|
367
340
|
|
|
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.
|
|
341
|
+
Renovate and Dependabot understand npm and Maven natively, so a contract bump arrives downstream as a pull request without any bespoke machinery.
|
|
342
|
+
That is the practical argument for preferring them, and the reason to design the coordinates so that standard update bots can read them.
|
|
372
343
|
|
|
344
|
+
[#pre-releases]
|
|
373
345
|
== Pre-releases
|
|
374
346
|
|
|
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.
|
|
347
|
+
API-first design means implementation starts against a contract that is not finished.
|
|
348
|
+
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
349
|
|
|
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.
|
|
350
|
+
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
351
|
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
352
|
|
|
385
|
-
The hard rule is the other half of it.
|
|
386
|
-
production build:
|
|
353
|
+
The hard rule is the other half of it.
|
|
354
|
+
A pre-release must never quietly satisfy a production build:
|
|
387
355
|
|
|
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.
|
|
356
|
+
* npm publishes it under the `next` dist-tag, never `latest`, so `npm install` cannot pick one up by accident.
|
|
357
|
+
* A GitHub release is marked as a pre-release, so "latest release" never resolves to an unfinished contract.
|
|
358
|
+
* 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
359
|
|
|
360
|
+
[#configuration]
|
|
396
361
|
== Configuration
|
|
397
362
|
|
|
398
|
-
Everything project-specific lives in `apionly.yaml`, not in the code.
|
|
399
|
-
the tool to another project means editing configuration.
|
|
363
|
+
Everything project-specific lives in `apionly.yaml`, not in the code.
|
|
364
|
+
Adapting the tool to another project means editing configuration.
|
|
400
365
|
|
|
401
366
|
[source,yaml]
|
|
402
367
|
----
|
|
403
368
|
schemaVersion: 1
|
|
404
369
|
|
|
405
370
|
sources:
|
|
406
|
-
root: specs # staged wholesale; the
|
|
371
|
+
root: specs # staged wholesale; the kinds share fragments
|
|
407
372
|
openapi: openapi
|
|
408
373
|
asyncapi: asyncapi
|
|
409
374
|
|
|
@@ -418,7 +383,7 @@ defaults:
|
|
|
418
383
|
|
|
419
384
|
build:
|
|
420
385
|
staging: build/staging
|
|
421
|
-
dist: dist
|
|
386
|
+
dist: build/dist
|
|
422
387
|
reports:
|
|
423
388
|
lint: build/reports/lint
|
|
424
389
|
lint:
|
|
@@ -434,83 +399,82 @@ channels:
|
|
|
434
399
|
clean: true # keep only the version being published
|
|
435
400
|
|
|
436
401
|
targets:
|
|
437
|
-
|
|
438
|
-
# versionFile: versions/
|
|
439
|
-
# default:
|
|
402
|
+
orders:
|
|
403
|
+
# versionFile: versions/orders.properties
|
|
404
|
+
# default: orders.bundle.properties beside the first bundle root
|
|
440
405
|
openapi:
|
|
441
|
-
bundle: bundles/
|
|
406
|
+
bundle: bundles/orders.yaml
|
|
442
407
|
asyncapi:
|
|
443
|
-
bundle:
|
|
408
|
+
bundle: orders_asyncapi.yaml
|
|
444
409
|
portfolio:
|
|
445
|
-
publish: false # built and linted, never
|
|
410
|
+
publish: false # built and linted, never published
|
|
446
411
|
openapi:
|
|
447
|
-
bundle: bundles/
|
|
412
|
+
bundle: bundles/portfolio.yaml
|
|
448
413
|
----
|
|
449
414
|
|
|
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.
|
|
415
|
+
`targets` is the single source of truth for what gets built.
|
|
453
416
|
|
|
417
|
+
[#pinning-the-bundlers]
|
|
454
418
|
=== Pinning the bundlers
|
|
455
419
|
|
|
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.
|
|
420
|
+
`toolchain` pins the bundler versions.
|
|
421
|
+
Unpinned, the documents this tool produces could change because an upstream release happened, with nothing in the library having changed.
|
|
422
|
+
An upgrade should be a deliberate commit that moves the pin, reviewed with whatever it changes in the published documents.
|
|
461
423
|
|
|
424
|
+
[#behaviour]
|
|
462
425
|
== Behaviour worth knowing about
|
|
463
426
|
|
|
427
|
+
[#unresolved-placeholders]
|
|
464
428
|
=== Unresolved placeholders fail the build
|
|
465
429
|
|
|
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.
|
|
430
|
+
An unresolved `{{token}}` is an error, so a broken contract cannot ship from a green build.
|
|
431
|
+
Set `defaults.placeholders.strict: false` to leave the token visible in the document instead.
|
|
471
432
|
|
|
433
|
+
[#placeholder-search]
|
|
472
434
|
=== Placeholder snippets are found from the source root
|
|
473
435
|
|
|
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.
|
|
436
|
+
`<token>.md` is searched for from the staged source root downwards, so a snippet can live anywhere under it.
|
|
478
437
|
|
|
479
|
-
[
|
|
438
|
+
[#unreferenced]
|
|
480
439
|
=== A fragment no target reaches fails the lint
|
|
481
440
|
|
|
482
441
|
A linter checks documents, and a fragment reaches a document only through a `$ref`.
|
|
483
442
|
A fragment that no target references is therefore never linted, however wrong it is, until the day a target starts to use it.
|
|
484
443
|
|
|
485
|
-
So `lint`, run without `--target`, also lists every YAML file under the source root that no target's closure reaches, in
|
|
444
|
+
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
445
|
Every target counts, `publish: false` ones included, because they are linted too.
|
|
487
446
|
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
447
|
|
|
448
|
+
Configuration is not a fragment, so it may sit anywhere, `sources.root` included.
|
|
449
|
+
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`.
|
|
450
|
+
It also skips the lint configurations `defaults.<kind>.lint` names, whatever they are called.
|
|
451
|
+
Any other YAML file under the source root counts.
|
|
452
|
+
|
|
489
453
|
`lint.unreferenced` in `apionly.yaml` sets what happens: `error` by default, `warn` to report without failing, or `off` not to look.
|
|
490
454
|
`lint --target` does not look, because an unreferenced fragment belongs to no target.
|
|
491
455
|
|
|
492
456
|
`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
457
|
|
|
458
|
+
[#version-stamping]
|
|
494
459
|
=== Version stamping edits one scalar
|
|
495
460
|
|
|
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
|
-
wrote it. A document with no `info` block, or no version inside it, is an error.
|
|
461
|
+
`info.version` is located structurally and that one scalar is spliced.
|
|
462
|
+
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.
|
|
463
|
+
Splicing keeps every other byte exactly as the bundler wrote it.
|
|
464
|
+
A document with no `info` block, or no version inside it, is an error.
|
|
501
465
|
|
|
466
|
+
[#distribution]
|
|
502
467
|
=== `distribution` is transitional
|
|
503
468
|
|
|
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.
|
|
469
|
+
A producer has no business knowing the directory layout of the projects that consume it.
|
|
470
|
+
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.
|
|
471
|
+
Once they subscribe, each consuming project declares where its own copy lands, and the block goes away.
|
|
509
472
|
|
|
473
|
+
[#adapting]
|
|
510
474
|
== Adapting it to another project
|
|
511
475
|
|
|
512
|
-
The tool has no dependency on any particular product, framework or directory
|
|
513
|
-
|
|
476
|
+
The tool has no dependency on any particular product, framework or directory layout.
|
|
477
|
+
In order of what usually needs changing:
|
|
514
478
|
|
|
515
479
|
[cols="2,3", options="header"]
|
|
516
480
|
|===
|
|
@@ -523,7 +487,7 @@ layout. In order of what usually needs changing:
|
|
|
523
487
|
|`sources`
|
|
524
488
|
|
|
525
489
|
|What each document is called
|
|
526
|
-
|`defaults.<
|
|
490
|
+
|`defaults.<kind>.outputName`
|
|
527
491
|
|
|
528
492
|
|The lint rules
|
|
529
493
|
|`defaults.openapi.lint`
|
|
@@ -532,7 +496,7 @@ layout. In order of what usually needs changing:
|
|
|
532
496
|
|`reports.lint`, defaulting to `build/reports/lint/<target>/<kind>.txt`
|
|
533
497
|
|
|
534
498
|
|Where built documents go
|
|
535
|
-
|`build.dist
|
|
499
|
+
|`build.dist`
|
|
536
500
|
|
|
537
501
|
|Where artifacts are published
|
|
538
502
|
|`channels`
|
|
@@ -541,37 +505,30 @@ layout. In order of what usually needs changing:
|
|
|
541
505
|
|Its version file, or `targets.<target>.versionFile`
|
|
542
506
|
|===
|
|
543
507
|
|
|
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.
|
|
508
|
+
Beyond that, a `components/common/` directory beside per-target directories is one convention for organising a multi-target fragment library, not a requirement.
|
|
509
|
+
A single-target project can keep every fragment in one flat directory, or skip fragments entirely and hand-author one complete document.
|
|
510
|
+
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
511
|
|
|
551
|
-
Placeholders are optional too.
|
|
552
|
-
nothing substituted.
|
|
512
|
+
Placeholders are optional too.
|
|
513
|
+
A library with no `{{token}}` anywhere simply has nothing substituted.
|
|
553
514
|
|
|
515
|
+
[#breaking-changes]
|
|
554
516
|
== Breaking changes
|
|
555
517
|
|
|
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.
|
|
518
|
+
The tool does not police them.
|
|
519
|
+
`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
520
|
|
|
561
521
|
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.
|
|
522
|
+
A specification library should write down its own policy for what counts as breaking.
|
|
562
523
|
|
|
563
|
-
|
|
564
|
-
uses is at `usable-suspects/docs/breaking-changes.adoc`, and is a reasonable
|
|
565
|
-
starting point to copy.
|
|
566
|
-
|
|
524
|
+
[#as-a-library]
|
|
567
525
|
== Using it as a library
|
|
568
526
|
|
|
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:
|
|
527
|
+
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
528
|
|
|
572
529
|
[source,javascript]
|
|
573
530
|
----
|
|
574
|
-
const { loadFrom, prepare,
|
|
531
|
+
const { loadFrom, prepare, changedSince } =
|
|
575
532
|
require('@arc-e-tect/api-only-publisher');
|
|
576
533
|
|
|
577
534
|
const config = loadFrom('.');
|
|
@@ -584,19 +541,20 @@ for (const result of changedSince(config, 'origin/main')) {
|
|
|
584
541
|
}
|
|
585
542
|
----
|
|
586
543
|
|
|
544
|
+
[#exit-codes]
|
|
587
545
|
== Exit codes
|
|
588
546
|
|
|
589
547
|
[cols="1,3",options="header"]
|
|
590
548
|
|===
|
|
591
549
|
| Code | Meaning
|
|
592
550
|
| `0` | Success.
|
|
593
|
-
| `1` | A handled failure: bad configuration, an unresolved placeholder, a
|
|
594
|
-
|
|
595
|
-
refused the artifact. The message says which.
|
|
551
|
+
| `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.
|
|
552
|
+
The message says which.
|
|
596
553
|
|===
|
|
597
554
|
|
|
598
555
|
Anything else is a bug; please report it.
|
|
599
556
|
|
|
557
|
+
[#tests]
|
|
600
558
|
== Tests
|
|
601
559
|
|
|
602
560
|
[source,console]
|
|
@@ -604,32 +562,22 @@ Anything else is a bug; please report it.
|
|
|
604
562
|
npm test
|
|
605
563
|
----
|
|
606
564
|
|
|
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.
|
|
565
|
+
Covers every module and every command: placeholder substitution, version stamping, version files, configuration parsing, closures, packing, channels, splitting, and the scaffold.
|
|
566
|
+
`build.e2e.test.js` runs the real bundlers.
|
|
567
|
+
The run fails below 90% of lines and functions, or 80% of branches.
|
|
613
568
|
|
|
569
|
+
[#relationship]
|
|
614
570
|
== Relationship to the API-Only Subscriber
|
|
615
571
|
|
|
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].
|
|
572
|
+
This half publishes; link:../api-only-subscriber/README.adoc[the Subscriber →] fetches and verifies.
|
|
573
|
+
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.
|
|
574
|
+
|
|
575
|
+
* link:../docs/README.adoc[The documentation →] helps you pick the use case that fits, with a tutorial and AI prompts for each.
|
|
576
|
+
* 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.
|
|
577
|
+
* link:../docs/reference/run-book.adoc[The run-book →] covers operating both, and every failure message.
|
|
632
578
|
|
|
579
|
+
[#license]
|
|
633
580
|
== License
|
|
634
581
|
|
|
635
|
-
MIT.
|
|
582
|
+
MIT.
|
|
583
|
+
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");
|
package/src/pipeline.js
CHANGED
|
@@ -55,12 +55,8 @@ function stage(config, kind, log) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Substitute placeholders across the staged tree, in place.
|
|
57
57
|
*
|
|
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.
|
|
58
|
+
* Every YAML file is visited, not only the bundle roots, so a placeholder works
|
|
59
|
+
* wherever it is written, and no file has to be preprocessed separately first.
|
|
64
60
|
*/
|
|
65
61
|
function substituteTree(config, kind, log) {
|
|
66
62
|
const stagingRoot = config.stagingRoot(kind);
|
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
|