@heroiclands/package-build 17.2.0 → 18.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 +1711 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +225 -127
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +55 -83
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/manifest.mjs +43 -2
- package/package.json +18 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +54 -49
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +2 -43
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
|
@@ -47,6 +47,15 @@ import { extractPack } from "@foundryvtt/foundryvtt-cli";
|
|
|
47
47
|
|
|
48
48
|
import log from "loglevel";
|
|
49
49
|
|
|
50
|
+
import {
|
|
51
|
+
metadataRelationships,
|
|
52
|
+
metadataCacheDir,
|
|
53
|
+
metadataFileName,
|
|
54
|
+
newestVersionDir,
|
|
55
|
+
isComplete as metadataIsComplete,
|
|
56
|
+
markComplete as markMetadataComplete,
|
|
57
|
+
} from "./metadata-index.mjs";
|
|
58
|
+
|
|
50
59
|
/** Written once a fetch completes, so a half-finished cache is never used. */
|
|
51
60
|
const STAMP = ".complete";
|
|
52
61
|
|
|
@@ -426,6 +435,185 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
426
435
|
}
|
|
427
436
|
}
|
|
428
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Fetch one dependency's published content index (#239).
|
|
440
|
+
*
|
|
441
|
+
* **The chain is entirely declared.** The relationship names the dependency's
|
|
442
|
+
* manifest, the manifest advertises `flags.metadataUrl`, and that URL is the
|
|
443
|
+
* index — so nothing here holds an address of its own, and a dependency that
|
|
444
|
+
* moves its release assets does not break its consumers.
|
|
445
|
+
*
|
|
446
|
+
* Pinned by the same rule as the catalogue: `compatibility.verified` is the
|
|
447
|
+
* version this repository was built against, so a floating `releases/latest`
|
|
448
|
+
* URL is rewritten to it. A consumer resolving addresses against whatever the
|
|
449
|
+
* dependency published this morning is not reproducible.
|
|
450
|
+
*
|
|
451
|
+
* Idempotent: a complete cache for the resolved version is left alone.
|
|
452
|
+
*
|
|
453
|
+
* @param {object} config - The resolved build configuration.
|
|
454
|
+
* @param {{id: string, manifest: string, verified?: string}} rel - The declared
|
|
455
|
+
* relationship.
|
|
456
|
+
* @returns {Promise<string>} The cached index file.
|
|
457
|
+
*/
|
|
458
|
+
export async function fetchMetadata(config, rel) {
|
|
459
|
+
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
460
|
+
const manifest = await fetchManifest(url);
|
|
461
|
+
const version = manifest.version;
|
|
462
|
+
if (!version) {
|
|
463
|
+
throw new Error(`${rel.id}: its manifest declares no \`version\``);
|
|
464
|
+
}
|
|
465
|
+
if (!pinned && rel.verified && version !== rel.verified) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`${rel.id}: declares \`compatibility.verified: ${rel.verified}\` but ` +
|
|
468
|
+
`${url} offers ${version}. Building against a moving target is ` +
|
|
469
|
+
`not reproducible — update \`verified\`, or point \`manifest\` ` +
|
|
470
|
+
`at a pinned release.`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const dir = metadataCacheDir(config, rel.id, version);
|
|
475
|
+
const indexUrl = manifest.flags?.metadataUrl;
|
|
476
|
+
if (!indexUrl) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`${rel.id}@${version}: its manifest advertises no ` +
|
|
479
|
+
`\`flags.metadataUrl\`, so it publishes no content index and ` +
|
|
480
|
+
`nothing can link into it. It needs a release built with ` +
|
|
481
|
+
`package-build 18 or later.`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (metadataIsComplete(dir)) {
|
|
486
|
+
log.info(`${rel.id}@${version}: index already cached`);
|
|
487
|
+
return path.join(dir, path.basename(new URL(indexUrl).pathname));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
491
|
+
// half-written index is worse than none.
|
|
492
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
493
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
494
|
+
|
|
495
|
+
log.info(`${rel.id}@${version}: downloading ${indexUrl}`);
|
|
496
|
+
const res = await fetch(indexUrl, { redirect: "follow" });
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`${rel.id}@${version}: could not download its content index at ` +
|
|
500
|
+
`${indexUrl}: HTTP ${res.status} ${res.statusText}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const file = path.join(dir, path.basename(new URL(indexUrl).pathname));
|
|
504
|
+
fs.writeFileSync(file, Buffer.from(await res.arrayBuffer()));
|
|
505
|
+
markMetadataComplete(dir);
|
|
506
|
+
return file;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Fill the index cache from a locally built artifact rather than a release.
|
|
511
|
+
*
|
|
512
|
+
* The counterpart of {@link fetchCatalogFromPath}, and the same escape hatch
|
|
513
|
+
* for the same reason: two packages being changed together cannot each wait for
|
|
514
|
+
* the other to ship. The index is looked for beside the manifest — which is
|
|
515
|
+
* where a build leaves it and where the release publishes it — so a package
|
|
516
|
+
* directory and an unpacked zip are both usable as-is.
|
|
517
|
+
*
|
|
518
|
+
* @param {object} config - The resolved build configuration.
|
|
519
|
+
* @param {{id: string}} rel - The declared relationship.
|
|
520
|
+
* @param {string} source - Path to the artifact or its directory.
|
|
521
|
+
* @returns {Promise<string>} The cached index file.
|
|
522
|
+
*/
|
|
523
|
+
export async function fetchMetadataFromPath(config, rel, source) {
|
|
524
|
+
if (!fs.existsSync(source)) {
|
|
525
|
+
throw new Error(`${rel.id}: nothing at ${source}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `content-build-meta-${rel.id}-`));
|
|
529
|
+
try {
|
|
530
|
+
let root = source;
|
|
531
|
+
if (!fs.statSync(source).isDirectory()) {
|
|
532
|
+
writeZipEntries(unzipSync(new Uint8Array(fs.readFileSync(source))), staging);
|
|
533
|
+
root = staging;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const manifest = readLocalManifest(root);
|
|
537
|
+
if (!manifest) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`${rel.id}: ${source} holds no system.json or module.json, so ` +
|
|
540
|
+
`its version cannot be read`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
if (manifest.id && manifest.id !== rel.id) {
|
|
544
|
+
throw new Error(`${rel.id}: ${source} is package "${manifest.id}", not "${rel.id}"`);
|
|
545
|
+
}
|
|
546
|
+
const version = manifest.version;
|
|
547
|
+
if (!version) {
|
|
548
|
+
throw new Error(`${rel.id}: ${source} declares no \`version\``);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Named by the manifest where it advertises one, so a local artifact
|
|
552
|
+
// and a released one are cached under the same name; falling back to
|
|
553
|
+
// the id covers a build whose manifest predates the flag.
|
|
554
|
+
const name =
|
|
555
|
+
manifest.flags?.metadataUrl ?
|
|
556
|
+
path.basename(new URL(manifest.flags.metadataUrl).pathname)
|
|
557
|
+
: metadataFileName(rel.id);
|
|
558
|
+
const found = findLocalIndex(root, name);
|
|
559
|
+
if (!found) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`${rel.id}: ${source} holds no ${name}, so it publishes no ` +
|
|
562
|
+
`content index. Build it before fetching from it.`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const dir = metadataCacheDir(config, rel.id, version);
|
|
567
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
568
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
569
|
+
const file = path.join(dir, name);
|
|
570
|
+
fs.copyFileSync(found, file);
|
|
571
|
+
markMetadataComplete(dir);
|
|
572
|
+
log.info(`${rel.id}@${version}: index cached from ${source}`);
|
|
573
|
+
return file;
|
|
574
|
+
} finally {
|
|
575
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Locate an index file in an unpacked artifact.
|
|
581
|
+
*
|
|
582
|
+
* Foundry archives are inconsistent about whether they nest their contents
|
|
583
|
+
* under a top-level directory, so try the root and then one level in — the same
|
|
584
|
+
* allowance {@link resolvePackPath} makes for packs.
|
|
585
|
+
*
|
|
586
|
+
* @param {string} root - The unpacked package root.
|
|
587
|
+
* @param {string} name - The index file name.
|
|
588
|
+
* @returns {string|null} The path, or null when absent.
|
|
589
|
+
*/
|
|
590
|
+
function findLocalIndex(root, name) {
|
|
591
|
+
const direct = path.join(root, name);
|
|
592
|
+
if (fs.existsSync(direct)) return direct;
|
|
593
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
594
|
+
if (!entry.isDirectory()) continue;
|
|
595
|
+
const nested = path.join(root, entry.name, name);
|
|
596
|
+
if (fs.existsSync(nested)) return nested;
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Fetch every declared dependency's content index.
|
|
603
|
+
*
|
|
604
|
+
* A wider set than {@link fetchAllCatalogs}: an index is fetched for *every*
|
|
605
|
+
* dependency, a catalogue only for those declaring `itemCatalog: true`. See
|
|
606
|
+
* {@link metadataRelationships} for why the two sets differ.
|
|
607
|
+
*
|
|
608
|
+
* @param {object} config - The resolved build configuration.
|
|
609
|
+
* @returns {Promise<number>} How many indexes were fetched.
|
|
610
|
+
*/
|
|
611
|
+
export async function fetchAllMetadata(config) {
|
|
612
|
+
const rels = metadataRelationships(config);
|
|
613
|
+
for (const rel of rels) await fetchMetadata(config, rel);
|
|
614
|
+
return rels.length;
|
|
615
|
+
}
|
|
616
|
+
|
|
429
617
|
/**
|
|
430
618
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
431
619
|
*
|
|
@@ -470,10 +658,13 @@ export function foreignItemCatalogDirs(config) {
|
|
|
470
658
|
`fetched. Run \`content-build deps fetch\` first.`,
|
|
471
659
|
);
|
|
472
660
|
}
|
|
473
|
-
// Newest
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
661
|
+
// Newest wins if several versions are cached; a fetch always writes
|
|
662
|
+
// the currently declared one, so that is the one to use. The
|
|
663
|
+
// comparison is the content-index cache's, shared rather than
|
|
664
|
+
// rewritten: a plain string sort would put `0.8.10` before `0.8.2` and
|
|
665
|
+
// silently resolve every embedded item against the older catalogue
|
|
666
|
+
// (#272).
|
|
667
|
+
const items = itemsDir(newestVersionDir(cached));
|
|
477
668
|
for (const name of fs.readdirSync(items)) {
|
|
478
669
|
dirs.push(path.join(items, name));
|
|
479
670
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Emitting this package's cross-package link manifest (#58).
|
|
16
16
|
*
|
|
17
|
-
* `engine/
|
|
17
|
+
* `engine/content-address.mjs` owns the address *grammar* — how a key is
|
|
18
18
|
* version is read, how a foreign file resolves. This module owns the *pass*:
|
|
19
19
|
* walking a content tree and deriving, for every note it publishes, the
|
|
20
20
|
* addresses that entry states. The two halves were split across the format
|
|
@@ -30,9 +30,12 @@
|
|
|
30
30
|
* a fact it has to be told (#1465).
|
|
31
31
|
*
|
|
32
32
|
* **An entry's `path` is derivable from the key it is filed under** (#181).
|
|
33
|
-
* `sohl-affliction-aconite` publishes at `affliction-aconite
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* `sohl-sohl-affliction-aconite` publishes at `affliction-aconite/` — the key
|
|
34
|
+
* with its package and system segments dropped — because a page's URL *is* its
|
|
35
|
+
* address; nothing in it comes from a display name, so a rename moves no URL and
|
|
36
|
+
* no uniqueness check stands between the two. The system segment goes with the
|
|
37
|
+
* package because a note publishes one page however many systems' documents it
|
|
38
|
+
* compiles into (#59). Every entry is
|
|
36
39
|
* derivable that way since #204 retired the section landing, which was the one
|
|
37
40
|
* that was not. The field is still written rather than left for a consumer to
|
|
38
41
|
* compute, because an absent `path` already means something else entirely (a
|
|
@@ -55,10 +58,16 @@
|
|
|
55
58
|
import fs from "node:fs";
|
|
56
59
|
import path from "node:path";
|
|
57
60
|
|
|
58
|
-
import { packageAddress } from "./content-address.mjs";
|
|
59
|
-
import {
|
|
61
|
+
import { canonicalKey, packageAddress } from "./content-address.mjs";
|
|
62
|
+
import { NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
|
|
63
|
+
import {
|
|
64
|
+
KNOWN_DOCUMENT_SUBTYPE_MAPS,
|
|
65
|
+
NEVER_PACKED_TYPES,
|
|
66
|
+
DERIVED_PACKED_TYPES,
|
|
67
|
+
} from "./note-claims.mjs";
|
|
60
68
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
61
|
-
import {
|
|
69
|
+
import { resolveNoteId } from "./note-ids.mjs";
|
|
70
|
+
import { compendiumUuid, currentType, packForType, pageUuid } from "./ids.mjs";
|
|
62
71
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
63
72
|
import { isHomepage } from "./homepage.mjs";
|
|
64
73
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
@@ -66,6 +75,7 @@ import {
|
|
|
66
75
|
assertNoAliasesField,
|
|
67
76
|
assertNoDraftField,
|
|
68
77
|
assertNoSectionField,
|
|
78
|
+
assertNoTraitsField,
|
|
69
79
|
} from "./retired-fields.mjs";
|
|
70
80
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
71
81
|
import { routerFor } from "./pack-router.mjs";
|
|
@@ -100,7 +110,7 @@ export const LEAD_ANCHOR = "$lead";
|
|
|
100
110
|
export function anchorsOf(entryUuid, entryId, body, name) {
|
|
101
111
|
const anchors = {};
|
|
102
112
|
splitPages(body, name).forEach((page, index) => {
|
|
103
|
-
const uuid = pageUuid(entryUuid, journalPageId(entryId, page
|
|
113
|
+
const uuid = pageUuid(entryUuid, journalPageId(entryId, page));
|
|
104
114
|
if (index === 0) anchors[LEAD_ANCHOR] = uuid;
|
|
105
115
|
if (page.anchorSlug) anchors[page.anchorSlug] = uuid;
|
|
106
116
|
});
|
|
@@ -128,7 +138,12 @@ export function anchorsOf(entryUuid, entryId, body, name) {
|
|
|
128
138
|
*/
|
|
129
139
|
export function entriesForNote(fm, name, address, body, ctx) {
|
|
130
140
|
const { contentPackage, foundryPackageId, packRouter } = ctx;
|
|
131
|
-
const key = canonicalKey(
|
|
141
|
+
const key = canonicalKey(
|
|
142
|
+
contentPackage,
|
|
143
|
+
systemOf(fm.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
|
|
144
|
+
fm.type,
|
|
145
|
+
fm.shortcode,
|
|
146
|
+
);
|
|
132
147
|
// `buildManifest` records `packageRelative(url, base)`, so the pair it is
|
|
133
148
|
// given has to round-trip. The address is already package-relative, so the
|
|
134
149
|
// honest pair is the address under a base of `"/"` — which strips straight
|
|
@@ -141,7 +156,23 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
141
156
|
// a consumer resolves the UUID verbatim, and a repository may ship several
|
|
142
157
|
// packs of one type (#1566).
|
|
143
158
|
const uuidFor = (type, id, routeFm) =>
|
|
144
|
-
|
|
159
|
+
// A type this cannot name a single compendium document for has no UUID
|
|
160
|
+
// to publish, whatever id it derives. That used to follow from such a
|
|
161
|
+
// note authoring no `id:`; since #270 every addressable note derives
|
|
162
|
+
// one, so "has an id" stopped being evidence a document exists and the
|
|
163
|
+
// rule is stated where it belongs — beside the addresses — rather than
|
|
164
|
+
// resting on an absent field. `collectFoundryEntries` skips such a note
|
|
165
|
+
// outright; the content index calls this function directly, so the
|
|
166
|
+
// guard has to live on this side of it.
|
|
167
|
+
//
|
|
168
|
+
// Two sets, for opposite reasons (see `note-claims.mjs`). A **homepage**
|
|
169
|
+
// is in no pack: it compiles to a page and there is nothing to address.
|
|
170
|
+
// A **folder** may be in several — it materialises in every pack holding
|
|
171
|
+
// a document that references it (#276) — so no one UUID identifies it,
|
|
172
|
+
// and its id is hashed under the `folder` namespace against its own
|
|
173
|
+
// address rather than under `document`. Emitting one would publish an
|
|
174
|
+
// `Item` UUID for a `Folder`, at an id no document carries.
|
|
175
|
+
id && !NEVER_PACKED_TYPES.has(String(type)) && !DERIVED_PACKED_TYPES.has(String(type)) ?
|
|
145
176
|
compendiumUuid(
|
|
146
177
|
foundryPackageId,
|
|
147
178
|
type,
|
|
@@ -153,9 +184,14 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
153
184
|
: undefined;
|
|
154
185
|
|
|
155
186
|
const carriesDoc =
|
|
156
|
-
ctx.docEntryTypes ?
|
|
187
|
+
ctx.docEntryTypes ?
|
|
188
|
+
ctx.docEntryTypes.has(String(currentType(fm.type)))
|
|
189
|
+
: hasDocEntry(fm.type);
|
|
157
190
|
if (carriesDoc) {
|
|
158
|
-
|
|
191
|
+
// `NO_SYSTEM`, whatever the item is: a documentation journal is a
|
|
192
|
+
// JournalEntry, which no game system defines, and there is one of them
|
|
193
|
+
// however many system blocks the note carries.
|
|
194
|
+
const docKey = canonicalKey(contentPackage, NO_SYSTEM, `doc${fm.type}`, fm.shortcode);
|
|
159
195
|
const docEntryId = fm.id ? itemDocEntryId(fm.id) : undefined;
|
|
160
196
|
const docUuid = uuidFor("doc", docEntryId);
|
|
161
197
|
return [
|
|
@@ -216,7 +252,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
216
252
|
* @returns {{entries: Array<object>, notes: number,
|
|
217
253
|
* skipped: Array<{file: string, reason: string}>}}
|
|
218
254
|
*/
|
|
219
|
-
export function
|
|
255
|
+
export function collectFoundryEntries(contentBase, ctx) {
|
|
220
256
|
const entries = [];
|
|
221
257
|
const skipped = [];
|
|
222
258
|
// Counted separately because they are genuinely different numbers: an item
|
|
@@ -227,6 +263,10 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
227
263
|
skipDirectories: ctx.skipDirectories,
|
|
228
264
|
})) {
|
|
229
265
|
if (!fm) continue;
|
|
266
|
+
// Its authored pin, or the id derived from its canonical address
|
|
267
|
+
// (#270). Resolved before anything reads `fm.id`, so the UUID this
|
|
268
|
+
// pass publishes is the one the pack passes compiled under.
|
|
269
|
+
resolveNoteId(fm, { pkg: ctx.contentPackage });
|
|
230
270
|
const rel = path.relative(contentBase, absPath);
|
|
231
271
|
assertNoDeclaredPackage(fm, {
|
|
232
272
|
file: rel,
|
|
@@ -236,6 +276,7 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
236
276
|
assertNoDraftField(fm, { file: rel, absPath });
|
|
237
277
|
assertNoAliasesField(fm, { file: rel, absPath });
|
|
238
278
|
assertNoSectionField(fm, { file: rel, absPath });
|
|
279
|
+
assertNoTraitsField(fm, { file: rel, absPath });
|
|
239
280
|
if (!fm.type || !fm.shortcode) continue;
|
|
240
281
|
// A homepage is addressed like every other note since #182, and a
|
|
241
282
|
// shortcode alone would now put it here. It stays out for the reason it
|
|
@@ -297,7 +338,7 @@ export function foundryIdentities(config = loadPackConfig()) {
|
|
|
297
338
|
* @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
|
|
298
339
|
* web: boolean, skipDirectories: readonly string[]}}
|
|
299
340
|
*/
|
|
300
|
-
export function
|
|
341
|
+
export function entryContext(config = loadPackConfig()) {
|
|
301
342
|
return {
|
|
302
343
|
...foundryIdentities(config),
|
|
303
344
|
web: publishesContentPages(config),
|
|
@@ -306,72 +347,3 @@ export function manifestContext(config = loadPackConfig()) {
|
|
|
306
347
|
skipDirectories: config.skipDirectories,
|
|
307
348
|
};
|
|
308
349
|
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Emits this package's link manifest.
|
|
312
|
-
*
|
|
313
|
-
* One package, because a configuration declares exactly one `contentPackage`
|
|
314
|
-
* and nothing in the surface can express a second. {@link writeManifests} keeps
|
|
315
|
-
* its package→entries map — it is the general writer — but there is no setting
|
|
316
|
-
* here to choose with.
|
|
317
|
-
*
|
|
318
|
-
* @param {object} [options] - Options.
|
|
319
|
-
* @param {string} [options.contentBase] - The content tree; defaults to the
|
|
320
|
-
* configured `paths.content`.
|
|
321
|
-
* @param {string} [options.outDir] - Where to write; defaults to the configured
|
|
322
|
-
* `paths.manifestOut`.
|
|
323
|
-
* @param {object} [options.config] - A resolved configuration; loaded when
|
|
324
|
-
* omitted.
|
|
325
|
-
* @returns {{written: Array<{package: string, file: string, count: number}>,
|
|
326
|
-
* entries: number, notes: number,
|
|
327
|
-
* skipped: Array<{file: string, reason: string}>}}
|
|
328
|
-
* @throws {Error} When the repository does not declare that it publishes a
|
|
329
|
-
* manifest, when the tree is absent, or when it yields no published note — a
|
|
330
|
-
* manifest claiming this package publishes nothing is worse than none, since
|
|
331
|
-
* a consumer reads it as authoritative and turns every link into this package
|
|
332
|
-
* into a reported typo.
|
|
333
|
-
*/
|
|
334
|
-
export function emitLinkManifest({ contentBase, outDir, config } = {}) {
|
|
335
|
-
const resolved = config ?? loadPackConfig();
|
|
336
|
-
const tree = contentBase ?? resolved.paths.content;
|
|
337
|
-
const dir = outDir ?? resolved.paths.manifestOut;
|
|
338
|
-
const ctx = manifestContext(resolved);
|
|
339
|
-
|
|
340
|
-
// A repository that has not declared it publishes a manifest must not
|
|
341
|
-
// produce one: the file is vendored by consumers and read as authoritative,
|
|
342
|
-
// so emitting it is a statement about this package rather than a local
|
|
343
|
-
// convenience. Checked here rather than in the command, so a library caller
|
|
344
|
-
// cannot route around the declaration.
|
|
345
|
-
if (!resolved.publish.manifests.publish) {
|
|
346
|
-
throw new Error(
|
|
347
|
-
`this repository does not publish a link manifest — set ` +
|
|
348
|
-
`\`publish.manifests.publish: true\` in its content-build ` +
|
|
349
|
-
`configuration to change that`,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!fs.existsSync(tree)) {
|
|
354
|
-
throw new Error(`no content tree at ${tree}`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const { entries, notes, skipped } = collectManifestEntries(tree, ctx);
|
|
358
|
-
if (entries.length === 0) {
|
|
359
|
-
throw new Error(
|
|
360
|
-
`${tree} yielded no published notes, so the manifest would ` +
|
|
361
|
-
`claim this package publishes nothing`,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const written = writeManifests(
|
|
366
|
-
new Map([[ctx.contentPackage, entries]]),
|
|
367
|
-
dir,
|
|
368
|
-
// The one surviving role of a base: `undefined` is the statement "this
|
|
369
|
-
// build publishes no pages", and no entry then carries a `path`
|
|
370
|
-
// (#1516). The value itself cancels — every address above is already
|
|
371
|
-
// package-relative — so it is a sentinel, not a location.
|
|
372
|
-
ctx.web ? { [ctx.contentPackage]: "/" } : undefined,
|
|
373
|
-
{ [ctx.contentPackage]: ctx.foundryPackageId },
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
return { written, entries: entries.length, notes, skipped };
|
|
377
|
-
}
|