@heroiclands/package-build 24.1.0 → 24.1.1
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 +10 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +183 -15
- package/bin/package-build.mjs +5 -170
- package/docs/commands.md +83 -0
- package/engine/build-assets.mjs +179 -0
- package/engine/calendar-artifacts.mjs +7 -3
- package/engine/compendiums.mjs +16 -11
- package/engine/content-session.mjs +62 -14
- package/engine/content-timings.mjs +67 -0
- package/engine/script-font-delivery.mjs +22 -15
- package/package.json +1 -1
- package/types/engine/build-assets.d.mts +23 -0
- package/types/engine/calendar-artifacts.d.mts +5 -1
- package/types/engine/content-session.d.mts +19 -1
- package/types/engine/content-timings.d.mts +14 -0
- package/types/engine/script-font-delivery.d.mts +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 24.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Content builds**
|
|
8
|
+
|
|
9
|
+
- Build indexes, calendars, assets and packs with
|
|
10
|
+
`package-build content build --calendaria-version 1.4.2`, sharing preparation
|
|
11
|
+
across validation and compilation.
|
|
12
|
+
|
|
3
13
|
## 24.1.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ npm install -D @heroiclands/package-build
|
|
|
16
16
|
|
|
17
17
|
The generated project includes a homepage, website and book targets, npm scripts, and a local `.gitignore`. [Getting started](docs/getting-started.md) routes you to a [system](docs/guides/system-project.md), [module](docs/guides/module-project.md), or [content authoring](docs/authoring/first-note.md) path. The [content-language-server](https://github.com/HeroicLands/content-language-server) provides editor navigation and completion.
|
|
18
18
|
|
|
19
|
+
Build indexes, calendars, staged assets and packs together with `package-build content build --calendaria-version 1.4.2`. The [complete content build](docs/commands.md#package-build-content-build) shares one source capture across publication, lint, links and compilation and reports each build stage's time.
|
|
20
|
+
|
|
19
21
|
Run selected content operations together with `package-build content run index lint links compile`. The [shared content command](docs/commands.md#package-build-content-run-targets) captures one source tree for the selected checks and outputs; standalone commands remain available.
|
|
20
22
|
|
|
21
23
|
Language annotations keep authored spelling: `[Zaron]{lang=vyl}` stays Roman.
|
package/bin/content-commands.mjs
CHANGED
|
@@ -135,6 +135,10 @@ import { serveSite } from "../engine/site-serve.mjs";
|
|
|
135
135
|
import { writeSiteRoot } from "../engine/site-root.mjs";
|
|
136
136
|
import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
|
|
137
137
|
import { prepareLinkSqlTables, prepareTreeSqlTables } from "../engine/sql-tables.mjs";
|
|
138
|
+
import { createContentTimings } from "../engine/content-timings.mjs";
|
|
139
|
+
import { emitCalendarArtifacts } from "../engine/calendar-artifacts.mjs";
|
|
140
|
+
import { stagePackageBuildAssets } from "../engine/build-assets.mjs";
|
|
141
|
+
import { DEPLOYED_ASSET_TYPES } from "../engine/asset-types.mjs";
|
|
138
142
|
import { createContentSession, CONTENT_TARGETS } from "../engine/content-session.mjs";
|
|
139
143
|
import { createFirstUseChecker } from "../engine/first-use.mjs";
|
|
140
144
|
import { createNameCatalog, createNamesReport, namesReportFinding } from "../engine/names.mjs";
|
|
@@ -297,7 +301,7 @@ export function registerContentCommands(cli) {
|
|
|
297
301
|
.command(proseCommand())
|
|
298
302
|
.command(contentFormatCommand())
|
|
299
303
|
.command(withIndexPreflight(linksCommand()))
|
|
300
|
-
.command(
|
|
304
|
+
.command(contentCommand())
|
|
301
305
|
.command(formatCommand())
|
|
302
306
|
.command(markdownCommand())
|
|
303
307
|
.command(
|
|
@@ -1494,25 +1498,187 @@ function markdownCommand() {
|
|
|
1494
1498
|
};
|
|
1495
1499
|
}
|
|
1496
1500
|
|
|
1497
|
-
/**
|
|
1498
|
-
* `package-build links` — check that every link in a content tree lands.
|
|
1499
|
-
*
|
|
1500
|
-
* Reports a dead `#anchor`, a dead qualified address, and a wikilink authored
|
|
1501
|
-
* in frontmatter, plus a vendored manifest that has drifted out of reach. All
|
|
1502
|
-
* of it is package-agnostic, so a consumer needs no script of its own: the
|
|
1503
|
-
* manifest directory is the only thing it might name, and that comes from its
|
|
1504
|
-
* configuration.
|
|
1505
|
-
*
|
|
1506
|
-
* @returns {object} The yargs command module.
|
|
1507
|
-
*/
|
|
1508
|
-
// eslint-disable-next-line
|
|
1501
|
+
/** Content operations with a shared invocation-local capture. */
|
|
1509
1502
|
function contentCommand() {
|
|
1510
1503
|
return {
|
|
1511
|
-
command: "content
|
|
1504
|
+
command: "content",
|
|
1505
|
+
describe: "Run content operations in one in-memory session",
|
|
1506
|
+
builder: (yargs) =>
|
|
1507
|
+
yargs
|
|
1508
|
+
.command(withIndexPreflight(contentRunCommand()))
|
|
1509
|
+
.command(contentBuildCommand())
|
|
1510
|
+
.demandCommand(1, "Choose content run or content build."),
|
|
1511
|
+
handler: () => {},
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/** The complete build writes only outside the captured input roots. */
|
|
1516
|
+
function contentBuildOutputs(config, packaging) {
|
|
1517
|
+
const roots = [
|
|
1518
|
+
config.paths.content,
|
|
1519
|
+
...DEPLOYED_ASSET_TYPES.map((entry) => path.resolve(config.paths.assets, entry.root)),
|
|
1520
|
+
];
|
|
1521
|
+
const stage = path.resolve(packaging.rootDir, packaging.stageDir);
|
|
1522
|
+
const outputs = [
|
|
1523
|
+
config.paths.contentIndex,
|
|
1524
|
+
config.paths.packJson,
|
|
1525
|
+
config.paths.stage,
|
|
1526
|
+
stage,
|
|
1527
|
+
path.join(config.rootDir, "build/calendars"),
|
|
1528
|
+
];
|
|
1529
|
+
const contains = (parent, child) => {
|
|
1530
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
1531
|
+
return (
|
|
1532
|
+
relative === "" ||
|
|
1533
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
1534
|
+
relative !== ".." &&
|
|
1535
|
+
!path.isAbsolute(relative))
|
|
1536
|
+
);
|
|
1537
|
+
};
|
|
1538
|
+
for (const output of [...outputs, ...packaging.assets.map(({ to }) => path.resolve(stage, to))])
|
|
1539
|
+
for (const root of roots)
|
|
1540
|
+
if (contains(root, output) || contains(output, root))
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
`content build output ${output} overlaps captured input ${root}; keep build outputs outside source roots`,
|
|
1543
|
+
);
|
|
1544
|
+
return [
|
|
1545
|
+
...new Set([
|
|
1546
|
+
config.paths.contentIndex,
|
|
1547
|
+
config.paths.packJson,
|
|
1548
|
+
path.join(config.rootDir, "build/calendars"),
|
|
1549
|
+
...config.packDirectories.map((name) => path.join(config.paths.stage, name)),
|
|
1550
|
+
]),
|
|
1551
|
+
];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** Publish, generate calendars, stage assets and check/compile one live session. */
|
|
1555
|
+
function contentBuildCommand() {
|
|
1556
|
+
return {
|
|
1557
|
+
command: "build",
|
|
1558
|
+
describe:
|
|
1559
|
+
"Publish fresh indexes, emit calendars, stage assets, lint, check links and compile",
|
|
1560
|
+
builder: (yargs) =>
|
|
1561
|
+
yargs
|
|
1562
|
+
.option("calendaria-version", {
|
|
1563
|
+
type: "string",
|
|
1564
|
+
demandOption: true,
|
|
1565
|
+
describe: "Calendaria import format version",
|
|
1566
|
+
})
|
|
1567
|
+
.check((argv) => {
|
|
1568
|
+
if (!/^\d+\.\d+\.\d+$/.test(argv.calendariaVersion))
|
|
1569
|
+
throw new Error("--calendaria-version requires a version such as 1.4.2");
|
|
1570
|
+
return true;
|
|
1571
|
+
}),
|
|
1572
|
+
handler: async (argv) => {
|
|
1573
|
+
const timings = createContentTimings();
|
|
1574
|
+
let session;
|
|
1575
|
+
const shared = {
|
|
1576
|
+
...argv,
|
|
1577
|
+
references: true,
|
|
1578
|
+
[PREPARATION_REPORTED]: true,
|
|
1579
|
+
[REPORTED_FAILURES]: new Set(),
|
|
1580
|
+
};
|
|
1581
|
+
const fail = (error) => {
|
|
1582
|
+
reportContentFailure(error, shared);
|
|
1583
|
+
process.exitCode = 1;
|
|
1584
|
+
};
|
|
1585
|
+
try {
|
|
1586
|
+
const config = loadPackConfig();
|
|
1587
|
+
const marker = path.join(config.paths.contentIndex, ".publication-ready");
|
|
1588
|
+
fs.rmSync(marker, { force: true });
|
|
1589
|
+
const packaging = loadPackageBuildConfig();
|
|
1590
|
+
const outputs = contentBuildOutputs(config, packaging);
|
|
1591
|
+
for (const output of outputs) fs.rmSync(output, { recursive: true, force: true });
|
|
1592
|
+
cachedMetadataIndexes(config);
|
|
1593
|
+
session = await createContentSession({ config, timings, trackChanges: true });
|
|
1594
|
+
shared[CONTENT_SESSION] = session;
|
|
1595
|
+
let prepared = false;
|
|
1596
|
+
const reportedProblems = new Set();
|
|
1597
|
+
try {
|
|
1598
|
+
assertCompendiumOperation(config, "compile");
|
|
1599
|
+
await session.prepare(["index"]);
|
|
1600
|
+
prepared = true;
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
if (error.findings) {
|
|
1603
|
+
for (const finding of error.findings) {
|
|
1604
|
+
emitDiagnostic(finding);
|
|
1605
|
+
reportedProblems.add(finding);
|
|
1606
|
+
}
|
|
1607
|
+
shared[REPORTED_FAILURES].add(error);
|
|
1608
|
+
process.exitCode = 1;
|
|
1609
|
+
} else fail(error);
|
|
1610
|
+
}
|
|
1611
|
+
for (const problem of session.problems) {
|
|
1612
|
+
if (!reportedProblems.has(problem)) emitDiagnostic(problem);
|
|
1613
|
+
process.exitCode = 1;
|
|
1614
|
+
}
|
|
1615
|
+
let staged = false;
|
|
1616
|
+
if (
|
|
1617
|
+
prepared &&
|
|
1618
|
+
(await timings.measure("index publication", () =>
|
|
1619
|
+
contentIndexCommand().handler(shared),
|
|
1620
|
+
))
|
|
1621
|
+
) {
|
|
1622
|
+
fs.writeFileSync(marker, `${config.contentPackage}\n`);
|
|
1623
|
+
try {
|
|
1624
|
+
await timings.measure("calendars", () => {
|
|
1625
|
+
const result = emitCalendarArtifacts({
|
|
1626
|
+
config,
|
|
1627
|
+
calendariaVersion: argv.calendariaVersion,
|
|
1628
|
+
sources: session.sources,
|
|
1629
|
+
});
|
|
1630
|
+
log.info(
|
|
1631
|
+
`${result.calendars} calendars emitted (${result.files} files).`,
|
|
1632
|
+
);
|
|
1633
|
+
});
|
|
1634
|
+
await timings.measure("assets", () =>
|
|
1635
|
+
stagePackageBuildAssets({
|
|
1636
|
+
config: packaging,
|
|
1637
|
+
packConfig: config,
|
|
1638
|
+
session,
|
|
1639
|
+
}),
|
|
1640
|
+
);
|
|
1641
|
+
staged = true;
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
fail(error);
|
|
1644
|
+
}
|
|
1645
|
+
try {
|
|
1646
|
+
session.assertUnchanged();
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
fs.rmSync(marker, { force: true });
|
|
1649
|
+
throw error;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
await timings.measure("validation", () => lintCommand().handler(shared));
|
|
1653
|
+
await timings.measure("links", () => linksCommand().handler(shared));
|
|
1654
|
+
if (staged)
|
|
1655
|
+
await timings.measure("compile", () =>
|
|
1656
|
+
packageCommand().handler({ ...shared, action: "compile" }),
|
|
1657
|
+
);
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
fail(error);
|
|
1660
|
+
} finally {
|
|
1661
|
+
const summary = timings.snapshot();
|
|
1662
|
+
log.info(
|
|
1663
|
+
`Content timings (exclusive): ${Object.entries(summary.stages)
|
|
1664
|
+
.map(([name, ms]) => `${name} ${Number(ms).toFixed(1)} ms`)
|
|
1665
|
+
.join("; ")}; total ${summary.totalMs.toFixed(1)} ms.`,
|
|
1666
|
+
);
|
|
1667
|
+
log.info(
|
|
1668
|
+
`Content work: ${session?.sources.size ?? 0} sources; ${session?.parsed.size ?? 0} parsed notes; ${session?.records.length ?? 0} records; reuse ${JSON.stringify(summary.hits)}; peak RSS ${(process.resourceUsage().maxRSS / 1024).toFixed(1)} MiB.`,
|
|
1669
|
+
);
|
|
1670
|
+
session?.dispose();
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function contentRunCommand() {
|
|
1677
|
+
return {
|
|
1678
|
+
command: "run <targets..>",
|
|
1512
1679
|
describe: "Run selected content operations with one in-memory source capture",
|
|
1513
1680
|
builder: (yargs) =>
|
|
1514
1681
|
yargs
|
|
1515
|
-
.positional("action", { type: "string", choices: ["run"] })
|
|
1516
1682
|
.positional("targets", { type: "string", array: true, choices: CONTENT_TARGETS })
|
|
1517
1683
|
.option("references", {
|
|
1518
1684
|
type: "boolean",
|
|
@@ -1801,9 +1967,11 @@ function contentIndexCommand() {
|
|
|
1801
1967
|
`${config.contentPackage} → ${path.relative(process.cwd(), written.file)} ` +
|
|
1802
1968
|
`(${written.rows} ${relation} rows)`,
|
|
1803
1969
|
);
|
|
1970
|
+
return true;
|
|
1804
1971
|
} catch (err) {
|
|
1805
1972
|
reportContentFailure(err, argv);
|
|
1806
1973
|
process.exitCode = 1;
|
|
1974
|
+
return false;
|
|
1807
1975
|
} finally {
|
|
1808
1976
|
if (!argv[CONTENT_SESSION]) session?.dispose();
|
|
1809
1977
|
}
|
package/bin/package-build.mjs
CHANGED
|
@@ -87,20 +87,13 @@ import yargs from "yargs";
|
|
|
87
87
|
import { hideBin } from "yargs/helpers";
|
|
88
88
|
|
|
89
89
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
90
|
-
import {
|
|
91
|
-
import { emitDiagnostic, formatDiagnostic
|
|
90
|
+
import { loadAssetTransform, stagePackageBuildAssets } from "../engine/build-assets.mjs";
|
|
91
|
+
import { emitDiagnostic, formatDiagnostic } from "../engine/diagnostics.mjs";
|
|
92
92
|
import { writeSiteRoot } from "../engine/site-root.mjs";
|
|
93
93
|
import { DEPLOY_ROOT } from "../engine/site-config.mjs";
|
|
94
94
|
import { compilesFoundryDocuments, siteBase } from "../content-config.mjs";
|
|
95
95
|
import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
|
|
96
|
-
import { cleanBuildArtifacts, resetStage
|
|
97
|
-
import { rowsStagingPictureRoots, stagePackageAssets } from "../engine/asset-shipping.mjs";
|
|
98
|
-
import { baseStyleStageEntry } from "../engine/base-styles.mjs";
|
|
99
|
-
import {
|
|
100
|
-
checkScriptFonts,
|
|
101
|
-
copyScriptFonts,
|
|
102
|
-
STAGED_FONTS_DIR,
|
|
103
|
-
} from "../engine/script-font-delivery.mjs";
|
|
96
|
+
import { cleanBuildArtifacts, resetStage } from "../stage.mjs";
|
|
104
97
|
import {
|
|
105
98
|
CDN_ENV,
|
|
106
99
|
cdnCredentials,
|
|
@@ -438,171 +431,13 @@ function cleanCommand() {
|
|
|
438
431
|
};
|
|
439
432
|
}
|
|
440
433
|
|
|
441
|
-
/**
|
|
442
|
-
* A finding for each `packageBuild.assets` row that would stage a picture root
|
|
443
|
-
* itself, located at the row's `from` when the configuration is YAML.
|
|
444
|
-
*
|
|
445
|
-
* @param {readonly {from: string, to: string}[]} rows - The configured rows.
|
|
446
|
-
* @param {{rootDir: string, assetsBase: string}} opts - Where they resolve.
|
|
447
|
-
* @returns {object[]} The findings.
|
|
448
|
-
*/
|
|
449
|
-
function pictureRootRowFindings(rows, { rootDir, assetsBase }) {
|
|
450
|
-
const offending = rowsStagingPictureRoots(rows, { rootDir, assetsBase });
|
|
451
|
-
if (!offending.length) return [];
|
|
452
|
-
const file = packConfigPath();
|
|
453
|
-
const text =
|
|
454
|
-
file.endsWith(".yaml") || file.endsWith(".yml") ? fs.readFileSync(file, "utf8") : "";
|
|
455
|
-
return offending.map(({ index, from, root }) => ({
|
|
456
|
-
file,
|
|
457
|
-
...(text ? positionOfYamlPath(text, ["packageBuild", "assets", index, "from"]) : {}),
|
|
458
|
-
severity: "error",
|
|
459
|
-
message:
|
|
460
|
-
`\`packageBuild.assets[${index}]\` copies \`${from}\`, which reaches ${root}/ — ` +
|
|
461
|
-
"the stage copies the icon and image roots itself, flat and filtered by " +
|
|
462
|
-
"each file's provenance, so a row copying them would ship every file " +
|
|
463
|
-
"unfiltered. Remove the row",
|
|
464
|
-
}));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* The repository's `packageBuild.assetTransform`, loaded.
|
|
469
|
-
*
|
|
470
|
-
* One loader for every command that writes a picture, so the stage and the CDN
|
|
471
|
-
* hold the same bytes for the same file.
|
|
472
|
-
*
|
|
473
|
-
* @param {object} config - The resolved package-build configuration.
|
|
474
|
-
* @returns {Promise<((sourcePath: string) => string|null|undefined)|undefined>}
|
|
475
|
-
* The transform, or `undefined` when the repository names none.
|
|
476
|
-
*/
|
|
477
|
-
async function loadAssetTransform(config) {
|
|
478
|
-
if (!config.assetTransform) return undefined;
|
|
479
|
-
const module = await import(`file://${config.assetTransform}`).catch((err) =>
|
|
480
|
-
die(
|
|
481
|
-
`cannot load \`packageBuild.assetTransform\` ` +
|
|
482
|
-
`(${config.assetTransform}): ${err.message}`,
|
|
483
|
-
),
|
|
484
|
-
);
|
|
485
|
-
const transform = module.transform;
|
|
486
|
-
if (typeof transform !== "function") {
|
|
487
|
-
die(
|
|
488
|
-
`\`packageBuild.assetTransform\` ` +
|
|
489
|
-
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
490
|
-
`function. It must export ` +
|
|
491
|
-
`\`transform(sourcePath) -> string | null\`.`,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
return transform;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* `assets` — stage the repository's static files into the package root.
|
|
499
|
-
*
|
|
500
|
-
* The table is data (`packageBuild.assets`). A repository that has to *change*
|
|
501
|
-
* a file on the way — SoHL rewrites each SVG's hard-coded fill so icons follow
|
|
502
|
-
* the Foundry theme — names a module in `packageBuild.assetTransform`, whose
|
|
503
|
-
* `transform(sourcePath)` returns replacement text or `null` to copy verbatim.
|
|
504
|
-
* That is the one genuine piece of code in the job, and it stays the
|
|
505
|
-
* repository's.
|
|
506
|
-
*
|
|
507
|
-
* @returns {object} The yargs command module.
|
|
508
|
-
*/
|
|
434
|
+
/** Stage the configured package assets. */
|
|
509
435
|
function assetsCommand() {
|
|
510
436
|
return {
|
|
511
437
|
command: "assets",
|
|
512
438
|
describe: "Stage static assets into the package root",
|
|
513
439
|
builder: (y) => y,
|
|
514
|
-
handler: handler(
|
|
515
|
-
const config = loadPackageBuildConfig();
|
|
516
|
-
const packConfig = loadPackConfig();
|
|
517
|
-
const assetsBase = packConfig.paths.assets;
|
|
518
|
-
const findings = [
|
|
519
|
-
...pictureRootRowFindings(config.assets, {
|
|
520
|
-
rootDir: config.rootDir,
|
|
521
|
-
assetsBase,
|
|
522
|
-
}),
|
|
523
|
-
...checkForeignAssetBindings(packConfig, { assets: config.assets }),
|
|
524
|
-
];
|
|
525
|
-
if (findings.length) {
|
|
526
|
-
const error = new Error(findings.map(formatDiagnostic).join("\n"));
|
|
527
|
-
error.located = true;
|
|
528
|
-
die(error);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const transform = await loadAssetTransform(config);
|
|
532
|
-
|
|
533
|
-
// `to:` is relative to the staged package root, so a
|
|
534
|
-
// repository's table reads `lang`, not `build/stage/lang`.
|
|
535
|
-
const entries = config.assets.map(({ from, to }) => [
|
|
536
|
-
from,
|
|
537
|
-
path.join(config.stageDir, to),
|
|
538
|
-
]);
|
|
539
|
-
let { entries: count, files } = stageAssets(entries, {
|
|
540
|
-
cwd: config.rootDir,
|
|
541
|
-
transform,
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
// The picture roots, staged by the toolchain rather than by a row:
|
|
545
|
-
// flat, filtered by each file's provenance, and listed in the
|
|
546
|
-
// package's asset set.
|
|
547
|
-
const pictures = stagePackageAssets({
|
|
548
|
-
assetsBase,
|
|
549
|
-
contentPackage: packConfig.contentPackage,
|
|
550
|
-
stageDir: path.resolve(config.rootDir, config.stageDir),
|
|
551
|
-
// The manifest's title names the set; a package with none is
|
|
552
|
-
// named by its content package.
|
|
553
|
-
title:
|
|
554
|
-
typeof config.manifest?.title === "string" && config.manifest.title.trim() ?
|
|
555
|
-
config.manifest.title
|
|
556
|
-
: packConfig.contentPackage,
|
|
557
|
-
transform,
|
|
558
|
-
shippingLicenses: config.shippingLicenses,
|
|
559
|
-
});
|
|
560
|
-
if (pictures.findings.length) {
|
|
561
|
-
const error = new Error(pictures.findings.map(formatDiagnostic).join("\n"));
|
|
562
|
-
error.located = true;
|
|
563
|
-
die(error);
|
|
564
|
-
}
|
|
565
|
-
files += pictures.staged.length;
|
|
566
|
-
if (pictures.staged.length || pictures.excluded.length) {
|
|
567
|
-
console.log(
|
|
568
|
-
`Pictures: ${pictures.staged.length} staged, ` +
|
|
569
|
-
`${pictures.excluded.length} left out by provenance.`,
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Staged in its own pass, without the repository's transform: the
|
|
574
|
-
// sheet is this toolchain's file rather than one of the
|
|
575
|
-
// repository's assets, and a `transform` written to retheme a
|
|
576
|
-
// package's own SVGs has no business being handed it.
|
|
577
|
-
if (config.baseStyles) {
|
|
578
|
-
const staged = stageAssets([baseStyleStageEntry(config.stageDir)], {
|
|
579
|
-
cwd: config.rootDir,
|
|
580
|
-
});
|
|
581
|
-
count += staged.entries;
|
|
582
|
-
files += staged.files;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// The installed fonts, when the package has installed the fonts
|
|
586
|
-
// package and a note sets text in one of its faces: the
|
|
587
|
-
// stylesheet, the WOFF2 files and the licence, which the
|
|
588
|
-
// manifest's `styles` names.
|
|
589
|
-
const fontFindings = checkScriptFonts(packConfig);
|
|
590
|
-
if (fontFindings.length) {
|
|
591
|
-
const error = new Error(fontFindings.map(formatDiagnostic).join("\n"));
|
|
592
|
-
error.located = true;
|
|
593
|
-
die(error);
|
|
594
|
-
}
|
|
595
|
-
const fonts = copyScriptFonts(
|
|
596
|
-
packConfig,
|
|
597
|
-
path.resolve(config.rootDir, config.stageDir, STAGED_FONTS_DIR),
|
|
598
|
-
["woff2"],
|
|
599
|
-
);
|
|
600
|
-
if (fonts) {
|
|
601
|
-
count += 1;
|
|
602
|
-
files += fonts;
|
|
603
|
-
}
|
|
604
|
-
console.log(`✅ Static assets staged (${count} entries, ${files} files).`);
|
|
605
|
-
}),
|
|
440
|
+
handler: handler(() => stagePackageBuildAssets()),
|
|
606
441
|
};
|
|
607
442
|
}
|
|
608
443
|
|
package/docs/commands.md
CHANGED
|
@@ -1556,6 +1556,89 @@ package-build: This repository declares no end-to-end suite to `run`. Name one u
|
|
|
1556
1556
|
|
|
1557
1557
|
## Content operations
|
|
1558
1558
|
|
|
1559
|
+
### `package-build content build`
|
|
1560
|
+
|
|
1561
|
+
**NAME**
|
|
1562
|
+
|
|
1563
|
+
Build indexes, calendars, assets and packs from one captured content tree.
|
|
1564
|
+
|
|
1565
|
+
**SYNOPSIS**
|
|
1566
|
+
|
|
1567
|
+
```text
|
|
1568
|
+
package-build content build --calendaria-version <version>
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
**DESCRIPTION**
|
|
1572
|
+
|
|
1573
|
+
The complete content build runs these existing operations in order:
|
|
1574
|
+
|
|
1575
|
+
1. Capture authored sources and prepare and publish fresh metadata, dictionary
|
|
1576
|
+
and terms indexes.
|
|
1577
|
+
2. Emit calendar definitions and Calendaria imports from the published index.
|
|
1578
|
+
3. Stage the configured assets, including calendars, provenance-filtered
|
|
1579
|
+
pictures, base styles and fonts.
|
|
1580
|
+
4. Run content lint, link checks and pack compilation with the same live session.
|
|
1581
|
+
|
|
1582
|
+
Finish dependency fetching and source generation before invoking the command.
|
|
1583
|
+
Public and complete SQL preparations remain separate and each is reused by
|
|
1584
|
+
operations needing that audience. No resolved index or source cache survives
|
|
1585
|
+
into another invocation. Standalone commands and `content run` remain available.
|
|
1586
|
+
|
|
1587
|
+
The command clears its index directory, generated calendar directory, pack JSON
|
|
1588
|
+
and configured compiled pack directories before preparing the new build. Other
|
|
1589
|
+
stage files, including a manifest written by a parallel packaging task, remain.
|
|
1590
|
+
Use `package-build stage reset` before starting the complete packaging workflow
|
|
1591
|
+
when the entire stage must start empty. Build output paths must be outside the
|
|
1592
|
+
captured content and picture roots. A phase that changes those inputs fails the
|
|
1593
|
+
build, invalidates publication readiness and stops further use of the capture;
|
|
1594
|
+
source generation belongs before this command.
|
|
1595
|
+
|
|
1596
|
+
After all three index files are published successfully, the command writes
|
|
1597
|
+
`<paths.contentIndex>/.publication-ready` (normally
|
|
1598
|
+
`build/content-index/.publication-ready`). It removes that marker at startup.
|
|
1599
|
+
The marker means this invocation published a complete fresh index, even when
|
|
1600
|
+
calendar emission, asset staging, lint, link checks or compilation later fails.
|
|
1601
|
+
A subsequent index consumer can check it to report independent findings after a
|
|
1602
|
+
build failure. Clear the marker before a surrounding workflow starts as well,
|
|
1603
|
+
so failures before the command starts cannot expose an earlier run's index.
|
|
1604
|
+
Never restore the marker or resolved indexes from a cache.
|
|
1605
|
+
|
|
1606
|
+
Preparation failure blocks publication and compilation but preserves useful
|
|
1607
|
+
independent lint and link findings. Calendar or asset failure blocks compilation;
|
|
1608
|
+
ordinary lint or link findings do not block an otherwise valid compile. Pack
|
|
1609
|
+
generation still refuses to compile incomplete output. An advisory warning does
|
|
1610
|
+
not fail the command.
|
|
1611
|
+
|
|
1612
|
+
The final timing summary includes eager capture and index derivation, SQL by
|
|
1613
|
+
audience, publication, calendars, assets, validation, rendering and native pack
|
|
1614
|
+
compilation. Stage times are exclusive: a nested SQL preparation is charged to
|
|
1615
|
+
SQL rather than its calling stage. Counts, preparation reuse and peak process
|
|
1616
|
+
RSS accompany the timings; importing the CLI and validating its arguments happen
|
|
1617
|
+
before the measured build interval.
|
|
1618
|
+
|
|
1619
|
+
**OPTIONS**
|
|
1620
|
+
|
|
1621
|
+
| Option | Type | Default | Description |
|
|
1622
|
+
| ---------------------- | ------ | -------- | -------------------------------------- |
|
|
1623
|
+
| `--calendaria-version` | string | required | Calendaria import version, e.g. 1.4.2. |
|
|
1624
|
+
|
|
1625
|
+
**EXIT STATUS**
|
|
1626
|
+
|
|
1627
|
+
0 when every operation succeeds. 1 when arguments, preparation or any build
|
|
1628
|
+
operation fails. Publication readiness alone does not mean the build passed.
|
|
1629
|
+
|
|
1630
|
+
**EXAMPLES**
|
|
1631
|
+
|
|
1632
|
+
```bash
|
|
1633
|
+
package-build deps fetch
|
|
1634
|
+
package-build content build --calendaria-version 1.4.2
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
**SEE ALSO**
|
|
1638
|
+
|
|
1639
|
+
`package-build content run`, `package-build content-index`, `package-build calendars`,
|
|
1640
|
+
`package-build assets`, `package-build package compile`.
|
|
1641
|
+
|
|
1559
1642
|
### `package-build content run <targets..>`
|
|
1560
1643
|
|
|
1561
1644
|
**NAME**
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { loadPackageBuildConfig } from "../config.mjs";
|
|
18
|
+
import { loadPackConfig, packConfigPath } from "./pack-config.mjs";
|
|
19
|
+
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
20
|
+
import { formatDiagnostic, positionOfYamlPath } from "./diagnostics.mjs";
|
|
21
|
+
import { stageAssets } from "../stage.mjs";
|
|
22
|
+
import { rowsStagingPictureRoots, stagePackageAssets } from "./asset-shipping.mjs";
|
|
23
|
+
import { baseStyleStageEntry } from "./base-styles.mjs";
|
|
24
|
+
import { checkScriptFonts, copyScriptFonts, STAGED_FONTS_DIR } from "./script-font-delivery.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A finding for each `packageBuild.assets` row that would stage a picture root
|
|
28
|
+
* itself, located at the row's `from` when the configuration is YAML.
|
|
29
|
+
*
|
|
30
|
+
* @param {readonly {from: string, to: string}[]} rows - The configured rows.
|
|
31
|
+
* @param {{rootDir: string, assetsBase: string}} opts - Where they resolve.
|
|
32
|
+
* @returns {object[]} The findings.
|
|
33
|
+
*/
|
|
34
|
+
function pictureRootRowFindings(rows, { rootDir, assetsBase }) {
|
|
35
|
+
const offending = rowsStagingPictureRoots(rows, { rootDir, assetsBase });
|
|
36
|
+
if (!offending.length) return [];
|
|
37
|
+
const file = packConfigPath();
|
|
38
|
+
const text =
|
|
39
|
+
file.endsWith(".yaml") || file.endsWith(".yml") ? fs.readFileSync(file, "utf8") : "";
|
|
40
|
+
return offending.map(({ index, from, root }) => ({
|
|
41
|
+
file,
|
|
42
|
+
...(text ? positionOfYamlPath(text, ["packageBuild", "assets", index, "from"]) : {}),
|
|
43
|
+
severity: "error",
|
|
44
|
+
message:
|
|
45
|
+
`\`packageBuild.assets[${index}]\` copies \`${from}\`, which reaches ${root}/ — ` +
|
|
46
|
+
"the stage copies the icon and image roots itself, flat and filtered by " +
|
|
47
|
+
"each file's provenance, so a row copying them would ship every file " +
|
|
48
|
+
"unfiltered. Remove the row",
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The repository's `packageBuild.assetTransform`, loaded.
|
|
54
|
+
*
|
|
55
|
+
* One loader for every command that writes a picture, so the stage and the CDN
|
|
56
|
+
* hold the same bytes for the same file.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} config - The resolved package-build configuration.
|
|
59
|
+
* @returns {Promise<((sourcePath: string) => string|null|undefined)|undefined>}
|
|
60
|
+
* The transform, or `undefined` when the repository names none.
|
|
61
|
+
*/
|
|
62
|
+
export async function loadAssetTransform(config) {
|
|
63
|
+
if (!config.assetTransform) return undefined;
|
|
64
|
+
const module = await import(pathToFileURL(config.assetTransform).href).catch((err) => {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`cannot load \`packageBuild.assetTransform\` (${config.assetTransform}): ${err.message}`,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
const transform = module.transform;
|
|
70
|
+
if (typeof transform !== "function") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`\`packageBuild.assetTransform\` ` +
|
|
73
|
+
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
74
|
+
`function. It must export ` +
|
|
75
|
+
`\`transform(sourcePath) -> string | null\`.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return transform;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stage static files, provenance-filtered pictures, base styles and fonts.
|
|
83
|
+
* @param {object} [options]
|
|
84
|
+
* @param {ReturnType<typeof loadPackageBuildConfig>} [options.config]
|
|
85
|
+
* @param {import("../content-config.mjs").ContentBuildConfig} [options.packConfig]
|
|
86
|
+
* @param {import("./content-session.mjs").ContentSession} [options.session]
|
|
87
|
+
*/
|
|
88
|
+
export async function stagePackageBuildAssets({
|
|
89
|
+
config = loadPackageBuildConfig(),
|
|
90
|
+
packConfig = loadPackConfig(),
|
|
91
|
+
session,
|
|
92
|
+
} = {}) {
|
|
93
|
+
const assetsBase = packConfig.paths.assets;
|
|
94
|
+
const findings = [
|
|
95
|
+
...pictureRootRowFindings(config.assets, {
|
|
96
|
+
rootDir: config.rootDir,
|
|
97
|
+
assetsBase,
|
|
98
|
+
}),
|
|
99
|
+
...checkForeignAssetBindings(packConfig, { assets: config.assets }),
|
|
100
|
+
];
|
|
101
|
+
if (findings.length) {
|
|
102
|
+
const error = new Error(findings.map(formatDiagnostic).join("\n"));
|
|
103
|
+
error.located = true;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const transform = await loadAssetTransform(config);
|
|
108
|
+
|
|
109
|
+
// `to:` is relative to the staged package root, so a
|
|
110
|
+
// repository's table reads `lang`, not `build/stage/lang`.
|
|
111
|
+
const entries = config.assets.map(({ from, to }) => [from, path.join(config.stageDir, to)]);
|
|
112
|
+
let { entries: count, files } = stageAssets(entries, {
|
|
113
|
+
cwd: config.rootDir,
|
|
114
|
+
transform,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The picture roots, staged by the toolchain rather than by a row:
|
|
118
|
+
// flat, filtered by each file's provenance, and listed in the
|
|
119
|
+
// package's asset set.
|
|
120
|
+
const pictures = stagePackageAssets({
|
|
121
|
+
assetsBase,
|
|
122
|
+
contentPackage: packConfig.contentPackage,
|
|
123
|
+
stageDir: path.resolve(config.rootDir, config.stageDir),
|
|
124
|
+
// The manifest's title names the set; a package with none is
|
|
125
|
+
// named by its content package.
|
|
126
|
+
title:
|
|
127
|
+
typeof config.manifest?.title === "string" && config.manifest.title.trim() ?
|
|
128
|
+
config.manifest.title
|
|
129
|
+
: packConfig.contentPackage,
|
|
130
|
+
transform,
|
|
131
|
+
shippingLicenses: config.shippingLicenses,
|
|
132
|
+
});
|
|
133
|
+
if (pictures.findings.length) {
|
|
134
|
+
const error = new Error(pictures.findings.map(formatDiagnostic).join("\n"));
|
|
135
|
+
error.located = true;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
files += pictures.staged.length;
|
|
139
|
+
if (pictures.staged.length || pictures.excluded.length) {
|
|
140
|
+
console.log(
|
|
141
|
+
`Pictures: ${pictures.staged.length} staged, ` +
|
|
142
|
+
`${pictures.excluded.length} left out by provenance.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Staged in its own pass, without the repository's transform: the
|
|
147
|
+
// sheet is this toolchain's file rather than one of the
|
|
148
|
+
// repository's assets, and a `transform` written to retheme a
|
|
149
|
+
// package's own SVGs has no business being handed it.
|
|
150
|
+
if (config.baseStyles) {
|
|
151
|
+
const staged = stageAssets([baseStyleStageEntry(config.stageDir)], {
|
|
152
|
+
cwd: config.rootDir,
|
|
153
|
+
});
|
|
154
|
+
count += staged.entries;
|
|
155
|
+
files += staged.files;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The installed fonts, when the package has installed the fonts
|
|
159
|
+
// package and a note sets text in one of its faces: the
|
|
160
|
+
// stylesheet, the WOFF2 files and the licence, which the
|
|
161
|
+
// manifest's `styles` names.
|
|
162
|
+
const fontFindings = checkScriptFonts(packConfig, session);
|
|
163
|
+
if (fontFindings.length) {
|
|
164
|
+
const error = new Error(fontFindings.map(formatDiagnostic).join("\n"));
|
|
165
|
+
error.located = true;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const fonts = copyScriptFonts(
|
|
169
|
+
packConfig,
|
|
170
|
+
path.resolve(config.rootDir, config.stageDir, STAGED_FONTS_DIR),
|
|
171
|
+
["woff2"],
|
|
172
|
+
session,
|
|
173
|
+
);
|
|
174
|
+
if (fonts) {
|
|
175
|
+
count += 1;
|
|
176
|
+
files += fonts;
|
|
177
|
+
}
|
|
178
|
+
console.log(`✅ Static assets staged (${count} entries, ${files} files).`);
|
|
179
|
+
}
|
|
@@ -77,16 +77,20 @@ function readIndex(file) {
|
|
|
77
77
|
*
|
|
78
78
|
* @param {object} opts
|
|
79
79
|
* @param {object} opts.config - Resolved content configuration.
|
|
80
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Captured authored text for diagnostics.
|
|
80
81
|
* @param {string} opts.calendariaVersion - Version whose import format is targeted.
|
|
81
82
|
* @returns {{directory: string, calendars: number, files: number}}
|
|
82
83
|
*/
|
|
83
|
-
export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
84
|
+
export function emitCalendarArtifacts({ config, calendariaVersion, sources } = {}) {
|
|
84
85
|
if (!config) throw new Error("calendar emission requires a content configuration");
|
|
85
86
|
if (!/^\d+\.\d+\.\d+$/.test(calendariaVersion ?? "")) {
|
|
86
87
|
throw new Error("calendar emission requires a Calendaria version such as 1.4.2");
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const root = config.rootDir;
|
|
91
|
+
const rawSource = (file) =>
|
|
92
|
+
sources?.get(path.resolve(root, file))?.raw ??
|
|
93
|
+
fs.readFileSync(path.resolve(root, file), "utf8");
|
|
90
94
|
const contentBase = path.resolve(root, config.paths.content);
|
|
91
95
|
const indexFile = path.resolve(
|
|
92
96
|
root,
|
|
@@ -108,7 +112,7 @@ export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
|
108
112
|
);
|
|
109
113
|
const names = new Map();
|
|
110
114
|
for (const note of calendars) {
|
|
111
|
-
const raw =
|
|
115
|
+
const raw = rawSource(note.file);
|
|
112
116
|
const shortcode = note.fm.shortcode;
|
|
113
117
|
if (typeof shortcode !== "string" || !/^[a-z0-9]+$/.test(shortcode)) {
|
|
114
118
|
failAt(
|
|
@@ -135,7 +139,7 @@ export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
|
135
139
|
failAt(
|
|
136
140
|
note.file,
|
|
137
141
|
"calendar notes require a world note declaring data.year",
|
|
138
|
-
|
|
142
|
+
rawSource(note.file),
|
|
139
143
|
["subType"],
|
|
140
144
|
);
|
|
141
145
|
}
|
package/engine/compendiums.mjs
CHANGED
|
@@ -96,6 +96,7 @@ export async function compilePacks({
|
|
|
96
96
|
contentBase: config.paths.content,
|
|
97
97
|
skipDirectories: config.skipDirectories,
|
|
98
98
|
});
|
|
99
|
+
const measured = (name, work) => (session ? session.timings.measure(name, work) : work());
|
|
99
100
|
const packNames = sourcePacks.filter((name) => !packName || name === packName);
|
|
100
101
|
|
|
101
102
|
// A prebuilt pack's JSON is checked in rather than generated, so it is
|
|
@@ -114,12 +115,14 @@ export async function compilePacks({
|
|
|
114
115
|
// content tree, and a package that ships only prebuilt packs has none at
|
|
115
116
|
// all — so running it would fail the build over a tree nothing reads.
|
|
116
117
|
if (needsGeneration) {
|
|
117
|
-
const errors = await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
const errors = await measured("rendering/documents", () =>
|
|
119
|
+
generatePacksJson({
|
|
120
|
+
only: packName,
|
|
121
|
+
config,
|
|
122
|
+
session,
|
|
123
|
+
reportPreparation,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
123
126
|
if (errors > 0) {
|
|
124
127
|
throw new Error(
|
|
125
128
|
`Pack JSON generation reported ${errors} error(s); refusing to ` +
|
|
@@ -141,11 +144,13 @@ export async function compilePacks({
|
|
|
141
144
|
|
|
142
145
|
const stage = path.join(stageDest, name);
|
|
143
146
|
log.info(`Pack ${name}: compiling to LevelDB at ${stage}`);
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
await measured("pack compilation", () =>
|
|
148
|
+
compilePack(source, stage, {
|
|
149
|
+
recursive: true,
|
|
150
|
+
log: false,
|
|
151
|
+
transformEntry: cleanPackEntry,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
149
154
|
|
|
150
155
|
// A Scene's map image lives on an embedded Level, stored under its own
|
|
151
156
|
// LevelDB key. Nothing in Foundry ties the two together on read: a
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
/** One invocation's authored sources and context-specific prepared views. */
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import { DEPLOYED_ASSET_TYPES } from "./asset-types.mjs";
|
|
18
|
+
import { createContentTimings } from "./content-timings.mjs";
|
|
16
19
|
import * as contentIndex from "./content-index.mjs";
|
|
17
20
|
import * as blocks from "./content-blocks.mjs";
|
|
18
21
|
import * as alerts from "./content-alerts.mjs";
|
|
@@ -52,6 +55,8 @@ import { lintNoteStates } from "./stub-lint.mjs";
|
|
|
52
55
|
* @property {() => Promise<import("./content-index.mjs").PreparedContentIndex>} publication
|
|
53
56
|
* @property {() => ReturnType<import("./compile-corpus.mjs").buildCompileCorpus>} compileCorpus
|
|
54
57
|
* @property {(targets: readonly ContentTarget[]) => Promise<ContentSession>} prepare
|
|
58
|
+
* @property {ReturnType<typeof createContentTimings>} timings
|
|
59
|
+
* @property {() => void} assertUnchanged
|
|
55
60
|
* @property {() => void} dispose
|
|
56
61
|
*/
|
|
57
62
|
|
|
@@ -71,6 +76,8 @@ export const CONTENT_TARGETS = Object.freeze(["index", "lint", "links", "compile
|
|
|
71
76
|
* @param {string} [options.assetsBase] - Assets root, defaulting to configuration.
|
|
72
77
|
* @param {readonly string[]} [options.skipDirectories] - Resolved content scope.
|
|
73
78
|
* @param {ReturnType<typeof routerFor>} [options.router] - One router shared by this session's compile.
|
|
79
|
+
* @param {boolean} [options.trackChanges] - Snapshot physical input metadata for build phase guards.
|
|
80
|
+
* @param {ReturnType<typeof createContentTimings>} [options.timings] - Invocation stage accounting.
|
|
74
81
|
* @returns {Promise<ContentSession>} Captured sources and lazy prepared views; dispose after use.
|
|
75
82
|
*/
|
|
76
83
|
export async function createContentSession({
|
|
@@ -79,14 +86,36 @@ export async function createContentSession({
|
|
|
79
86
|
assetsBase = config.paths.assets,
|
|
80
87
|
skipDirectories = config.skipDirectories,
|
|
81
88
|
router = routerFor(config),
|
|
89
|
+
timings = createContentTimings(),
|
|
90
|
+
trackChanges = false,
|
|
82
91
|
} = {}) {
|
|
83
92
|
const tree = path.resolve(contentBase);
|
|
84
93
|
const assets = path.resolve(assetsBase);
|
|
85
94
|
const scope = [...skipDirectories];
|
|
86
|
-
const inventory =
|
|
95
|
+
const inventory = timings.sync("source capture", () =>
|
|
96
|
+
captureContentSources(tree, assets, scope),
|
|
97
|
+
);
|
|
98
|
+
const fingerprint = (file) => {
|
|
99
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false });
|
|
100
|
+
return stat ? `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : "absent";
|
|
101
|
+
};
|
|
102
|
+
const fingerprints =
|
|
103
|
+
trackChanges ?
|
|
104
|
+
new Map(
|
|
105
|
+
[
|
|
106
|
+
...new Set([
|
|
107
|
+
tree,
|
|
108
|
+
...DEPLOYED_ASSET_TYPES.map((entry) => path.resolve(assets, entry.root)),
|
|
109
|
+
...inventory.paths.map((entry) => entry.file),
|
|
110
|
+
]),
|
|
111
|
+
].map((file) => [file, fingerprint(file)]),
|
|
112
|
+
)
|
|
113
|
+
: undefined;
|
|
87
114
|
const parsed = new Map();
|
|
88
115
|
const problems = [];
|
|
89
|
-
const foreign =
|
|
116
|
+
const foreign = timings.sync("dependency indexes", () =>
|
|
117
|
+
loadForeignIndexes(config, [config.contentPackage]),
|
|
118
|
+
);
|
|
90
119
|
const syntaxCache = new Map();
|
|
91
120
|
const prepared = new Map();
|
|
92
121
|
let disposed = false;
|
|
@@ -104,23 +133,31 @@ export async function createContentSession({
|
|
|
104
133
|
});
|
|
105
134
|
return syntaxCache.get(text);
|
|
106
135
|
};
|
|
107
|
-
const records =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
const records = timings.sync("index derivation", () =>
|
|
137
|
+
contentIndex.indexRecordsFor({
|
|
138
|
+
config,
|
|
139
|
+
contentBase: tree,
|
|
140
|
+
assetsBase: assets,
|
|
141
|
+
skipDirectories: scope,
|
|
142
|
+
sources: inventory.indexSources,
|
|
143
|
+
parsed,
|
|
144
|
+
problems,
|
|
145
|
+
foreignIndex: foreign.index,
|
|
146
|
+
syntax,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
118
149
|
const memo = (key, make) => {
|
|
119
150
|
active();
|
|
120
|
-
if (!prepared.has(key))
|
|
151
|
+
if (!prepared.has(key))
|
|
152
|
+
prepared.set(
|
|
153
|
+
key,
|
|
154
|
+
Promise.resolve().then(() => timings.measure(key, make)),
|
|
155
|
+
);
|
|
156
|
+
else timings.hit(key);
|
|
121
157
|
return prepared.get(key);
|
|
122
158
|
};
|
|
123
159
|
const session = {
|
|
160
|
+
timings,
|
|
124
161
|
config,
|
|
125
162
|
contentBase: tree,
|
|
126
163
|
assetsBase: assets,
|
|
@@ -133,6 +170,17 @@ export async function createContentSession({
|
|
|
133
170
|
problems,
|
|
134
171
|
foreign,
|
|
135
172
|
syntax,
|
|
173
|
+
/** Refuse a stale capture after any build phase changes an input. */
|
|
174
|
+
assertUnchanged() {
|
|
175
|
+
active();
|
|
176
|
+
if (!fingerprints)
|
|
177
|
+
throw new Error("Content change tracking was not enabled for this session");
|
|
178
|
+
for (const [file, before] of fingerprints)
|
|
179
|
+
if (fingerprint(file) !== before)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Content sources changed during the build: ${file}. Finish source generation before content build.`,
|
|
182
|
+
);
|
|
183
|
+
},
|
|
136
184
|
/** Refuse use with a different configuration, root, scope or router. */
|
|
137
185
|
assertContext(options = {}) {
|
|
138
186
|
active();
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/** Sequential, nested stage accounting for one content invocation. */
|
|
15
|
+
export function createContentTimings() {
|
|
16
|
+
const started = performance.now();
|
|
17
|
+
let changed = started;
|
|
18
|
+
const stack = ["orchestration"];
|
|
19
|
+
const stages = new Map();
|
|
20
|
+
const hits = new Map();
|
|
21
|
+
const account = () => {
|
|
22
|
+
const now = performance.now();
|
|
23
|
+
const name = stack.at(-1);
|
|
24
|
+
stages.set(name, (stages.get(name) ?? 0) + now - changed);
|
|
25
|
+
changed = now;
|
|
26
|
+
};
|
|
27
|
+
const begin = (name) => {
|
|
28
|
+
account();
|
|
29
|
+
stack.push(name);
|
|
30
|
+
return () => {
|
|
31
|
+
account();
|
|
32
|
+
stack.pop();
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
/** @template T @param {string} name @param {() => T} work @returns {T} */
|
|
37
|
+
sync(name, work) {
|
|
38
|
+
const stop = begin(name);
|
|
39
|
+
try {
|
|
40
|
+
return work();
|
|
41
|
+
} finally {
|
|
42
|
+
stop();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
/** @template T @param {string} name @param {() => Promise<T>|T} work @returns {Promise<T>} */
|
|
46
|
+
async measure(name, work) {
|
|
47
|
+
const stop = begin(name);
|
|
48
|
+
try {
|
|
49
|
+
return await work();
|
|
50
|
+
} finally {
|
|
51
|
+
stop();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
/** @param {string} name */
|
|
55
|
+
hit(name) {
|
|
56
|
+
hits.set(name, (hits.get(name) ?? 0) + 1);
|
|
57
|
+
},
|
|
58
|
+
snapshot() {
|
|
59
|
+
account();
|
|
60
|
+
return {
|
|
61
|
+
totalMs: changed - started,
|
|
62
|
+
stages: Object.fromEntries(stages),
|
|
63
|
+
hits: Object.fromEntries(hits),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -76,14 +76,15 @@ const FRONTMATTER = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
|
76
76
|
*
|
|
77
77
|
* @param {string} source - The note's file.
|
|
78
78
|
* @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
|
|
79
|
+
* @param {import("./content-session.mjs").ContentNoteSource} [parsed] - Already parsed authored note.
|
|
79
80
|
* @returns {string|undefined} The face's slug.
|
|
80
81
|
*/
|
|
81
|
-
function writtenInFace(source, installed) {
|
|
82
|
+
function writtenInFace(source, installed, parsed) {
|
|
82
83
|
const yaml = FRONTMATTER.exec(source)?.[1];
|
|
83
84
|
if (yaml === undefined) return undefined;
|
|
84
85
|
let fm;
|
|
85
86
|
try {
|
|
86
|
-
fm = YAML.parse(yaml);
|
|
87
|
+
fm = parsed ? parsed.frontmatter : YAML.parse(yaml);
|
|
87
88
|
} catch {
|
|
88
89
|
return undefined;
|
|
89
90
|
}
|
|
@@ -102,10 +103,12 @@ function writtenInFace(source, installed) {
|
|
|
102
103
|
*
|
|
103
104
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
104
105
|
* @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
|
|
106
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
105
107
|
* @returns {ReadonlySet<string>} The slugs of the faces used.
|
|
106
108
|
*/
|
|
107
|
-
export function facesUsed(config, installed) {
|
|
108
|
-
const
|
|
109
|
+
export function facesUsed(config, installed, session) {
|
|
110
|
+
const cacheKey = session ?? config;
|
|
111
|
+
const known = usedFaces.get(cacheKey);
|
|
109
112
|
if (known) return known;
|
|
110
113
|
const used = new Set();
|
|
111
114
|
const content =
|
|
@@ -119,9 +122,9 @@ export function facesUsed(config, installed) {
|
|
|
119
122
|
const full = path.join(dir, entry.name);
|
|
120
123
|
if (entry.isDirectory()) walk(full);
|
|
121
124
|
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
122
|
-
const source = fs.readFileSync(full, "utf8");
|
|
125
|
+
const source = session?.sources.get(full)?.raw ?? fs.readFileSync(full, "utf8");
|
|
123
126
|
if (/^\s*writtenIn:/m.test(source)) {
|
|
124
|
-
const face = writtenInFace(source, installed);
|
|
127
|
+
const face = writtenInFace(source, installed, session?.parsed.get(full));
|
|
125
128
|
if (face) used.add(face);
|
|
126
129
|
}
|
|
127
130
|
if (!/\bfont=/.test(source)) continue;
|
|
@@ -135,7 +138,7 @@ export function facesUsed(config, installed) {
|
|
|
135
138
|
}
|
|
136
139
|
};
|
|
137
140
|
if (content && fs.existsSync(content)) walk(content);
|
|
138
|
-
usedFaces.set(
|
|
141
|
+
usedFaces.set(cacheKey, used);
|
|
139
142
|
return used;
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -143,15 +146,16 @@ export function facesUsed(config, installed) {
|
|
|
143
146
|
* The installed package, when this consumer is to ship it.
|
|
144
147
|
*
|
|
145
148
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
149
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources.
|
|
146
150
|
* @returns {import("./script-fonts.mjs").ScriptFonts|undefined} The package, or
|
|
147
151
|
* `undefined` without a root, an install, a manifest, or a note that sets
|
|
148
152
|
* text in one of its faces.
|
|
149
153
|
*/
|
|
150
|
-
function delivered(config) {
|
|
154
|
+
function delivered(config, session) {
|
|
151
155
|
if (!config?.rootDir) return undefined;
|
|
152
156
|
const installed = resolveScriptFonts(config.rootDir);
|
|
153
157
|
if (!installed?.manifest) return undefined;
|
|
154
|
-
return facesUsed(config, installed).size ? installed : undefined;
|
|
158
|
+
return facesUsed(config, installed, session).size ? installed : undefined;
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
/**
|
|
@@ -159,12 +163,13 @@ function delivered(config) {
|
|
|
159
163
|
*
|
|
160
164
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
161
165
|
* @param {string[]} formats - The font formats the target uses: `woff2`, `ttf`.
|
|
166
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
162
167
|
* @returns {{root: string, version: string, files: string[]}|undefined} The
|
|
163
168
|
* files, sorted: `fonts.css`, `LICENSE` and every file in `fonts/` of a
|
|
164
169
|
* listed format. `undefined` when the consumer ships none.
|
|
165
170
|
*/
|
|
166
|
-
export function scriptFontFiles(config, formats) {
|
|
167
|
-
const installed = delivered(config);
|
|
171
|
+
export function scriptFontFiles(config, formats, session) {
|
|
172
|
+
const installed = delivered(config, session);
|
|
168
173
|
if (!installed) return undefined;
|
|
169
174
|
const fontsDir = path.join(installed.root, "fonts");
|
|
170
175
|
const fonts =
|
|
@@ -250,10 +255,11 @@ export function stagedStylesheet(css, staged) {
|
|
|
250
255
|
* @param {string} dest - The directory to fill; created when there is
|
|
251
256
|
* something to put in it.
|
|
252
257
|
* @param {string[]} formats - The font formats the target uses.
|
|
258
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
253
259
|
* @returns {number} How many files were written; 0 when the consumer ships none.
|
|
254
260
|
*/
|
|
255
|
-
export function copyScriptFonts(config, dest, formats) {
|
|
256
|
-
const found = scriptFontFiles(config, formats);
|
|
261
|
+
export function copyScriptFonts(config, dest, formats, session) {
|
|
262
|
+
const found = scriptFontFiles(config, formats, session);
|
|
257
263
|
if (!found) return 0;
|
|
258
264
|
const staged = new Set(found.files);
|
|
259
265
|
for (const file of found.files) {
|
|
@@ -274,11 +280,12 @@ export function copyScriptFonts(config, dest, formats) {
|
|
|
274
280
|
* stylesheet, the licence and every file the manifest lists.
|
|
275
281
|
*
|
|
276
282
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
283
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
277
284
|
* @returns {{file: string, severity: "error", message: string}[]} One finding
|
|
278
285
|
* per missing file, located at the manifest; empty when the consumer ships none.
|
|
279
286
|
*/
|
|
280
|
-
export function checkScriptFonts(config) {
|
|
281
|
-
const installed = delivered(config);
|
|
287
|
+
export function checkScriptFonts(config, session) {
|
|
288
|
+
const installed = delivered(config, session);
|
|
282
289
|
if (!installed) return [];
|
|
283
290
|
const manifest = path.join(installed.root, FONTS_MANIFEST);
|
|
284
291
|
const wanted = new Set([STYLESHEET, LICENSE]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "24.1.
|
|
3
|
+
"version": "24.1.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repository's `packageBuild.assetTransform`, loaded.
|
|
3
|
+
*
|
|
4
|
+
* One loader for every command that writes a picture, so the stage and the CDN
|
|
5
|
+
* hold the same bytes for the same file.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} config - The resolved package-build configuration.
|
|
8
|
+
* @returns {Promise<((sourcePath: string) => string|null|undefined)|undefined>}
|
|
9
|
+
* The transform, or `undefined` when the repository names none.
|
|
10
|
+
*/
|
|
11
|
+
export function loadAssetTransform(config: object): Promise<((sourcePath: string) => string | null | undefined) | undefined>;
|
|
12
|
+
/**
|
|
13
|
+
* Stage static files, provenance-filtered pictures, base styles and fonts.
|
|
14
|
+
* @param {object} [options]
|
|
15
|
+
* @param {ReturnType<typeof loadPackageBuildConfig>} [options.config]
|
|
16
|
+
* @param {import("../content-config.mjs").ContentBuildConfig} [options.packConfig]
|
|
17
|
+
* @param {import("./content-session.mjs").ContentSession} [options.session]
|
|
18
|
+
*/
|
|
19
|
+
export function stagePackageBuildAssets({ config, packConfig, session, }?: {
|
|
20
|
+
config?: Readonly<import("../config.mjs").PackageBuildConfig> | undefined;
|
|
21
|
+
packConfig?: import("../content-config.mjs").ContentBuildConfig | undefined;
|
|
22
|
+
session?: import("./content-session.mjs").ContentSession | undefined;
|
|
23
|
+
}): Promise<void>;
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @param {object} opts
|
|
5
5
|
* @param {object} opts.config - Resolved content configuration.
|
|
6
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Captured authored text for diagnostics.
|
|
6
7
|
* @param {string} opts.calendariaVersion - Version whose import format is targeted.
|
|
7
8
|
* @returns {{directory: string, calendars: number, files: number}}
|
|
8
9
|
*/
|
|
9
|
-
export function emitCalendarArtifacts({ config, calendariaVersion }?: {
|
|
10
|
+
export function emitCalendarArtifacts({ config, calendariaVersion, sources }?: {
|
|
10
11
|
config: object;
|
|
12
|
+
sources?: Map<string, {
|
|
13
|
+
raw: string;
|
|
14
|
+
}> | undefined;
|
|
11
15
|
calendariaVersion: string;
|
|
12
16
|
}): {
|
|
13
17
|
directory: string;
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* @param {string} [options.assetsBase] - Assets root, defaulting to configuration.
|
|
9
9
|
* @param {readonly string[]} [options.skipDirectories] - Resolved content scope.
|
|
10
10
|
* @param {ReturnType<typeof routerFor>} [options.router] - One router shared by this session's compile.
|
|
11
|
+
* @param {boolean} [options.trackChanges] - Snapshot physical input metadata for build phase guards.
|
|
12
|
+
* @param {ReturnType<typeof createContentTimings>} [options.timings] - Invocation stage accounting.
|
|
11
13
|
* @returns {Promise<ContentSession>} Captured sources and lazy prepared views; dispose after use.
|
|
12
14
|
*/
|
|
13
|
-
export function createContentSession({ config, contentBase, assetsBase, skipDirectories, router, }?: {
|
|
15
|
+
export function createContentSession({ config, contentBase, assetsBase, skipDirectories, router, timings, trackChanges, }?: {
|
|
14
16
|
config?: import("../content-config.mjs").ContentBuildConfig | undefined;
|
|
15
17
|
contentBase?: string | undefined;
|
|
16
18
|
assetsBase?: string | undefined;
|
|
@@ -21,6 +23,17 @@ export function createContentSession({ config, contentBase, assetsBase, skipDire
|
|
|
21
23
|
packsOfType: (docType: string) => string[];
|
|
22
24
|
defaultOf: (docType: string) => string | undefined;
|
|
23
25
|
} | undefined;
|
|
26
|
+
trackChanges?: boolean | undefined;
|
|
27
|
+
timings?: {
|
|
28
|
+
sync<T>(name: string, work: () => T): T;
|
|
29
|
+
measure<T>(name: string, work: () => Promise<T> | T): Promise<T>;
|
|
30
|
+
hit(name: string): void;
|
|
31
|
+
snapshot(): {
|
|
32
|
+
totalMs: number;
|
|
33
|
+
stages: any;
|
|
34
|
+
hits: any;
|
|
35
|
+
};
|
|
36
|
+
} | undefined;
|
|
24
37
|
}): Promise<ContentSession>;
|
|
25
38
|
/**
|
|
26
39
|
* @typedef {"index"|"lint"|"links"|"compile"} ContentTarget
|
|
@@ -47,6 +60,8 @@ export function createContentSession({ config, contentBase, assetsBase, skipDire
|
|
|
47
60
|
* @property {() => Promise<import("./content-index.mjs").PreparedContentIndex>} publication
|
|
48
61
|
* @property {() => ReturnType<import("./compile-corpus.mjs").buildCompileCorpus>} compileCorpus
|
|
49
62
|
* @property {(targets: readonly ContentTarget[]) => Promise<ContentSession>} prepare
|
|
63
|
+
* @property {ReturnType<typeof createContentTimings>} timings
|
|
64
|
+
* @property {() => void} assertUnchanged
|
|
50
65
|
* @property {() => void} dispose
|
|
51
66
|
*/
|
|
52
67
|
/**
|
|
@@ -96,6 +111,8 @@ export type ContentSession = {
|
|
|
96
111
|
publication: () => Promise<import("./content-index.mjs").PreparedContentIndex>;
|
|
97
112
|
compileCorpus: () => ReturnType<typeof import("./compile-corpus.mjs").buildCompileCorpus>;
|
|
98
113
|
prepare: (targets: readonly ContentTarget[]) => Promise<ContentSession>;
|
|
114
|
+
timings: ReturnType<typeof createContentTimings>;
|
|
115
|
+
assertUnchanged: () => void;
|
|
99
116
|
dispose: () => void;
|
|
100
117
|
};
|
|
101
118
|
import * as blocks from "./content-blocks.mjs";
|
|
@@ -106,3 +123,4 @@ import { captureContentSources } from "./content-sources.mjs";
|
|
|
106
123
|
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
107
124
|
import * as sql from "./sql-tables.mjs";
|
|
108
125
|
import { buildLinkIndex } from "./content-links.mjs";
|
|
126
|
+
import { createContentTimings } from "./content-timings.mjs";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Sequential, nested stage accounting for one content invocation. */
|
|
2
|
+
export function createContentTimings(): {
|
|
3
|
+
/** @template T @param {string} name @param {() => T} work @returns {T} */
|
|
4
|
+
sync<T>(name: string, work: () => T): T;
|
|
5
|
+
/** @template T @param {string} name @param {() => Promise<T>|T} work @returns {Promise<T>} */
|
|
6
|
+
measure<T>(name: string, work: () => Promise<T> | T): Promise<T>;
|
|
7
|
+
/** @param {string} name */
|
|
8
|
+
hit(name: string): void;
|
|
9
|
+
snapshot(): {
|
|
10
|
+
totalMs: number;
|
|
11
|
+
stages: any;
|
|
12
|
+
hits: any;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -7,19 +7,21 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
9
9
|
* @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
|
|
10
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
10
11
|
* @returns {ReadonlySet<string>} The slugs of the faces used.
|
|
11
12
|
*/
|
|
12
|
-
export function facesUsed(config: DeliveryConfig, installed: import("./script-fonts.mjs").ScriptFonts): ReadonlySet<string>;
|
|
13
|
+
export function facesUsed(config: DeliveryConfig, installed: import("./script-fonts.mjs").ScriptFonts, session?: import("./content-session.mjs").ContentSession): ReadonlySet<string>;
|
|
13
14
|
/**
|
|
14
15
|
* The files a target receives, as paths relative to the fonts package root.
|
|
15
16
|
*
|
|
16
17
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
17
18
|
* @param {string[]} formats - The font formats the target uses: `woff2`, `ttf`.
|
|
19
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
18
20
|
* @returns {{root: string, version: string, files: string[]}|undefined} The
|
|
19
21
|
* files, sorted: `fonts.css`, `LICENSE` and every file in `fonts/` of a
|
|
20
22
|
* listed format. `undefined` when the consumer ships none.
|
|
21
23
|
*/
|
|
22
|
-
export function scriptFontFiles(config: DeliveryConfig, formats: string[]): {
|
|
24
|
+
export function scriptFontFiles(config: DeliveryConfig, formats: string[], session?: import("./content-session.mjs").ContentSession): {
|
|
23
25
|
root: string;
|
|
24
26
|
version: string;
|
|
25
27
|
files: string[];
|
|
@@ -46,18 +48,20 @@ export function stagedStylesheet(css: string, staged: ReadonlySet<string>): stri
|
|
|
46
48
|
* @param {string} dest - The directory to fill; created when there is
|
|
47
49
|
* something to put in it.
|
|
48
50
|
* @param {string[]} formats - The font formats the target uses.
|
|
51
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
49
52
|
* @returns {number} How many files were written; 0 when the consumer ships none.
|
|
50
53
|
*/
|
|
51
|
-
export function copyScriptFonts(config: DeliveryConfig, dest: string, formats: string[]): number;
|
|
54
|
+
export function copyScriptFonts(config: DeliveryConfig, dest: string, formats: string[], session?: import("./content-session.mjs").ContentSession): number;
|
|
52
55
|
/**
|
|
53
56
|
* Whether the package ships what its manifest and stylesheet promise: the
|
|
54
57
|
* stylesheet, the licence and every file the manifest lists.
|
|
55
58
|
*
|
|
56
59
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
60
|
+
* @param {import("./content-session.mjs").ContentSession} [session] - Captured sources when staging within a content build.
|
|
57
61
|
* @returns {{file: string, severity: "error", message: string}[]} One finding
|
|
58
62
|
* per missing file, located at the manifest; empty when the consumer ships none.
|
|
59
63
|
*/
|
|
60
|
-
export function checkScriptFonts(config: DeliveryConfig): {
|
|
64
|
+
export function checkScriptFonts(config: DeliveryConfig, session?: import("./content-session.mjs").ContentSession): {
|
|
61
65
|
file: string;
|
|
62
66
|
severity: "error";
|
|
63
67
|
message: string;
|