@heroiclands/package-build 23.4.0 → 23.5.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 +34 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -28
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -7
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/reference/format-details.md +218 -50
- package/docs/reference/note-types.md +71 -71
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +618 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pathnames.mjs +21 -10
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +4 -3
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +311 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
**Affiliations**
|
|
8
|
+
|
|
9
|
+
- `package-build lint` requires every affiliation's ladder to have a level-0 rung for those it excludes, beside its level-1 members.
|
|
10
|
+
|
|
11
|
+
**Provenance**
|
|
12
|
+
|
|
13
|
+
- Every icon and image must be covered by a provenance record, and every record must state `ai: true` or `ai: false`; a package missing either now fails its build until it adds them.
|
|
14
|
+
|
|
15
|
+
**What a Foundry package ships**
|
|
16
|
+
|
|
17
|
+
- A package ships only the icons and images whose provenance says `ai: false` under a public-domain, CC0, CC-BY, CC-BY-SA or Noun Project Pro licence; everything else stays in the repository.
|
|
18
|
+
- Shipped pictures sit flat at the package root as `icons/<file>` and `images/<file>`, and compendium documents and the website point there; remove any `packageBuild.assets` row copying `assets/icons` or `assets/images`, which is now refused.
|
|
19
|
+
- `asset-sets/default.json` in the package lists the shipped pictures, and each release attaches `icons.zip` and `images.zip`.
|
|
20
|
+
- A picture the package leaves out shows the package's `missing` stand-in, and every picture reference in a compiled document records which asset it is.
|
|
21
|
+
- Items created without art get their default icon again, at its flat `icons/` location.
|
|
22
|
+
- A package can ship pictures under further licences by listing them in `packageBuild.shippingLicenses`; the default list still applies and `ai: true` pictures never ship.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- New `package-build cdn publish --set <file> --name <name>` uploads every icon and image a package has to its CDN folder, flat, and publishes the art the package leaves out as a named asset set a world can add.
|
|
27
|
+
- A second run uploads nothing, and `--dry-run` lists what would upload; the bucket is read from `CDN_ENDPOINT`, `CDN_BUCKET`, `CDN_ACCESS_KEY_ID` and `CDN_SECRET_ACCESS_KEY`.
|
|
28
|
+
- The end-to-end container can run an S3-compatible server beside Foundry: set `FOUNDRYVTT_CONTAINER_S3=1` and a suite can exercise S3 storage.
|
|
29
|
+
|
|
30
|
+
## 23.4.1
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- Beings may give their hair as light brown or as greying in any color, and their eyes as red, gold, yellow, white, silver or pink.
|
|
35
|
+
- Silver hair reads as a natural color as well as an aged one.
|
|
36
|
+
|
|
3
37
|
## 23.4.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -20,34 +20,38 @@ The generated project includes a homepage, website and book targets, npm scripts
|
|
|
20
20
|
|
|
21
21
|
The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md). This toolchain does not build its own site or book. A Hugo theme dependency would form a cycle because the theme uses package-build for releases.
|
|
22
22
|
|
|
23
|
-
| Topic | Guide
|
|
24
|
-
| ------------------------------------------------- |
|
|
25
|
-
| Documentation landing | [Home](docs/index.md)
|
|
26
|
-
| Project layout and scripts | [Project setup](docs/project-setup.md)
|
|
27
|
-
| Note model and output | [Content format](docs/content-format.md)
|
|
28
|
-
| Frontmatter and system blocks | [Frontmatter](docs/authoring/frontmatter.md)
|
|
29
|
-
| Addresses, links, anchors, SQL, and secret blocks | [Links and markup](docs/authoring/links-and-markup.md)
|
|
30
|
-
| Images, icons, audio, and provenance | [Assets](docs/authoring/assets.md)
|
|
31
|
-
| Dates, calendars, and eras | [Dates and calendars](docs/authoring/dates-and-calendars.md)
|
|
32
|
-
| Complete note fields and mappings | [Note types](docs/reference/note-types.md)
|
|
33
|
-
| Detailed format rules and examples | [Format details](docs/reference/format-details.md)
|
|
34
|
-
| Compendium routing and document builds | [Packs](docs/guides/packs.md)
|
|
35
|
-
| Website build and publication | [Site](docs/guides/site.md)
|
|
36
|
-
| What the theme draws on a page | [Theme pages](docs/reference/theme-pages.md)
|
|
37
|
-
| PDF book build | [Book](docs/guides/book.md)
|
|
38
|
-
| Icon generation and use | [Icons](docs/guides/icons.md)
|
|
39
|
-
| Reference pages and content indexes | [Reference pages](docs/guides/reference-pages.md)
|
|
40
|
-
| Localization and templates | [Localization](docs/guides/localization.md)
|
|
41
|
-
| Foundry integration testing | [End-to-end testing](docs/guides/e2e.md)
|
|
42
|
-
| Release and deployment | [Release and deploy](docs/guides/release-and-deploy.md)
|
|
43
|
-
| Every CLI command | [Commands](docs/commands.md)
|
|
44
|
-
| Every build setting | [Configuration](docs/configuration.md)
|
|
45
|
-
| Programmatic exports | [API](docs/api.md)
|
|
46
|
-
| Error locations and severities | [Diagnostics](docs/diagnostics.md)
|
|
47
|
-
| Architectural rationale | [Design](docs/design.md)
|
|
23
|
+
| Topic | Guide |
|
|
24
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
25
|
+
| Documentation landing | [Home](docs/index.md) |
|
|
26
|
+
| Project layout and scripts | [Project setup](docs/project-setup.md) |
|
|
27
|
+
| Note model and output | [Content format](docs/content-format.md) |
|
|
28
|
+
| Frontmatter and system blocks | [Frontmatter](docs/authoring/frontmatter.md) |
|
|
29
|
+
| Addresses, links, anchors, SQL, and secret blocks | [Links and markup](docs/authoring/links-and-markup.md) |
|
|
30
|
+
| Images, icons, audio, and provenance | [Assets](docs/authoring/assets.md) |
|
|
31
|
+
| Dates, calendars, and eras | [Dates and calendars](docs/authoring/dates-and-calendars.md) |
|
|
32
|
+
| Complete note fields and mappings | [Note types](docs/reference/note-types.md) |
|
|
33
|
+
| Detailed format rules and examples | [Format details](docs/reference/format-details.md) |
|
|
34
|
+
| Compendium routing and document builds | [Packs](docs/guides/packs.md) |
|
|
35
|
+
| Website build and publication | [Site](docs/guides/site.md) |
|
|
36
|
+
| What the theme draws on a page | [Theme pages](docs/reference/theme-pages.md) |
|
|
37
|
+
| PDF book build | [Book](docs/guides/book.md) |
|
|
38
|
+
| Icon generation and use | [Icons](docs/guides/icons.md) |
|
|
39
|
+
| Reference pages and content indexes | [Reference pages](docs/guides/reference-pages.md) |
|
|
40
|
+
| Localization and templates | [Localization](docs/guides/localization.md) |
|
|
41
|
+
| Foundry integration testing | [End-to-end testing](docs/guides/e2e.md), including an optional S3 server |
|
|
42
|
+
| Release and deployment | [Release and deploy](docs/guides/release-and-deploy.md) |
|
|
43
|
+
| Every CLI command | [Commands](docs/commands.md) |
|
|
44
|
+
| Every build setting | [Configuration](docs/configuration.md) |
|
|
45
|
+
| Programmatic exports | [API](docs/api.md) |
|
|
46
|
+
| Error locations and severities | [Diagnostics](docs/diagnostics.md) |
|
|
47
|
+
| Architectural rationale | [Design](docs/design.md) |
|
|
48
48
|
|
|
49
49
|
Place notes may declare a governing affiliation with `data.government` (`Address`, default target type `affiliation`), or explicit `null` for complete anarchy. Positive population with an omitted government produces an advisory. See [place governments](docs/authoring/frontmatter.md#place-governments).
|
|
50
50
|
|
|
51
|
+
Every icon and image is covered by a provenance record stating `attribution`, `license` and `ai`. A Foundry package ships only the pictures whose record says `ai: false` under a licence on the shipping list, staged flat at the package root as `icons/<basename>` and `images/<basename>` and listed in its asset set, `asset-sets/default.json`; a reference to a picture it leaves out draws the package's `missing` stand-in, and every compiled reference states its asset's address. See [assets](docs/authoring/assets.md#what-the-foundry-package-ships) and [what a package ships](docs/reference/format-details.md#what-a-package-ships).
|
|
52
|
+
|
|
53
|
+
`package-build cdn publish --set <file> --name <display name>` uploads every icon and image the package has, shipped or not, to `<site.assets>/<contentPackage>/` in that flat layout, and writes the named set listing the pictures the package leaves out. It reads the bucket from `CDN_ENDPOINT`, `CDN_BUCKET`, `CDN_ACCESS_KEY_ID` and `CDN_SECRET_ACCESS_KEY`, in the environment or `.env.local`. See [publish pictures to the CDN](docs/guides/release-and-deploy.md#publish-pictures-to-the-cdn).
|
|
54
|
+
|
|
51
55
|
Leading captions, generic fenced divs, inline spans, and poetry code fences are described in [links and markup](docs/authoring/links-and-markup.md).
|
|
52
56
|
|
|
53
57
|
The package is GPL-3.0-or-later for code and CC-BY-SA-4.0 for original content. See [LICENSE.md](LICENSE.md).
|
package/bin/package-build.mjs
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* npx package-build bundle check
|
|
62
62
|
* npx package-build release
|
|
63
63
|
* npx package-build deploy <stage>
|
|
64
|
+
* npx package-build cdn publish --set <file> --name <display name> [--dry-run]
|
|
64
65
|
* npx package-build container <stage> <action>
|
|
65
66
|
* npx package-build e2e <seed|run|open|fast|sweep>
|
|
66
67
|
*
|
|
@@ -87,13 +88,25 @@ import { hideBin } from "yargs/helpers";
|
|
|
87
88
|
|
|
88
89
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
89
90
|
import { checkForeignAssetBindings } from "../engine/asset-bindings.mjs";
|
|
90
|
-
import { emitDiagnostic, formatDiagnostic } from "../engine/diagnostics.mjs";
|
|
91
|
+
import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "../engine/diagnostics.mjs";
|
|
91
92
|
import { writeSiteRoot } from "../engine/site-root.mjs";
|
|
92
93
|
import { DEPLOY_ROOT } from "../engine/site-config.mjs";
|
|
93
94
|
import { compilesFoundryDocuments } from "../content-config.mjs";
|
|
94
95
|
import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
|
|
95
96
|
import { cleanBuildArtifacts, resetStage, stageAssets } from "../stage.mjs";
|
|
97
|
+
import { rowsStagingPictureRoots, stagePackageAssets } from "../engine/asset-shipping.mjs";
|
|
96
98
|
import { baseStyleStageEntry } from "../engine/base-styles.mjs";
|
|
99
|
+
import {
|
|
100
|
+
CDN_ENV,
|
|
101
|
+
cdnCredentials,
|
|
102
|
+
noteArtFindings,
|
|
103
|
+
noteArtReferences,
|
|
104
|
+
planCdnPublish,
|
|
105
|
+
publicStore,
|
|
106
|
+
publishToCdn,
|
|
107
|
+
s3Store,
|
|
108
|
+
setFileProblem,
|
|
109
|
+
} from "../engine/cdn-publish.mjs";
|
|
97
110
|
import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
|
|
98
111
|
import { emitCalendarArtifacts } from "../engine/calendar-artifacts.mjs";
|
|
99
112
|
import { dateFromCalendar, dateToCalendar } from "../engine/date-conversion.mjs";
|
|
@@ -421,6 +434,62 @@ function cleanCommand() {
|
|
|
421
434
|
};
|
|
422
435
|
}
|
|
423
436
|
|
|
437
|
+
/**
|
|
438
|
+
* A finding for each `packageBuild.assets` row that would stage a picture root
|
|
439
|
+
* itself, located at the row's `from` when the configuration is YAML.
|
|
440
|
+
*
|
|
441
|
+
* @param {readonly {from: string, to: string}[]} rows - The configured rows.
|
|
442
|
+
* @param {{rootDir: string, assetsBase: string}} opts - Where they resolve.
|
|
443
|
+
* @returns {object[]} The findings.
|
|
444
|
+
*/
|
|
445
|
+
function pictureRootRowFindings(rows, { rootDir, assetsBase }) {
|
|
446
|
+
const offending = rowsStagingPictureRoots(rows, { rootDir, assetsBase });
|
|
447
|
+
if (!offending.length) return [];
|
|
448
|
+
const file = packConfigPath();
|
|
449
|
+
const text =
|
|
450
|
+
file.endsWith(".yaml") || file.endsWith(".yml") ? fs.readFileSync(file, "utf8") : "";
|
|
451
|
+
return offending.map(({ index, from, root }) => ({
|
|
452
|
+
file,
|
|
453
|
+
...(text ? positionOfYamlPath(text, ["packageBuild", "assets", index, "from"]) : {}),
|
|
454
|
+
severity: "error",
|
|
455
|
+
message:
|
|
456
|
+
`\`packageBuild.assets[${index}]\` copies \`${from}\`, which reaches ${root}/ — ` +
|
|
457
|
+
"the stage copies the icon and image roots itself, flat and filtered by " +
|
|
458
|
+
"each file's provenance, so a row copying them would ship every file " +
|
|
459
|
+
"unfiltered. Remove the row",
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The repository's `packageBuild.assetTransform`, loaded.
|
|
465
|
+
*
|
|
466
|
+
* One loader for every command that writes a picture, so the stage and the CDN
|
|
467
|
+
* hold the same bytes for the same file.
|
|
468
|
+
*
|
|
469
|
+
* @param {object} config - The resolved package-build configuration.
|
|
470
|
+
* @returns {Promise<((sourcePath: string) => string|null|undefined)|undefined>}
|
|
471
|
+
* The transform, or `undefined` when the repository names none.
|
|
472
|
+
*/
|
|
473
|
+
async function loadAssetTransform(config) {
|
|
474
|
+
if (!config.assetTransform) return undefined;
|
|
475
|
+
const module = await import(`file://${config.assetTransform}`).catch((err) =>
|
|
476
|
+
die(
|
|
477
|
+
`cannot load \`packageBuild.assetTransform\` ` +
|
|
478
|
+
`(${config.assetTransform}): ${err.message}`,
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
const transform = module.transform;
|
|
482
|
+
if (typeof transform !== "function") {
|
|
483
|
+
die(
|
|
484
|
+
`\`packageBuild.assetTransform\` ` +
|
|
485
|
+
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
486
|
+
`function. It must export ` +
|
|
487
|
+
`\`transform(sourcePath) -> string | null\`.`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return transform;
|
|
491
|
+
}
|
|
492
|
+
|
|
424
493
|
/**
|
|
425
494
|
* `assets` — stage the repository's static files into the package root.
|
|
426
495
|
*
|
|
@@ -440,37 +509,22 @@ function assetsCommand() {
|
|
|
440
509
|
builder: (y) => y,
|
|
441
510
|
handler: handler(async () => {
|
|
442
511
|
const config = loadPackageBuildConfig();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
512
|
+
const packConfig = loadPackConfig();
|
|
513
|
+
const assetsBase = packConfig.paths.assets;
|
|
514
|
+
const findings = [
|
|
515
|
+
...pictureRootRowFindings(config.assets, {
|
|
516
|
+
rootDir: config.rootDir,
|
|
517
|
+
assetsBase,
|
|
518
|
+
}),
|
|
519
|
+
...checkForeignAssetBindings(packConfig, { assets: config.assets }),
|
|
520
|
+
];
|
|
521
|
+
if (findings.length) {
|
|
522
|
+
const error = new Error(findings.map(formatDiagnostic).join("\n"));
|
|
452
523
|
error.located = true;
|
|
453
524
|
die(error);
|
|
454
525
|
}
|
|
455
526
|
|
|
456
|
-
|
|
457
|
-
if (config.assetTransform) {
|
|
458
|
-
const module = await import(`file://${config.assetTransform}`).catch((err) =>
|
|
459
|
-
die(
|
|
460
|
-
`cannot load \`packageBuild.assetTransform\` ` +
|
|
461
|
-
`(${config.assetTransform}): ${err.message}`,
|
|
462
|
-
),
|
|
463
|
-
);
|
|
464
|
-
transform = module.transform;
|
|
465
|
-
if (typeof transform !== "function") {
|
|
466
|
-
die(
|
|
467
|
-
`\`packageBuild.assetTransform\` ` +
|
|
468
|
-
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
469
|
-
`function. It must export ` +
|
|
470
|
-
`\`transform(sourcePath) -> string | null\`.`,
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
527
|
+
const transform = await loadAssetTransform(config);
|
|
474
528
|
|
|
475
529
|
// `to:` is relative to the staged package root, so a
|
|
476
530
|
// repository's table reads `lang`, not `build/stage/lang`.
|
|
@@ -483,6 +537,35 @@ function assetsCommand() {
|
|
|
483
537
|
transform,
|
|
484
538
|
});
|
|
485
539
|
|
|
540
|
+
// The picture roots, staged by the toolchain rather than by a row:
|
|
541
|
+
// flat, filtered by each file's provenance, and listed in the
|
|
542
|
+
// package's asset set.
|
|
543
|
+
const pictures = stagePackageAssets({
|
|
544
|
+
assetsBase,
|
|
545
|
+
contentPackage: packConfig.contentPackage,
|
|
546
|
+
stageDir: path.resolve(config.rootDir, config.stageDir),
|
|
547
|
+
// The manifest's title names the set; a package with none is
|
|
548
|
+
// named by its content package.
|
|
549
|
+
title:
|
|
550
|
+
typeof config.manifest?.title === "string" && config.manifest.title.trim() ?
|
|
551
|
+
config.manifest.title
|
|
552
|
+
: packConfig.contentPackage,
|
|
553
|
+
transform,
|
|
554
|
+
shippingLicenses: config.shippingLicenses,
|
|
555
|
+
});
|
|
556
|
+
if (pictures.findings.length) {
|
|
557
|
+
const error = new Error(pictures.findings.map(formatDiagnostic).join("\n"));
|
|
558
|
+
error.located = true;
|
|
559
|
+
die(error);
|
|
560
|
+
}
|
|
561
|
+
files += pictures.staged.length;
|
|
562
|
+
if (pictures.staged.length || pictures.excluded.length) {
|
|
563
|
+
console.log(
|
|
564
|
+
`Pictures: ${pictures.staged.length} staged, ` +
|
|
565
|
+
`${pictures.excluded.length} left out by provenance.`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
486
569
|
// Staged in its own pass, without the repository's transform: the
|
|
487
570
|
// sheet is this toolchain's file rather than one of the
|
|
488
571
|
// repository's assets, and a `transform` written to retheme a
|
|
@@ -1464,6 +1547,9 @@ function releaseCommand() {
|
|
|
1464
1547
|
}),
|
|
1465
1548
|
);
|
|
1466
1549
|
}
|
|
1550
|
+
for (const archive of result.pictures ?? []) {
|
|
1551
|
+
console.log(`🖼 Pictures: ${path.relative(config.rootDir, archive)}`);
|
|
1552
|
+
}
|
|
1467
1553
|
for (const pdf of result.pdfs ?? []) {
|
|
1468
1554
|
console.log(`📕 Book: ${path.relative(config.rootDir, pdf)}`);
|
|
1469
1555
|
}
|
|
@@ -1534,6 +1620,150 @@ function deployCommand() {
|
|
|
1534
1620
|
};
|
|
1535
1621
|
}
|
|
1536
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* `cdn publish` — upload every picture the package has to the CDN, and the art
|
|
1625
|
+
* the package leaves out as a named asset set.
|
|
1626
|
+
*
|
|
1627
|
+
* The one writer to `<site.assets>/<package>/`. It is run by hand or by a
|
|
1628
|
+
* dispatched workflow, never by a build or a release. The store is named by
|
|
1629
|
+
* the `CDN_*` variables in `CDN_ENV`, read from the environment or
|
|
1630
|
+
* `.env.local`; a dry run with none of them set compares against the public
|
|
1631
|
+
* CDN instead.
|
|
1632
|
+
*
|
|
1633
|
+
* @returns {object} The yargs command module.
|
|
1634
|
+
*/
|
|
1635
|
+
function cdnPublishCommand() {
|
|
1636
|
+
return {
|
|
1637
|
+
command: "publish",
|
|
1638
|
+
describe: "Upload every picture to the CDN and write a named asset set",
|
|
1639
|
+
builder: (y) =>
|
|
1640
|
+
y
|
|
1641
|
+
.option("set", {
|
|
1642
|
+
type: "string",
|
|
1643
|
+
demandOption: true,
|
|
1644
|
+
describe: "The set file's name under the package's CDN root, e.g. ai-art.json",
|
|
1645
|
+
})
|
|
1646
|
+
.option("name", {
|
|
1647
|
+
type: "string",
|
|
1648
|
+
demandOption: true,
|
|
1649
|
+
describe: "The set's display name",
|
|
1650
|
+
})
|
|
1651
|
+
.option("dry-run", {
|
|
1652
|
+
type: "boolean",
|
|
1653
|
+
default: false,
|
|
1654
|
+
describe: "List what would upload and what is skipped; write nothing",
|
|
1655
|
+
}),
|
|
1656
|
+
handler: handler(async (args) => {
|
|
1657
|
+
const config = loadPackageBuildConfig();
|
|
1658
|
+
const packConfig = loadPackConfig();
|
|
1659
|
+
await loadEnvironment(config);
|
|
1660
|
+
|
|
1661
|
+
const setProblem = setFileProblem(args.set);
|
|
1662
|
+
if (setProblem) die(setProblem);
|
|
1663
|
+
const name = String(args.name ?? "").trim();
|
|
1664
|
+
if (!name) die("`--name` is blank; a set needs a display name");
|
|
1665
|
+
const host = String(packConfig.site?.assets ?? "").trim();
|
|
1666
|
+
if (!host) {
|
|
1667
|
+
die(
|
|
1668
|
+
"`site.assets` is not declared, and it is the CDN root every picture " +
|
|
1669
|
+
"is published under",
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
const credentials = cdnCredentials(process.env);
|
|
1673
|
+
if (!credentials && !args.dryRun) {
|
|
1674
|
+
die(
|
|
1675
|
+
`no CDN store is configured: set ${Object.values(CDN_ENV).join(", ")} ` +
|
|
1676
|
+
"in the environment or `.env.local`",
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const plan = planCdnPublish({
|
|
1681
|
+
assetsBase: packConfig.paths.assets,
|
|
1682
|
+
contentPackage: packConfig.contentPackage,
|
|
1683
|
+
host,
|
|
1684
|
+
setFile: args.set,
|
|
1685
|
+
setName: name,
|
|
1686
|
+
transform: await loadAssetTransform(config),
|
|
1687
|
+
shippingLicenses: config.shippingLicenses,
|
|
1688
|
+
});
|
|
1689
|
+
if (plan.findings.length) {
|
|
1690
|
+
const located = plan.findings.map((finding) => ({
|
|
1691
|
+
...finding,
|
|
1692
|
+
file:
|
|
1693
|
+
path.isAbsolute(finding.file) ?
|
|
1694
|
+
path.relative(process.cwd(), finding.file)
|
|
1695
|
+
: finding.file,
|
|
1696
|
+
}));
|
|
1697
|
+
reportFindings(located, {});
|
|
1698
|
+
process.exitCode = 1;
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const excluded = new Set(
|
|
1702
|
+
plan.uploads.filter((upload) => !upload.shipped).map((upload) => upload.record),
|
|
1703
|
+
);
|
|
1704
|
+
const references = noteArtReferences({
|
|
1705
|
+
contentBase: packConfig.paths.content,
|
|
1706
|
+
skipDirectories: packConfig.skipDirectories,
|
|
1707
|
+
contentPackage: packConfig.contentPackage,
|
|
1708
|
+
records: plan.records,
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
const store = credentials ? s3Store(credentials) : publicStore(host);
|
|
1712
|
+
console.log(
|
|
1713
|
+
`${args.dryRun ? "Dry run: comparing" : "Publishing"} ${plan.uploads.length} ` +
|
|
1714
|
+
`pictures against ${store.describe}.`,
|
|
1715
|
+
);
|
|
1716
|
+
const result = await publishToCdn(plan, {
|
|
1717
|
+
store,
|
|
1718
|
+
dryRun: args.dryRun,
|
|
1719
|
+
log: (line) => console.log(` ${line}`),
|
|
1720
|
+
});
|
|
1721
|
+
const failures = result.failed.map(({ upload, message }) => ({
|
|
1722
|
+
file: path.relative(
|
|
1723
|
+
process.cwd(),
|
|
1724
|
+
path.join(packConfig.paths.assets, ...upload.record.asset.path.split("/")),
|
|
1725
|
+
),
|
|
1726
|
+
severity: "error",
|
|
1727
|
+
message: `${upload.key} did not upload: ${message}`,
|
|
1728
|
+
}));
|
|
1729
|
+
const failedCount = reportFindings(
|
|
1730
|
+
[
|
|
1731
|
+
...failures,
|
|
1732
|
+
...noteArtFindings(references, {
|
|
1733
|
+
contentPackage: packConfig.contentPackage,
|
|
1734
|
+
published: result.published,
|
|
1735
|
+
excluded,
|
|
1736
|
+
}),
|
|
1737
|
+
],
|
|
1738
|
+
{},
|
|
1739
|
+
);
|
|
1740
|
+
if (failedCount > 0) process.exitCode = 1;
|
|
1741
|
+
const set = /** @type {NonNullable<typeof plan.set>} */ (plan.set);
|
|
1742
|
+
const setState =
|
|
1743
|
+
result.set === "skipped" ? "unchanged"
|
|
1744
|
+
: result.set === "withheld" ? "withheld"
|
|
1745
|
+
: args.dryRun ? "would change"
|
|
1746
|
+
: "written";
|
|
1747
|
+
console.log(
|
|
1748
|
+
`${args.dryRun ? "Would upload" : "Uploaded"} ${result.uploaded.length}, ` +
|
|
1749
|
+
`skipped ${result.skipped.length}, failed ${result.failed.length}; ` +
|
|
1750
|
+
`${plan.cdnRoot}${args.set} lists ` +
|
|
1751
|
+
`${set.set.assets.length} left-out pictures (${setState}).`,
|
|
1752
|
+
);
|
|
1753
|
+
}),
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** Publish the package's pictures to the CDN. */
|
|
1758
|
+
function cdnCommand() {
|
|
1759
|
+
return {
|
|
1760
|
+
command: "cdn <action>",
|
|
1761
|
+
describe: "Publish the package's pictures to the CDN",
|
|
1762
|
+
builder: (yargs) => yargs.command(cdnPublishCommand()).demandCommand(1).strict(),
|
|
1763
|
+
handler: () => {},
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1537
1767
|
/**
|
|
1538
1768
|
* `container <stage> <action>` — run a stage's Foundry in a container.
|
|
1539
1769
|
*
|
|
@@ -1749,6 +1979,7 @@ registerContentCommands(
|
|
|
1749
1979
|
.command(bundleCommand())
|
|
1750
1980
|
.command(releaseCommand())
|
|
1751
1981
|
.command(deployCommand())
|
|
1982
|
+
.command(cdnCommand())
|
|
1752
1983
|
.command(containerCommand())
|
|
1753
1984
|
.command(e2eCommand()),
|
|
1754
1985
|
)
|
package/ci/prettier.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs this repository's Prettier over the paths given, or over the whole tree
|
|
16
|
+
* when none are.
|
|
17
|
+
*
|
|
18
|
+
* `npm run format -- a.mjs b.md` appends `a.mjs b.md` to the script's command
|
|
19
|
+
* line, so a script of `prettier --write .` formats everything regardless.
|
|
20
|
+
* This wrapper takes the mode first and treats whatever follows as the paths.
|
|
21
|
+
* A path that no longer exists, such as a file a branch deleted, is skipped
|
|
22
|
+
* rather than failing the run. Prettier still resolves the repository's own
|
|
23
|
+
* configuration and ignore file.
|
|
24
|
+
*
|
|
25
|
+
* ```text
|
|
26
|
+
* node ci/prettier.mjs --write [paths...]
|
|
27
|
+
* node ci/prettier.mjs --check [paths...]
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import fs from "node:fs";
|
|
34
|
+
import { createRequire } from "node:module";
|
|
35
|
+
import { spawnSync } from "node:child_process";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The Prettier arguments for one invocation.
|
|
40
|
+
*
|
|
41
|
+
* @param {readonly string[]} args - The mode, `--write` or `--check`, then the
|
|
42
|
+
* paths.
|
|
43
|
+
* @param {(path: string) => boolean} [exists] - Whether a path exists.
|
|
44
|
+
* @returns {string[]|null} The arguments, `.` when no path was given, or `null`
|
|
45
|
+
* when paths were given and none exists, so there is nothing to run.
|
|
46
|
+
* @throws {Error} When the mode is missing or unknown.
|
|
47
|
+
*/
|
|
48
|
+
export function prettierArgs(args, exists = fs.existsSync) {
|
|
49
|
+
const [mode, ...paths] = args;
|
|
50
|
+
if (mode !== "--write" && mode !== "--check") {
|
|
51
|
+
throw new Error("the first argument must be --write or --check");
|
|
52
|
+
}
|
|
53
|
+
if (paths.length === 0) return [mode, "."];
|
|
54
|
+
const present = paths.filter((entry) => exists(entry));
|
|
55
|
+
return present.length ? [mode, ...present] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
59
|
+
let args;
|
|
60
|
+
try {
|
|
61
|
+
args = prettierArgs(process.argv.slice(2));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(error.message);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
if (args === null) {
|
|
67
|
+
console.log("None of the given paths exist; nothing to format.");
|
|
68
|
+
} else {
|
|
69
|
+
const bin = createRequire(import.meta.url).resolve("prettier/bin/prettier.cjs");
|
|
70
|
+
const result = spawnSync(process.execPath, [bin, ...args], { stdio: "inherit" });
|
|
71
|
+
process.exit(result.status ?? 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/config.mjs
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
59
|
import path from "node:path";
|
|
60
|
+
import { shippingLicensesProblem } from "./engine/asset-types.mjs";
|
|
60
61
|
import { fileURLToPath } from "node:url";
|
|
61
62
|
import { loadPackConfig, locateConfigError, packConfigPath } from "./engine/pack-config.mjs";
|
|
62
63
|
|
|
@@ -65,6 +66,7 @@ const SECTION_KEYS = [
|
|
|
65
66
|
"stageDir",
|
|
66
67
|
"assets",
|
|
67
68
|
"assetTransform",
|
|
69
|
+
"shippingLicenses",
|
|
68
70
|
"baseStyles",
|
|
69
71
|
"manifest",
|
|
70
72
|
"manifestFlags",
|
|
@@ -556,6 +558,8 @@ function normalizeExceptions(value, field, where) {
|
|
|
556
558
|
* @property {string} stageDir The staged package root, relative to
|
|
557
559
|
* `rootDir`. Every asset `to:` lands under it.
|
|
558
560
|
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
561
|
+
* @property {readonly string[]} shippingLicenses Licences this package adds to the
|
|
562
|
+
* default shipping list.
|
|
559
563
|
* @property {boolean} baseStyles Whether the package takes the shared base
|
|
560
564
|
* stylesheet, staged into `stageDir` and
|
|
561
565
|
* named first in the manifest's `styles`.
|
|
@@ -785,6 +789,9 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
785
789
|
}
|
|
786
790
|
const assets = (section.assets ?? []).map(normalizeAsset);
|
|
787
791
|
|
|
792
|
+
const shippingProblem = shippingLicensesProblem(section.shippingLicenses);
|
|
793
|
+
if (shippingProblem) fail("packageBuild.shippingLicenses", shippingProblem);
|
|
794
|
+
|
|
788
795
|
const clean = section.clean ?? {};
|
|
789
796
|
if (!isMapping(clean)) fail("packageBuild.clean", "must be a mapping");
|
|
790
797
|
rejectUnknownKeys(
|
|
@@ -899,6 +906,9 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
899
906
|
ARTIFACT_OF_KIND[/** @type {"systems"|"modules"} */ (shared.packageKind)]
|
|
900
907
|
: requireNonEmptyString(releaseInput.artifact, "packageBuild.release.artifact"),
|
|
901
908
|
assets: Object.freeze(assets),
|
|
909
|
+
shippingLicenses: Object.freeze([
|
|
910
|
+
.../** @type {string[]} */ (section.shippingLicenses ?? []),
|
|
911
|
+
]),
|
|
902
912
|
// On by default, because the rules it carries are what compiled content
|
|
903
913
|
// needs to be legible and a package that declined them by omission
|
|
904
914
|
// would ship the content unstyled without anything saying so. A package
|