@heroiclands/package-build 10.0.1 → 11.0.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/CHANGELOG.md +279 -0
- package/CONTENT.md +218 -71
- package/MIGRATING.md +64 -0
- package/bin/content-build.mjs +59 -75
- package/docs/content-format.md +90 -67
- package/engine/base-compiler.mjs +7 -1
- package/engine/content-address.mjs +71 -18
- package/engine/content-format-check.mjs +1 -1
- package/engine/content-links.mjs +93 -112
- package/engine/content-lint.mjs +14 -10
- package/engine/content-slug.mjs +39 -105
- package/engine/diagnostics.mjs +16 -2
- package/engine/frontmatter-lint.mjs +26 -13
- package/engine/helpers.mjs +31 -68
- package/engine/homepage.mjs +131 -86
- package/engine/index.mjs +2 -5
- package/engine/manifest-emit.mjs +23 -4
- package/engine/note-vocabulary.mjs +58 -1
- package/engine/retired-fields.mjs +117 -6
- package/engine/site-build.mjs +182 -59
- package/engine/site-index.mjs +57 -102
- package/engine/web-wikilinks.mjs +183 -127
- package/engine/wikilink-syntax.mjs +174 -34
- package/engine/wikilinks.mjs +159 -117
- package/package.json +1 -1
- package/types/engine/base-compiler.d.mts +1 -1
- package/types/engine/content-address.d.mts +46 -14
- package/types/engine/content-links.d.mts +13 -17
- package/types/engine/content-slug.d.mts +11 -48
- package/types/engine/diagnostics.d.mts +14 -1
- package/types/engine/helpers.d.mts +4 -3
- package/types/engine/homepage.d.mts +96 -60
- package/types/engine/index.d.mts +0 -1
- package/types/engine/note-vocabulary.d.mts +43 -0
- package/types/engine/retired-fields.d.mts +78 -1
- package/types/engine/site-build.d.mts +70 -17
- package/types/engine/site-index.d.mts +19 -21
- package/types/engine/web-wikilinks.d.mts +29 -28
- package/types/engine/wikilink-syntax.d.mts +126 -40
- package/types/engine/wikilinks.d.mts +29 -24
- package/engine/abbreviations.mjs +0 -0
- package/engine/alias-index.mjs +0 -153
- package/types/engine/abbreviations.d.mts +0 -44
- package/types/engine/alias-index.d.mts +0 -122
package/engine/site-index.mjs
CHANGED
|
@@ -24,23 +24,20 @@
|
|
|
24
24
|
* scheme, the section a note is filed under, whether developer docs are part of
|
|
25
25
|
* the site at all — those genuinely differ, and the two builds differ on all
|
|
26
26
|
* three. So this takes *entries that already know their own URL* and does the
|
|
27
|
-
* part that is the same everywhere: index them,
|
|
28
|
-
*
|
|
27
|
+
* part that is the same everywhere: index them, merge the foreign packages in,
|
|
28
|
+
* and report what cannot be addressed unambiguously.
|
|
29
29
|
*
|
|
30
|
-
* **
|
|
30
|
+
* **Two key spaces, one map**, and both are addresses. `section/slug` and
|
|
31
|
+
* `type/shortcode` are unique by construction, so they always resolve.
|
|
32
|
+
* `type/shortcode` is the authored form; the canonical
|
|
33
|
+
* `package-type-shortcode` is set alongside it, which is what a cross-package
|
|
34
|
+
* link and every merged foreign entry use (#1499).
|
|
31
35
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* that would map to two different pages is dropped and remembered, so
|
|
38
|
-
* `[[Name]]` on it fails the build rather than silently picking one. The
|
|
39
|
-
* author disambiguates with `[[section/slug|Label]]`.
|
|
40
|
-
* - Aliases are indexed **scoped to their type**, which is what makes a bare
|
|
41
|
-
* `[[Shock]]` resolvable when "Shock" is both a rules page and a trauma item.
|
|
42
|
-
* Two notes *of the same type* sharing a name poison it, and the author
|
|
43
|
-
* writes `[[type/shortcode|Text]]`.
|
|
36
|
+
* **A page's *name* is not a key** (#180). It was, as one of a set of
|
|
37
|
+
* collision-aware fallbacks a bare `[[Name]]` was looked up in — which is what
|
|
38
|
+
* made two pages of one type forbidden from sharing a display name (#179). The
|
|
39
|
+
* bare form is retired, so the fallbacks answer nothing and the constraint they
|
|
40
|
+
* imposed is gone with them.
|
|
44
41
|
*
|
|
45
42
|
* **It reports rather than exits.** A build script owns its diagnostics and its
|
|
46
43
|
* exit code; this returns what it found. That is the same rule the rest of the
|
|
@@ -54,9 +51,8 @@ import path from "node:path";
|
|
|
54
51
|
import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
|
|
55
52
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
56
53
|
import { contentPackage } from "./content-package.mjs";
|
|
57
|
-
// The
|
|
58
|
-
|
|
59
|
-
import { aliasesOf, indexAliases } from "./alias-index.mjs";
|
|
54
|
+
// The declared tag vocabulary (#172), which is where `draft` is stated.
|
|
55
|
+
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
60
56
|
|
|
61
57
|
/**
|
|
62
58
|
* One page the site will publish, as the index needs to see it.
|
|
@@ -79,11 +75,13 @@ import { aliasesOf, indexAliases } from "./alias-index.mjs";
|
|
|
79
75
|
* The resolved index and everything a wikilink resolver reads beside it.
|
|
80
76
|
*
|
|
81
77
|
* @typedef {object} SiteIndex
|
|
82
|
-
* @property {Map<string, {url: string, name?: string}>} index
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @property {Set<string>}
|
|
78
|
+
* @property {Map<string, {url: string, name?: string, draft?: boolean}>} index
|
|
79
|
+
* Address → page. `draft` says the page
|
|
80
|
+
* carries the `draft` tag, which marks a
|
|
81
|
+
* link *into* it (#183).
|
|
82
|
+
* @property {Set<string>} ambiguous Short addresses claimed by two
|
|
83
|
+
* packages, and so deliberately absent
|
|
84
|
+
* from `index`.
|
|
87
85
|
* @property {Set<string>} contentTypes Every type the resolver should read as
|
|
88
86
|
* an address qualifier, local and foreign.
|
|
89
87
|
* @property {Set<string>} sections Section names, lowercased.
|
|
@@ -95,31 +93,6 @@ import { aliasesOf, indexAliases } from "./alias-index.mjs";
|
|
|
95
93
|
* build failure; the caller reports it.
|
|
96
94
|
*/
|
|
97
95
|
|
|
98
|
-
/**
|
|
99
|
-
* Add a collision-aware fallback key.
|
|
100
|
-
*
|
|
101
|
-
* First writer wins *until* a second, different page claims the key — at which
|
|
102
|
-
* point the key is removed and blacklisted, so neither page answers to it. That
|
|
103
|
-
* is deliberate: resolving to whichever note happened to be walked first is a
|
|
104
|
-
* silently wrong link, and a failed build is not.
|
|
105
|
-
*
|
|
106
|
-
* @param {Map<string, object>} index - The index being built.
|
|
107
|
-
* @param {Set<string>} collide - Keys already found ambiguous.
|
|
108
|
-
* @param {string} key - The candidate key, in any case.
|
|
109
|
-
* @param {{url: string}} value - The page it would resolve to.
|
|
110
|
-
*/
|
|
111
|
-
function addFallback(index, collide, key, value) {
|
|
112
|
-
const k = String(key).toLowerCase();
|
|
113
|
-
if (collide.has(k)) return;
|
|
114
|
-
const cur = index.get(k);
|
|
115
|
-
if (cur && cur.url !== value.url) {
|
|
116
|
-
index.delete(k);
|
|
117
|
-
collide.add(k);
|
|
118
|
-
} else if (!cur) {
|
|
119
|
-
index.set(k, value);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
96
|
/**
|
|
124
97
|
* Merge the packages this build does not publish into the local index.
|
|
125
98
|
*
|
|
@@ -131,14 +104,18 @@ function addFallback(index, collide, key, value) {
|
|
|
131
104
|
* The short `type/shortcode` form is merged too, because a bare `[[doc-xyz]]`
|
|
132
105
|
* carries no package and must still find a foreign note when exactly one
|
|
133
106
|
* package publishes that address. Claimed by two, it is genuinely ambiguous and
|
|
134
|
-
* the author writes the qualified form
|
|
135
|
-
*
|
|
107
|
+
* the author writes the qualified form. **Local wins**: a live build is
|
|
108
|
+
* authoritative and a
|
|
136
109
|
* vendored manifest can only be staler, so a short key the local tree already
|
|
137
110
|
* claims is left alone.
|
|
138
111
|
*
|
|
139
112
|
* @param {Map<string, object>} index - The local index, mutated.
|
|
140
113
|
* @param {Map<string, {package: string, type?: string}>} foreignIndex - Merged in.
|
|
141
|
-
* @returns {{key: string, package: string}[]
|
|
114
|
+
* @returns {{conflicts: {key: string, package: string}[],
|
|
115
|
+
* ambiguous: Set<string>}} The addresses two packages both claim outright,
|
|
116
|
+
* and the short `type/shortcode` forms two foreign packages claim — those are
|
|
117
|
+
* left out of the index, so a resolver can say *ambiguous* rather than
|
|
118
|
+
* *nothing answers*.
|
|
142
119
|
*/
|
|
143
120
|
function mergeForeign(index, foreignIndex) {
|
|
144
121
|
const conflicts = [];
|
|
@@ -166,7 +143,7 @@ function mergeForeign(index, foreignIndex) {
|
|
|
166
143
|
for (const [key, value] of short) {
|
|
167
144
|
if (!index.has(key)) index.set(key, value);
|
|
168
145
|
}
|
|
169
|
-
return conflicts;
|
|
146
|
+
return { conflicts, ambiguous };
|
|
170
147
|
}
|
|
171
148
|
|
|
172
149
|
/**
|
|
@@ -182,9 +159,6 @@ function mergeForeign(index, foreignIndex) {
|
|
|
182
159
|
*/
|
|
183
160
|
export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
184
161
|
const index = new Map();
|
|
185
|
-
const ambiguous = new Set();
|
|
186
|
-
/** One entry per page, fed to {@link indexAliases} in a pass of its own. */
|
|
187
|
-
const aliasEntries = [];
|
|
188
162
|
const contentTypes = new Set();
|
|
189
163
|
const sections = new Set();
|
|
190
164
|
const refIndex = new Map();
|
|
@@ -195,16 +169,22 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
195
169
|
const ownPackage = contentPackage();
|
|
196
170
|
const packages = new Set(ownPackage ? [ownPackage] : []);
|
|
197
171
|
|
|
198
|
-
// `section/slug` is unique by construction
|
|
172
|
+
// `section/slug` is unique by construction. A page's name, filename and
|
|
173
|
+
// bare slug were indexed here too, as collision-aware fallbacks the bare
|
|
174
|
+
// `[[Name]]` form looked up; that form is retired and nothing consults
|
|
175
|
+
// them, so they are gone and with them the rule that two pages of a type
|
|
176
|
+
// may not share a name (#179, #180).
|
|
199
177
|
for (const e of entries) {
|
|
200
178
|
sections.add(String(e.sec).toLowerCase());
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
179
|
+
// `draft` rides on every key a page is addressable by, because a link
|
|
180
|
+
// into a draft note renders marked whichever of them the author wrote
|
|
181
|
+
// (#183). It decides nothing about resolution: the page is indexed and
|
|
182
|
+
// published as any other.
|
|
183
|
+
index.set(`${e.sec}/${e.slug}`.toLowerCase(), {
|
|
184
|
+
url: e.url,
|
|
185
|
+
name: e.name,
|
|
186
|
+
draft: isDraftNote(e.fm),
|
|
187
|
+
});
|
|
208
188
|
}
|
|
209
189
|
|
|
210
190
|
// A foreign package may use a type this build has never seen. Seeding those
|
|
@@ -225,7 +205,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
225
205
|
// The corollary is that a conflict can only be reported against the keys
|
|
226
206
|
// that exist at this point — the addressing ones, `section/slug` and the
|
|
227
207
|
// bare fallbacks — which is precisely the overlap worth refusing.
|
|
228
|
-
const conflicts = mergeForeign(index, foreignIndex);
|
|
208
|
+
const { conflicts, ambiguous } = mergeForeign(index, foreignIndex);
|
|
229
209
|
|
|
230
210
|
for (const e of entries) {
|
|
231
211
|
// A page with no type or shortcode — a developer doc — is addressable
|
|
@@ -233,7 +213,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
233
213
|
if (e.kind !== "content") continue;
|
|
234
214
|
const type = String(e.fm.type).toLowerCase();
|
|
235
215
|
contentTypes.add(type);
|
|
236
|
-
const value = { url: e.url, name: e.name };
|
|
216
|
+
const value = { url: e.url, name: e.name, draft: isDraftNote(e.fm) };
|
|
237
217
|
|
|
238
218
|
const shortcode = e.fm.shortcode;
|
|
239
219
|
if (typeof shortcode === "string" && shortcode) {
|
|
@@ -263,36 +243,11 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
263
243
|
index.set(`doc${type}/${shortcode}`.toLowerCase(), value);
|
|
264
244
|
}
|
|
265
245
|
}
|
|
266
|
-
|
|
267
|
-
// The alias sources are the shared ones — `aliases`, `name.aliases`
|
|
268
|
-
// and `name.full`, and deliberately **not** the filename (#131) — plus
|
|
269
|
-
// this page's display name, which is what `name.full` becomes on the
|
|
270
|
-
// site and is carried here already resolved.
|
|
271
|
-
aliasEntries.push({
|
|
272
|
-
type,
|
|
273
|
-
aliases: [...aliasesOf(e.fm), e.name],
|
|
274
|
-
value,
|
|
275
|
-
});
|
|
276
246
|
}
|
|
277
247
|
|
|
278
|
-
// Built in one pass at the end, by the shared rule, so a collision is a
|
|
279
|
-
// reportable fact rather than a silently deleted key. Two pages of one
|
|
280
|
-
// type sharing an alias resolve to neither, and `aliasCollisions` names
|
|
281
|
-
// every claimant — the citing page is innocent (#13, #131).
|
|
282
|
-
const {
|
|
283
|
-
byKey: typeAlias,
|
|
284
|
-
collisions: aliasCollisions,
|
|
285
|
-
claims: aliasClaims,
|
|
286
|
-
} = indexAliases(aliasEntries, { same: (a, b) => a.url === b.url });
|
|
287
|
-
const typeCollide = new Set(aliasCollisions.map((c) => c.key));
|
|
288
|
-
|
|
289
248
|
return {
|
|
290
249
|
index,
|
|
291
250
|
ambiguous,
|
|
292
|
-
typeAlias,
|
|
293
|
-
typeCollide,
|
|
294
|
-
aliasCollisions,
|
|
295
|
-
aliasClaims,
|
|
296
251
|
contentTypes,
|
|
297
252
|
sections,
|
|
298
253
|
packages,
|
|
@@ -313,32 +268,32 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
313
268
|
* @param {object} options - Per-page inputs.
|
|
314
269
|
* @param {string} options.src - Source path of the page being resolved, for
|
|
315
270
|
* diagnostics.
|
|
316
|
-
* @param {string|null} [options.type] - The citing note's type,
|
|
317
|
-
*
|
|
271
|
+
* @param {string|null} [options.type] - The citing note's type, carried for a
|
|
272
|
+
* consumer's own diagnostics.
|
|
318
273
|
* @param {object[]} options.errors - Collector the resolver appends to.
|
|
274
|
+
* @param {string} [options.file] - The page's source file, which a link
|
|
275
|
+
* diagnostic names. Absent, `src` stands in.
|
|
319
276
|
* @param {Map<string, object>} [options.foreignIndex] - The foreign index, for
|
|
320
277
|
* resolvers that distinguish a foreign hit from a local one.
|
|
321
|
-
* @param {boolean} [options.manifestsComplete] - Whether every package this
|
|
322
|
-
* build links into supplied a manifest. When false, a resolver may soften an
|
|
323
|
-
* unresolved cross-package link rather than fail.
|
|
324
278
|
* @returns {object} The resolver context.
|
|
279
|
+
*
|
|
280
|
+
* There is deliberately **no `manifestsComplete`**. It used to let a resolver
|
|
281
|
+
* soften an unresolved cross-package address while any package's manifest was
|
|
282
|
+
* missing; #184 retired the softening, since the pack compilers and the link
|
|
283
|
+
* checker never had it and one authored link must not get two verdicts. A
|
|
284
|
+
* caller still passing it is ignored rather than obeyed.
|
|
325
285
|
*/
|
|
326
|
-
export function wikiContext(
|
|
327
|
-
built,
|
|
328
|
-
{ src, type = null, errors, foreignIndex = new Map(), manifestsComplete = true },
|
|
329
|
-
) {
|
|
286
|
+
export function wikiContext(built, { src, file, type = null, errors, foreignIndex = new Map() }) {
|
|
330
287
|
return {
|
|
331
288
|
index: built.index,
|
|
332
289
|
foreign: foreignIndex,
|
|
333
|
-
manifestsComplete,
|
|
334
290
|
collide: built.ambiguous,
|
|
335
291
|
sections: built.sections,
|
|
336
|
-
typeAlias: built.typeAlias,
|
|
337
|
-
typeCollide: built.typeCollide,
|
|
338
292
|
contentTypes: built.contentTypes,
|
|
339
293
|
packages: built.packages,
|
|
340
294
|
type,
|
|
341
295
|
errors,
|
|
342
296
|
src,
|
|
297
|
+
file,
|
|
343
298
|
};
|
|
344
299
|
}
|