@heroiclands/package-build 3.4.0 → 5.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 +182 -0
- package/CONTENT.md +100 -22
- package/MIGRATING.md +120 -0
- package/bin/content-build.mjs +6 -2
- package/content-config.mjs +109 -6
- package/engine/base-compiler.mjs +33 -36
- package/engine/content-links.mjs +15 -13
- package/engine/content-package.mjs +3 -2
- package/engine/field-reference.mjs +2 -2
- package/engine/frontmatter-lint.mjs +26 -0
- package/engine/helpers.mjs +10 -12
- package/engine/homepage.mjs +150 -0
- package/engine/index.mjs +10 -1
- package/engine/journals.mjs +2 -3
- package/engine/macros.mjs +2 -3
- package/engine/manifest-emit.mjs +17 -12
- package/engine/note-package.mjs +75 -68
- package/engine/note-schemas.mjs +44 -0
- package/engine/pack-router.mjs +3 -3
- package/engine/retired-fields.mjs +123 -0
- package/engine/scenes.mjs +7 -11
- package/engine/site-build.mjs +151 -22
- package/engine/site-index.mjs +5 -5
- package/package.json +1 -1
- package/sohl/actors.mjs +2 -3
- package/sohl/items.mjs +2 -3
- package/sohl/note-schemas.mjs +8 -0
- package/types/content-config.d.mts +105 -4
- package/types/engine/base-compiler.d.mts +12 -17
- package/types/engine/content-package.d.mts +3 -2
- package/types/engine/helpers.d.mts +5 -8
- package/types/engine/homepage.d.mts +118 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/manifest-emit.d.mts +7 -8
- package/types/engine/note-package.d.mts +29 -34
- package/types/engine/note-schemas.d.mts +6 -0
- package/types/engine/pack-router.d.mts +3 -3
- package/types/engine/retired-fields.d.mts +54 -0
- package/types/engine/site-build.d.mts +52 -4
- package/types/sohl/note-schemas.d.mts +6 -0
package/engine/index.mjs
CHANGED
|
@@ -54,9 +54,18 @@ export * as packRouter from "./pack-router.mjs";
|
|
|
54
54
|
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
55
55
|
export * as contentPackage from "./content-package.mjs";
|
|
56
56
|
|
|
57
|
-
/** Which content package a note belongs to, and refusing one that
|
|
57
|
+
/** Which content package a note belongs to, and refusing one that says so. */
|
|
58
58
|
export * as notePackage from "./note-package.mjs";
|
|
59
59
|
|
|
60
|
+
/** Frontmatter fields a note may no longer declare, and the refusal of them. */
|
|
61
|
+
export * as retiredFields from "./retired-fields.mjs";
|
|
62
|
+
|
|
63
|
+
/** The package homepage: the note type that compiles to a page, not a document. */
|
|
64
|
+
export * as homepage from "./homepage.mjs";
|
|
65
|
+
|
|
66
|
+
/** The note types the engine itself declares, whatever a consumer registers. */
|
|
67
|
+
export * as noteSchemas from "./note-schemas.mjs";
|
|
68
|
+
|
|
60
69
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
61
70
|
|
|
62
71
|
/** The URL a content note is published at — the one web-address rule. */
|
package/engine/journals.mjs
CHANGED
|
@@ -38,9 +38,8 @@
|
|
|
38
38
|
* Not a standalone script — exports the `Journals` compiler class, imported
|
|
39
39
|
* and driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
40
40
|
*
|
|
41
|
-
* The walk itself — filtering by
|
|
42
|
-
*
|
|
43
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
41
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
42
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
44
43
|
* states only what makes this pass its own (#1509).
|
|
45
44
|
*/
|
|
46
45
|
|
package/engine/macros.mjs
CHANGED
|
@@ -44,9 +44,8 @@
|
|
|
44
44
|
* Not a standalone script — exports the `Macros` compiler class, imported and
|
|
45
45
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
46
46
|
*
|
|
47
|
-
* The walk itself — filtering by
|
|
48
|
-
*
|
|
49
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
47
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
48
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
50
49
|
* states only what makes this pass its own (#1509).
|
|
51
50
|
*/
|
|
52
51
|
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -54,10 +54,12 @@ import { canonicalKey, writeManifests } from "./kb-manifest.mjs";
|
|
|
54
54
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
55
55
|
import { compendiumUuid, packForType, pageUuid } from "./ids.mjs";
|
|
56
56
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
57
|
-
import {
|
|
57
|
+
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
58
|
+
import { assertNoDraftField } from "./retired-fields.mjs";
|
|
58
59
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
59
60
|
import { routerFor } from "./pack-router.mjs";
|
|
60
61
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
62
|
+
import { publishesContentPages } from "../content-config.mjs";
|
|
61
63
|
|
|
62
64
|
/**
|
|
63
65
|
* The reserved anchor name for a journal's **first** page.
|
|
@@ -193,14 +195,13 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
193
195
|
/**
|
|
194
196
|
* Every note this package publishes, as manifest entries.
|
|
195
197
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* filtered out of a manifest that then claimed the package published nothing.
|
|
198
|
+
* Every note in the tree is this package's, so nothing here selects by package:
|
|
199
|
+
* the key's first segment is `contentPackage` (#56). A note still declaring the
|
|
200
|
+
* retired `package:` or `draft:` field **throws** rather than being skipped —
|
|
201
|
+
* skipping one silently is how a whole tree came to be filtered out of a
|
|
202
|
+
* manifest that then claimed the package published nothing, and it is what let
|
|
203
|
+
* a drafted note's inbound links look like links to a note that never existed
|
|
204
|
+
* (#69).
|
|
204
205
|
*
|
|
205
206
|
* A note that has no address is **reported, not guessed** — the finding carries
|
|
206
207
|
* the file and the reason, so a caller can print it or fail on it. Inventing an
|
|
@@ -226,8 +227,12 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
226
227
|
)) {
|
|
227
228
|
if (!fm) continue;
|
|
228
229
|
const rel = path.relative(contentBase, absPath);
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
assertNoDeclaredPackage(fm, {
|
|
231
|
+
file: rel,
|
|
232
|
+
absPath,
|
|
233
|
+
configured: ctx.contentPackage,
|
|
234
|
+
});
|
|
235
|
+
assertNoDraftField(fm, { file: rel, absPath });
|
|
231
236
|
if (!fm.type || !fm.shortcode) continue;
|
|
232
237
|
|
|
233
238
|
const base = path.basename(absPath);
|
|
@@ -267,7 +272,7 @@ export function manifestContext(config = loadPackConfig()) {
|
|
|
267
272
|
foundryPackageId: config.foundryPackage,
|
|
268
273
|
packRouter: routerFor(config),
|
|
269
274
|
scheme: config.publish.address,
|
|
270
|
-
web: config
|
|
275
|
+
web: publishesContentPages(config),
|
|
271
276
|
// The walk's own configuration, threaded through rather than left to
|
|
272
277
|
// its default, so a caller that passes a config drives every read.
|
|
273
278
|
skipDirectories: config.skipDirectories,
|
package/engine/note-package.mjs
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Which content package a note belongs to.
|
|
16
16
|
*
|
|
17
|
-
* **It is the repository's configured `contentPackage
|
|
18
|
-
* exactly one package's notes — every package is
|
|
19
|
-
* repository that ships it — so the package is a property
|
|
20
|
-
* not of the note.
|
|
17
|
+
* **It is the repository's configured `contentPackage`, and nothing else.** A
|
|
18
|
+
* content tree holds exactly one package's notes — every package is
|
|
19
|
+
* single-sourced in the repository that ships it — so the package is a property
|
|
20
|
+
* of the *repository*, not of the note, and no note declares it.
|
|
21
21
|
*
|
|
22
22
|
* It used to be a property of the note, and a **selector**: the compile loop
|
|
23
23
|
* read `package:` out of frontmatter and skipped anything that did not match
|
|
@@ -27,100 +27,107 @@
|
|
|
27
27
|
* **zero notes and exited 0** — which is exactly the state the un-migrated
|
|
28
28
|
* `hm-loc-*` / `hm-adv-*` repositories are in today.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
30
|
+
* The field was retired in three steps, and all three have landed:
|
|
31
31
|
*
|
|
32
|
-
* 1. **Optional
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 2. **Swept** out of every content tree
|
|
36
|
-
*
|
|
32
|
+
* 1. **Optional** (3.3.0). An absent `package:` was normal and a present one
|
|
33
|
+
* was accepted while it agreed. Non-breaking, so a consumer adopted it
|
|
34
|
+
* before changing a note.
|
|
35
|
+
* 2. **Swept** out of every content tree on the org — 6,235 notes across
|
|
36
|
+
* `sohl`, `thalorna`, `kethira` and `harnensemble`.
|
|
37
|
+
* 3. **Rejected outright**, as this major. A note declaring the field fails the
|
|
38
|
+
* build naming the file, whatever the value says.
|
|
37
39
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* Step 3 is deliberately a rejection rather than continued tolerance: a field
|
|
41
|
+
* accepted while it agrees is a field that grows back, one note at a time, and
|
|
42
|
+
* every one of them is a line restating a constant the configuration already
|
|
43
|
+
* carries. There is no value that makes writing it correct, so the diagnostic
|
|
44
|
+
* says what to write instead rather than which value to change.
|
|
45
|
+
*
|
|
46
|
+
* **One `package` survives, and it is synthesised.** A `dataview` table scopes
|
|
47
|
+
* itself with `WHERE … and package = "<pkg>"`, and that clause resolves against
|
|
48
|
+
* frontmatter like any other field. {@link searchableFrontmatter} supplies the
|
|
49
|
+
* derived value to the search so the 45 authored clauses across `sohl` and
|
|
50
|
+
* `thalorna` keep matching. It is a *search* value, never an authored one, and
|
|
51
|
+
* it is never written back to a note.
|
|
45
52
|
*
|
|
46
53
|
* @module
|
|
47
54
|
*/
|
|
48
55
|
|
|
49
56
|
import { contentPackage } from "./content-package.mjs";
|
|
57
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* Non-validating: the answer for a note that declares nothing, and for one that
|
|
55
|
-
* declares the configured package, is the same value. A note declaring some
|
|
56
|
-
* *other* package is answered literally here rather than corrected — the
|
|
57
|
-
* compile pass reports that, once, through {@link assertNotePackage}.
|
|
58
|
-
*
|
|
59
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
60
|
-
* could not be parsed.
|
|
61
|
-
* @param {string} [configured] - The package this build compiles. Defaults to
|
|
62
|
-
* the configured `contentPackage`; passed explicitly by callers that already
|
|
63
|
-
* carry it in a context object, so a caller's configuration drives every read.
|
|
64
|
-
* @returns {string} The package.
|
|
65
|
-
*/
|
|
66
|
-
export function notePackage(fm, configured) {
|
|
67
|
-
const declared = fm?.package;
|
|
68
|
-
// A blank is a declaration of nothing, not a package named "".
|
|
69
|
-
if (declared != null && declared !== "") return declared;
|
|
70
|
-
// Resolved only when it is needed, so a caller holding a note that declares
|
|
71
|
-
// one never touches the configuration (#2).
|
|
72
|
-
return configured ?? contentPackage();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A note's frontmatter as a generated table searches it — its package present
|
|
77
|
-
* whether or not the note declares one.
|
|
60
|
+
* A note's frontmatter as a generated table searches it — its package present,
|
|
61
|
+
* though no note declares one.
|
|
78
62
|
*
|
|
79
63
|
* A `dataview` query resolves `package` out of frontmatter like any other
|
|
80
64
|
* field, so a collection note that scopes itself with `WHERE … and package =
|
|
81
|
-
* "sohl"`
|
|
82
|
-
* table** in silence.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* way.
|
|
65
|
+
* "sohl"` would match nothing now that the field is gone, and would render an
|
|
66
|
+
* **empty table** in silence. Supplying the derived value here is what kept the
|
|
67
|
+
* sweep mechanical rather than a trap (#56) — and a query that never mentions
|
|
68
|
+
* `package` is unaffected either way.
|
|
86
69
|
*
|
|
87
|
-
* The
|
|
88
|
-
*
|
|
70
|
+
* The frontmatter is copied rather than written into: it is the note's own
|
|
71
|
+
* parsed object, shared with every other reader, and the derived package is a
|
|
72
|
+
* property of *this search*, not of the note.
|
|
89
73
|
*
|
|
90
74
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
91
|
-
* @param {string} [configured] - The package this build compiles.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
75
|
+
* @param {string} [configured] - The package this build compiles. Defaults to
|
|
76
|
+
* the configured `contentPackage`; passed explicitly by callers that already
|
|
77
|
+
* carry it in a context object, so a caller's configuration drives every read.
|
|
78
|
+
* @returns {object|null|undefined} A shallow copy carrying the derived package,
|
|
79
|
+
* or whatever was passed when it is not frontmatter at all.
|
|
94
80
|
*/
|
|
95
81
|
export function searchableFrontmatter(fm, configured) {
|
|
96
82
|
if (!fm || typeof fm !== "object") return fm;
|
|
97
|
-
|
|
98
|
-
return { ...fm, package: notePackage(fm, configured) };
|
|
83
|
+
return { ...fm, package: configured ?? contentPackage() };
|
|
99
84
|
}
|
|
100
85
|
|
|
101
86
|
/**
|
|
102
|
-
*
|
|
87
|
+
* Refuse a note that declares `package:` at all.
|
|
103
88
|
*
|
|
104
|
-
*
|
|
89
|
+
* Presence is the whole test — a declaration that *agrees* with the
|
|
90
|
+
* configuration is as retired as one that disagrees, and an empty one
|
|
91
|
+
* (`package:`, which parses as `null`) is still the field.
|
|
92
|
+
*
|
|
93
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
94
|
+
* could not be parsed.
|
|
105
95
|
* @param {object} [options] - Options.
|
|
106
96
|
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
107
97
|
* it where the caller emits through a diagnostic, which puts the locator at
|
|
108
98
|
* the start of the line already — repeating it prints the path twice.
|
|
99
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
100
|
+
* failing path to locate the offending line and column. The position rides on
|
|
101
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
109
102
|
* @param {string} [options.configured] - The package this build compiles.
|
|
110
103
|
* Defaults to the configured `contentPackage`.
|
|
111
|
-
* @returns {
|
|
112
|
-
* @throws {Error} When the note declares
|
|
104
|
+
* @returns {void}
|
|
105
|
+
* @throws {Error} When the note declares the field.
|
|
113
106
|
*/
|
|
114
|
-
export function
|
|
107
|
+
export function assertNoDeclaredPackage(
|
|
108
|
+
fm,
|
|
109
|
+
{ file, absPath, configured } = {},
|
|
110
|
+
) {
|
|
111
|
+
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "package")) return;
|
|
112
|
+
|
|
115
113
|
const target = configured ?? contentPackage();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const declared = fm.package;
|
|
115
|
+
const wrote =
|
|
116
|
+
declared === null || declared === undefined || declared === "" ?
|
|
117
|
+
"`package:`"
|
|
118
|
+
: `\`package: ${declared}\``;
|
|
119
|
+
|
|
120
|
+
const err = new Error(
|
|
121
|
+
`${wrote} is a retired frontmatter field — delete it` +
|
|
121
122
|
(file ? ` — ${file}` : "") +
|
|
122
|
-
`. A note's package is
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
`. A note's package is this repository's configured ` +
|
|
124
|
+
`\`contentPackage\` ("${target}", in package-build.config.yaml), ` +
|
|
125
|
+
`and every note in the tree belongs to it.`,
|
|
125
126
|
);
|
|
127
|
+
// Where the field is, so the caller's diagnostic opens on the line that has
|
|
128
|
+
// to be deleted. Read here rather than carried through every walk: this is
|
|
129
|
+
// the failing path, and the build stops on it.
|
|
130
|
+
const position = locateFrontmatterKey(absPath, "package");
|
|
131
|
+
if (position) err.position = position;
|
|
132
|
+
throw err;
|
|
126
133
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The note types the **engine** declares — the ones whose vocabulary is a fact
|
|
16
|
+
* about the note format rather than about any game system (#51).
|
|
17
|
+
*
|
|
18
|
+
* `sohl/note-schemas.mjs` is the other half, and the line between them is the
|
|
19
|
+
* `engine/` ÷ `sohl/` line everywhere else in this package: note-format
|
|
20
|
+
* knowledge here, game-system knowledge there. It is not a permission boundary
|
|
21
|
+
* between consumers — every content project authors the full vocabulary — but it
|
|
22
|
+
* is a reachability one in exactly one direction. A package that declares no
|
|
23
|
+
* `itemBuilders` (`HarnMaster-3-FoundryVTT`, and every HM3 module) uses only the
|
|
24
|
+
* packaging half of the toolchain, so a type declared in the SoHL registry would
|
|
25
|
+
* be unavailable to it. These are the types every package has, whatever it
|
|
26
|
+
* ships.
|
|
27
|
+
*
|
|
28
|
+
* One entry today. A consumer merges it under its own registry —
|
|
29
|
+
* `{ ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS }` — so a game system may extend
|
|
30
|
+
* these but the engine's declaration stands wherever no registry is configured.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { HOMEPAGE_FIELDS, HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Every engine-level content type, and what a note of that type may write.
|
|
39
|
+
*
|
|
40
|
+
* @type {Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>}
|
|
41
|
+
*/
|
|
42
|
+
export const ENGINE_NOTE_SCHEMAS = Object.freeze({
|
|
43
|
+
[HOMEPAGE_TYPE]: HOMEPAGE_FIELDS,
|
|
44
|
+
});
|
package/engine/pack-router.mjs
CHANGED
|
@@ -69,10 +69,10 @@ export class PackRoutingError extends Error {
|
|
|
69
69
|
/**
|
|
70
70
|
* The frontmatter field a note declares its pack in.
|
|
71
71
|
*
|
|
72
|
-
* Deliberately close to the
|
|
72
|
+
* Deliberately close to the retired `package:` and deliberately not the same
|
|
73
73
|
* word: `package:` said which *distribution* owned a note — now the
|
|
74
|
-
* repository's `contentPackage
|
|
75
|
-
* receives its document.
|
|
74
|
+
* repository's `contentPackage`, and no longer authorable (#56) — while `pack:`
|
|
75
|
+
* says which *compendium* receives its document.
|
|
76
76
|
*/
|
|
77
77
|
export const PACK_FIELD = "pack";
|
|
78
78
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Frontmatter fields a note may no longer declare.
|
|
16
|
+
*
|
|
17
|
+
* A retired field has exactly two possible fates, and only one of them is
|
|
18
|
+
* honest. Left honoured, it keeps doing whatever it did — which is why it was
|
|
19
|
+
* retired. Left *ignored*, it reads to its author as though it still works: the
|
|
20
|
+
* note says one thing and the build does another, and nothing says so. So a
|
|
21
|
+
* retired field is **refused**, naming the file and the line, and the message
|
|
22
|
+
* says what to write instead rather than which value to correct.
|
|
23
|
+
*
|
|
24
|
+
* `package:` is retired the same way and is refused from `note-package.mjs`,
|
|
25
|
+
* where the concept it belonged to still lives. `draft:` has no such home —
|
|
26
|
+
* there is no surviving concept it was part of — so it is refused here.
|
|
27
|
+
*
|
|
28
|
+
* **What `draft:` did (#69).** It excluded a note from the compiled packs, from
|
|
29
|
+
* the link manifest and from a consuming site build. Nothing reported the
|
|
30
|
+
* consequence: `content-links.mjs`, `site-index.mjs` and `content-lint.mjs`
|
|
31
|
+
* never read the field, so a link into a drafted note was indistinguishable
|
|
32
|
+
* from a link to a note that does not exist, and the checkers could not say
|
|
33
|
+
* which. The field's entire effect was to move a note from *published* to
|
|
34
|
+
* *unresolvable*, silently — and it also suppressed real build failures, since
|
|
35
|
+
* a note the compilers never reached could not fail on the defects it carried.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import fs from "node:fs";
|
|
41
|
+
|
|
42
|
+
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a note declaring `draft:` is told, in one place.
|
|
46
|
+
*
|
|
47
|
+
* Written once and shared by the compile-time refusal and the frontmatter lint,
|
|
48
|
+
* because an author meets whichever of the two runs first and they should read
|
|
49
|
+
* the same. It says what the field did and what to write instead, rather than
|
|
50
|
+
* which value to correct: no value makes declaring it right.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
53
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
54
|
+
* line — repeating it prints the path twice.
|
|
55
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
56
|
+
*/
|
|
57
|
+
export function draftRetiredMessage(file) {
|
|
58
|
+
return (
|
|
59
|
+
"`draft:` is a retired frontmatter field — delete it" +
|
|
60
|
+
(file ? ` — ${file}` : "") +
|
|
61
|
+
". It excluded the note from the compiled packs, the link manifest " +
|
|
62
|
+
"and the site, and no checker reported the exclusion, so every " +
|
|
63
|
+
"wikilink into it read as a link to a note that does not exist. To " +
|
|
64
|
+
"mark a note as unfinished, tag it `#draft` instead: the build " +
|
|
65
|
+
"ignores tags, and a `FROM #draft` query still finds it"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Refuse a note that declares `draft:` at all.
|
|
71
|
+
*
|
|
72
|
+
* Presence is the whole test. `draft: false` is as retired as `draft: true` —
|
|
73
|
+
* it reads as "publish this note", which is what happens either way, and is
|
|
74
|
+
* exactly the belief the message exists to correct.
|
|
75
|
+
*
|
|
76
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
77
|
+
* could not be parsed.
|
|
78
|
+
* @param {object} [options] - Options.
|
|
79
|
+
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
80
|
+
* it where the caller emits through a diagnostic, which puts the locator at
|
|
81
|
+
* the start of the line already — repeating it prints the path twice.
|
|
82
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
83
|
+
* failing path to locate the offending line and column. The position rides on
|
|
84
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
85
|
+
* @returns {void}
|
|
86
|
+
* @throws {Error} When the note declares the field.
|
|
87
|
+
*/
|
|
88
|
+
export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
89
|
+
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "draft")) return;
|
|
90
|
+
|
|
91
|
+
const err = new Error(`${draftRetiredMessage(file)}.`);
|
|
92
|
+
// Where the field is, so the caller's diagnostic opens on the line that has
|
|
93
|
+
// to be deleted. Read here rather than carried through every walk: this is
|
|
94
|
+
// the failing path, and the build stops on it.
|
|
95
|
+
const position = locateFrontmatterKey(absPath, "draft");
|
|
96
|
+
if (position) err.position = position;
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A frontmatter key's position in a note's file, or nothing.
|
|
102
|
+
*
|
|
103
|
+
* {@link positionInFrontmatter} answers the same question from the file's
|
|
104
|
+
* *text*; this reads the file to ask it. Kept apart from either caller because
|
|
105
|
+
* both refusals need it and a second copy is a second thing to keep correct.
|
|
106
|
+
*
|
|
107
|
+
* @param {string|undefined} absPath - The note's file.
|
|
108
|
+
* @param {string} key - The top-level frontmatter key.
|
|
109
|
+
* @returns {{line?: number, column?: number}|undefined} Spreadable position
|
|
110
|
+
* fields, dropped rather than guessed when the file cannot be read or the key
|
|
111
|
+
* cannot be found — as `formatDiagnostic` requires.
|
|
112
|
+
*/
|
|
113
|
+
export function locateFrontmatterKey(absPath, key) {
|
|
114
|
+
if (!absPath) return undefined;
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const at = positionInFrontmatter(raw, key);
|
|
122
|
+
return at.line === undefined ? undefined : at;
|
|
123
|
+
}
|
package/engine/scenes.mjs
CHANGED
|
@@ -42,9 +42,8 @@
|
|
|
42
42
|
* Not a standalone script — exports the `Scenes` compiler class, imported and
|
|
43
43
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
44
44
|
*
|
|
45
|
-
* The walk itself — filtering by
|
|
46
|
-
*
|
|
47
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
45
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
46
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
48
47
|
* states only what makes this pass its own (#1509).
|
|
49
48
|
*/
|
|
50
49
|
|
|
@@ -192,10 +191,11 @@ export class Scenes extends BasePackCompiler {
|
|
|
192
191
|
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(
|
|
193
192
|
this.contentBase,
|
|
194
193
|
)) {
|
|
195
|
-
// No
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
194
|
+
// No retired-field test: this pass's own walk — the shared compile
|
|
195
|
+
// loop — is where a note still declaring `package:` (#56) or
|
|
196
|
+
// `draft:` (#69) is reported, once. Repeating either check here
|
|
197
|
+
// would double the diagnostic or throw past it. A refused note is
|
|
198
|
+
// indexed and then never compiled, so it reaches no document.
|
|
199
199
|
if (!fm || !fm.id) continue;
|
|
200
200
|
if (
|
|
201
201
|
fm.shortcode &&
|
|
@@ -216,10 +216,6 @@ export class Scenes extends BasePackCompiler {
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
if (!isMapType(fm.type)) continue;
|
|
219
|
-
if (fm.draft === true) {
|
|
220
|
-
log.debug(`Skipping draft map: ${absPath}`);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
219
|
maps.push({ fm, body, absPath });
|
|
224
220
|
}
|
|
225
221
|
return { maps, effectsByAddress };
|