@heroiclands/package-build 22.0.1 → 22.0.3
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 +38 -0
- package/bin/package-build.mjs +33 -0
- package/config.mjs +36 -2
- package/content-config.mjs +11 -4
- package/docs/api.md +10 -7
- package/docs/commands.md +54 -0
- package/docs/configuration.md +3 -1
- package/docs/content-format.md +11 -0
- package/engine/asset-index.mjs +44 -0
- package/engine/content-index.mjs +25 -6
- package/engine/site-root.mjs +165 -0
- package/engine/svg-theme.mjs +140 -0
- package/package.json +1 -1
- package/types/config.d.mts +56 -33
- package/types/engine/asset-index.d.mts +20 -0
- package/types/engine/site-root.d.mts +87 -0
- package/types/engine/svg-theme.d.mts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fd58dc0: **A package may ship assets and compile nothing.** `packs: []` is now a package
|
|
8
|
+
saying it has no documents, rather than one that forgot to say which, and a
|
|
9
|
+
package with no packs needs no content tree — its index is its asset records.
|
|
10
|
+
An alternative-art module is the case: the same addresses another package
|
|
11
|
+
publishes, resolving to different files when it is installed.
|
|
12
|
+
|
|
13
|
+
A package that _does_ declare packs is unchanged: a missing content tree is
|
|
14
|
+
still a misconfigured path and still fails, and an index that would state a
|
|
15
|
+
package has no content at all is still refused.
|
|
16
|
+
- cfdf061: **Two pieces of per-repository tooling move here.** Every site-publishing
|
|
17
|
+
package held its own copy of both, and a copy per consumer is a copy free to
|
|
18
|
+
drift — which is what happened, invisibly, because nobody reads all of them at
|
|
19
|
+
once.
|
|
20
|
+
|
|
21
|
+
- `packageBuild.assetTransform: svg-theme` names a shipped transform, so an
|
|
22
|
+
icon follows the reader's colour scheme without a module in the repository.
|
|
23
|
+
The value still takes a path, so a consumer with a transform of its own is
|
|
24
|
+
unaffected.
|
|
25
|
+
- `package-build site-root` writes a deployment's `_headers` and `_redirects`:
|
|
26
|
+
indexing suppressed on every host-assigned address, and both spellings of the
|
|
27
|
+
prefix root redirected to the landing with a lifetime on the 301. The package
|
|
28
|
+
name comes from `contentPackage`, which was the only thing the copies actually
|
|
29
|
+
varied.
|
|
30
|
+
|
|
31
|
+
## 22.0.2
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 83fa58e: **A record states `attribution` and `license`, or it is a finding.** The nearest
|
|
36
|
+
provenance record is the whole answer and is never merged with the one above it,
|
|
37
|
+
so a key left out is not inherited — it is absent, and a record stating neither
|
|
38
|
+
would leave every file beneath it with no rights holder and no terms. `source`,
|
|
39
|
+
`ai` and `notes` stay optional, because blank is a truthful answer for each.
|
|
40
|
+
|
|
3
41
|
## 22.0.1
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/bin/package-build.mjs
CHANGED
|
@@ -78,6 +78,7 @@ import yargs from "yargs";
|
|
|
78
78
|
import { hideBin } from "yargs/helpers";
|
|
79
79
|
|
|
80
80
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
81
|
+
import { writeSiteRoot } from "../engine/site-root.mjs";
|
|
81
82
|
import { compilesFoundryDocuments } from "../content-config.mjs";
|
|
82
83
|
import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
|
|
83
84
|
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
@@ -466,6 +467,37 @@ function manifestCommand() {
|
|
|
466
467
|
};
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/**
|
|
471
|
+
* `package-build site-root` — the deployment's root files.
|
|
472
|
+
*
|
|
473
|
+
* Hugo owns everything under the `/<package>/` prefix; this owns what sits
|
|
474
|
+
* beside it, which is the pair Cloudflare Pages reads from the uploaded
|
|
475
|
+
* directory and nowhere else.
|
|
476
|
+
*
|
|
477
|
+
* @returns {object} The yargs command.
|
|
478
|
+
*/
|
|
479
|
+
function siteRootCommand() {
|
|
480
|
+
return {
|
|
481
|
+
command: "site-root",
|
|
482
|
+
describe: "Write the deployment's _headers and _redirects",
|
|
483
|
+
builder: (y) =>
|
|
484
|
+
y.option("out", {
|
|
485
|
+
type: "string",
|
|
486
|
+
describe: "The directory that is deployed (default: build/site)",
|
|
487
|
+
}),
|
|
488
|
+
handler: handler(async (argv) => {
|
|
489
|
+
const config = loadPackageBuildConfig();
|
|
490
|
+
const shared = loadPackConfig();
|
|
491
|
+
const out = path.resolve(config.rootDir, argv.out ?? "build/site");
|
|
492
|
+
|
|
493
|
+
const { files } = writeSiteRoot({ pkg: shared.contentPackage, out });
|
|
494
|
+
for (const file of files) {
|
|
495
|
+
console.log(`✅ Wrote ${path.relative(config.rootDir, file)}.`);
|
|
496
|
+
}
|
|
497
|
+
}),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
469
501
|
/**
|
|
470
502
|
* Every file matching a glob, as `{ path, text }` with the path relative to the
|
|
471
503
|
* repository root — the form both the rules and the findings want.
|
|
@@ -1214,6 +1246,7 @@ yargs(hideBin(process.argv))
|
|
|
1214
1246
|
.command(cleanCommand())
|
|
1215
1247
|
.command(assetsCommand())
|
|
1216
1248
|
.command(manifestCommand())
|
|
1249
|
+
.command(siteRootCommand())
|
|
1217
1250
|
.command(schemaCommand())
|
|
1218
1251
|
.command(langCommand())
|
|
1219
1252
|
.command(labelsCommand())
|
package/config.mjs
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
59
|
import path from "node:path";
|
|
60
|
+
import { fileURLToPath } from "node:url";
|
|
60
61
|
import { loadPackConfig, locateConfigError, packConfigPath } from "./engine/pack-config.mjs";
|
|
61
62
|
|
|
62
63
|
/** Keys the reserved section may declare. */
|
|
@@ -565,6 +566,39 @@ function normalizeExceptions(value, field, where) {
|
|
|
565
566
|
* collections, as collection → source directory.
|
|
566
567
|
*/
|
|
567
568
|
|
|
569
|
+
/**
|
|
570
|
+
* The asset transforms this package ships, by the name a consumer writes.
|
|
571
|
+
*
|
|
572
|
+
* `packageBuild.assetTransform` takes either one of these names or a path to a
|
|
573
|
+
* module of the consumer's own. A name is the answer where every package wants
|
|
574
|
+
* the same behaviour — theming an icon to the reader's colour scheme is not a
|
|
575
|
+
* per-package decision, and a copy in each consumer is a copy that drifts.
|
|
576
|
+
*
|
|
577
|
+
* @type {Readonly<Record<string, string>>}
|
|
578
|
+
*/
|
|
579
|
+
export const BUILT_IN_ASSET_TRANSFORMS = Object.freeze({
|
|
580
|
+
"svg-theme": "./engine/svg-theme.mjs",
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Where a declared `assetTransform` is loaded from.
|
|
585
|
+
*
|
|
586
|
+
* A built-in name resolves to the module this package ships. Anything else is a
|
|
587
|
+
* path, resolved against the repository root the way it always was — so a
|
|
588
|
+
* consumer with a transform of its own is unaffected.
|
|
589
|
+
*
|
|
590
|
+
* @param {string} declared - The authored value.
|
|
591
|
+
* @param {string} rootDir - The repository root.
|
|
592
|
+
* @returns {string} An absolute path to import.
|
|
593
|
+
*/
|
|
594
|
+
export function resolveAssetTransform(declared, rootDir) {
|
|
595
|
+
const builtIn = BUILT_IN_ASSET_TRANSFORMS[declared];
|
|
596
|
+
if (builtIn) {
|
|
597
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), builtIn);
|
|
598
|
+
}
|
|
599
|
+
return path.resolve(rootDir, declared);
|
|
600
|
+
}
|
|
601
|
+
|
|
568
602
|
/**
|
|
569
603
|
* Resolve a package-build configuration from an already-loaded shared one.
|
|
570
604
|
*
|
|
@@ -703,9 +737,9 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
703
737
|
assetTransform:
|
|
704
738
|
section.assetTransform === undefined ?
|
|
705
739
|
null
|
|
706
|
-
:
|
|
707
|
-
shared.rootDir,
|
|
740
|
+
: resolveAssetTransform(
|
|
708
741
|
requireNonEmptyString(section.assetTransform, "packageBuild.assetTransform"),
|
|
742
|
+
shared.rootDir,
|
|
709
743
|
),
|
|
710
744
|
manifest: normalizeManifest(section.manifest),
|
|
711
745
|
manifestFlags:
|
package/content-config.mjs
CHANGED
|
@@ -2328,10 +2328,17 @@ export function defineConfig(config) {
|
|
|
2328
2328
|
}
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2331
|
+
// A Foundry package need not compile anything. A module may ship assets and
|
|
2332
|
+
// nothing else — alternative art for another package is the case, and it is
|
|
2333
|
+
// an ordinary module that installs, is enabled, and supplies files. So
|
|
2334
|
+
// `packs: []` is a package saying it compiles no documents, not a package
|
|
2335
|
+
// that forgot to say which.
|
|
2336
|
+
//
|
|
2337
|
+
// What that leaves uncovered is a tree of notes with no pack to compile them
|
|
2338
|
+
// into, which would be silently ignored. `packs` cannot see the tree, so the
|
|
2339
|
+
// walk reports it: a declared pack that compiles nothing from a non-empty
|
|
2340
|
+
// tree already fails, and so does a note whose `pack:` names none.
|
|
2341
|
+
if (!documentation && !Array.isArray(input.packs)) fail("packs", "must be an array");
|
|
2335
2342
|
const declaredPacks = Array.isArray(input.packs) ? input.packs : [];
|
|
2336
2343
|
const packs = declaredPacks.map((pack, index) => normalizePack(pack, `packs[${index}]`));
|
|
2337
2344
|
|
package/docs/api.md
CHANGED
|
@@ -567,13 +567,14 @@ The asset types — `icon`, `image` and `audio` — and the three roots they are
|
|
|
567
567
|
|
|
568
568
|
The asset record: one line of the content index per addressable file. The record is emitted by the package holding the bytes, so its `path` is that package's path and each consumer joins its own root onto it — Foundry its data directory, the website its CDN prefix, the book its asset base.
|
|
569
569
|
|
|
570
|
-
| Export | Signature | Returns | Use it when
|
|
571
|
-
| --------------------------- | --------------------------------------------------------------- | ----------------------------- |
|
|
572
|
-
| `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from
|
|
573
|
-
| `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields
|
|
574
|
-
| `
|
|
575
|
-
| `
|
|
576
|
-
| `
|
|
570
|
+
| Export | Signature | Returns | Use it when |
|
|
571
|
+
| --------------------------- | --------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
572
|
+
| `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from |
|
|
573
|
+
| `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields |
|
|
574
|
+
| `REQUIRED_PROVENANCE_KEYS` | `const REQUIRED_PROVENANCE_KEYS` | `ReadonlySet<string>` | the keys a provenance file must state — a record resolves wholesale, so one omitting them leaves every file it covers with no rights holder or no terms |
|
|
575
|
+
| `PROVENANCE_FILE` | `const PROVENANCE_FILE` | `string` — `provenance.yaml` | naming the file a directory records provenance for its subtree in |
|
|
576
|
+
| `PROVENANCE_SIDECAR_SUFFIX` | `const PROVENANCE_SIDECAR_SUFFIX` | `string` — `.yaml` | naming a per-file record, which replaces an inherited one wholesale |
|
|
577
|
+
| `collectAssetRecords` | `collectAssetRecords(assetsBase, { contentPackage, problems })` | `Array<Record<string, any>>` | reading a package's asset roots into index records, without walking its notes |
|
|
577
578
|
|
|
578
579
|
### `engine.packages`
|
|
579
580
|
|
|
@@ -1487,6 +1488,8 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
|
|
|
1487
1488
|
| ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1488
1489
|
| `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
|
|
1489
1490
|
| `normalizeRepoUrl` | `normalizeRepoUrl(repository)` | `string` — normalised `https://` URL, no trailing slash | normalising `package.json`'s `repository` field (object or shorthand string, `git+…git` or plain) to the exact URL Foundry fetches release assets from |
|
|
1491
|
+
| `BUILT_IN_ASSET_TRANSFORMS` | `const BUILT_IN_ASSET_TRANSFORMS` | `Readonly<Record<string, string>>` | the asset transforms this package ships, by the name `packageBuild.assetTransform` writes — `svg-theme` themes an icon to the reader's colour scheme |
|
|
1492
|
+
| `resolveAssetTransform` | `resolveAssetTransform(declared, rootDir)` | `string` — an absolute path to import | resolving a declared transform: a built-in name reaches the shipped module, anything else is a path against the repository root |
|
|
1490
1493
|
| `HOMEPAGE_ORIGIN` | `HOMEPAGE_ORIGIN` | `string` — the origin every package's homepage is served from | stating the site origin once, so the address a manifest advertises and the address the configuration documents cannot disagree |
|
|
1491
1494
|
| `packageHomepage` | `packageHomepage(contentPackage)` | `string` — `<origin>/<contentPackage>/` | deriving the homepage a manifest's `url` points at; throws when given no name rather than advertising `<origin>/undefined/` |
|
|
1492
1495
|
| `releaseUrls` | `releaseUrls({ repoUrl, homeUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's addresses — `url` is the homepage a reader follows before installing, while `bugs`, `manifest` and `download` stay on the repository holding the artefacts |
|
package/docs/commands.md
CHANGED
|
@@ -260,6 +260,60 @@ package-build: `packageKind: documentation` ships no Foundry package, so there i
|
|
|
260
260
|
`content-build site`, `content-build pdf`, `package-build bundle check`,
|
|
261
261
|
[Configuration](configuration.md).
|
|
262
262
|
|
|
263
|
+
### `package-build site-root`
|
|
264
|
+
|
|
265
|
+
**NAME**
|
|
266
|
+
|
|
267
|
+
`package-build site-root` — write the deployment's `_headers` and `_redirects`.
|
|
268
|
+
|
|
269
|
+
**SYNOPSIS**
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
package-build site-root [--out <dir>]
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**DESCRIPTION**
|
|
276
|
+
|
|
277
|
+
Hugo renders into `<out>/<contentPackage>/`, because the deployment carries the
|
|
278
|
+
`/<contentPackage>/` prefix physically and the routing layer is a
|
|
279
|
+
path-preserving pass-through. The directory that is _uploaded_ is its parent,
|
|
280
|
+
and Cloudflare Pages reads `_headers` and `_redirects` from there and nowhere
|
|
281
|
+
else — a copy inside the prefix is published as a text file and never applied.
|
|
282
|
+
Hugo owns everything under the prefix; this owns what sits beside it.
|
|
283
|
+
|
|
284
|
+
Two things are written. Indexing is suppressed on every address a deployment
|
|
285
|
+
answers on but nobody advertises — the project's `pages.dev`, the per-deployment
|
|
286
|
+
`pages.dev`, and the custom domain the routing layer fetches — each of which
|
|
287
|
+
would otherwise compete with the canonical URL in search results. And both
|
|
288
|
+
spellings of the prefix root redirect to the landing, with a lifetime pinned on
|
|
289
|
+
the 301, because Pages sets no `Cache-Control` on a redirect it generates and a
|
|
290
|
+
301 without one is cached indefinitely on the most-linked URL there is.
|
|
291
|
+
|
|
292
|
+
The rules are scoped to those hostnames, so a site deployed under a domain of
|
|
293
|
+
its own stays indexable.
|
|
294
|
+
|
|
295
|
+
**OPTIONS**
|
|
296
|
+
|
|
297
|
+
`--out <dir>` — the directory that is deployed. Defaults to `build/site`.
|
|
298
|
+
|
|
299
|
+
**EXIT STATUS**
|
|
300
|
+
|
|
301
|
+
1 when `<out>/<contentPackage>/` holds no rendered site, which means the site
|
|
302
|
+
build has not run and writing root files would publish a deployment with nothing
|
|
303
|
+
under the prefix. Otherwise 0.
|
|
304
|
+
|
|
305
|
+
**EXAMPLES**
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
$ package-build site-root
|
|
309
|
+
✅ Wrote build/site/_headers.
|
|
310
|
+
✅ Wrote build/site/_redirects.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**SEE ALSO**
|
|
314
|
+
|
|
315
|
+
`content-build site`, [Configuration](configuration.md).
|
|
316
|
+
|
|
263
317
|
`package-build lang <action>` asks three independent questions about this
|
|
264
318
|
repository's localization, each blind to what the others see: `check`,
|
|
265
319
|
`coverage` and `hardcoded`, one section below per action.
|
package/docs/configuration.md
CHANGED
|
@@ -580,7 +580,9 @@ packs:
|
|
|
580
580
|
|
|
581
581
|
> ``package-build config: `packs` must be an array.``
|
|
582
582
|
|
|
583
|
-
|
|
583
|
+
A package may declare **no** packs. A module that ships assets and compiles
|
|
584
|
+
nothing — alternative art for another package is the case — writes `packs: []`,
|
|
585
|
+
and the manifest carries an empty pack list.
|
|
584
586
|
|
|
585
587
|
Two packs (including companions, anywhere in the tree) may not share a name
|
|
586
588
|
— two packs both named `x` produce:
|
package/docs/content-format.md
CHANGED
|
@@ -1508,6 +1508,17 @@ One address resolves its record in this order:
|
|
|
1508
1508
|
3. Otherwise nothing, and the provenance fields are blank. A package that
|
|
1509
1509
|
records no attribution is a fact to state, not a walk to fail.
|
|
1510
1510
|
|
|
1511
|
+
**The nearest record is the whole answer.** It is never merged with the one
|
|
1512
|
+
above it, so a record states every key it means to claim — which is what lets a
|
|
1513
|
+
reader open one file and know what the tree beneath it carries, instead of
|
|
1514
|
+
resolving a chain by hand.
|
|
1515
|
+
|
|
1516
|
+
That is also why `attribution` and `license` are **required in any record that
|
|
1517
|
+
exists**: a key left out is not inherited from above, it is simply absent, so a
|
|
1518
|
+
record stating neither leaves every file it covers with no rights holder and no
|
|
1519
|
+
terms. Omitting one is a finding. `source`, `ai` and `notes` stay optional,
|
|
1520
|
+
because a blank is a truthful answer for each of them.
|
|
1521
|
+
|
|
1511
1522
|
#### A font is not an asset
|
|
1512
1523
|
|
|
1513
1524
|
`assets/fonts` is not a root, and a font has no address.
|
package/engine/asset-index.mjs
CHANGED
|
@@ -88,6 +88,10 @@ export const PROVENANCE_SIDECAR_SUFFIX = ".yaml";
|
|
|
88
88
|
* @property {string} name - The key inside `asset`.
|
|
89
89
|
* @property {"walk"|"provenance"} from - Where the value comes from: the walk
|
|
90
90
|
* itself, or the provenance record resolved for the address.
|
|
91
|
+
* @property {boolean} [required] - Whether a provenance record that exists must
|
|
92
|
+
* state this key. Omitting one is a finding rather than a blank, because a
|
|
93
|
+
* record resolves wholesale: the nearest one is the whole answer, so a key it
|
|
94
|
+
* leaves out is not inherited from above but simply absent.
|
|
91
95
|
* @property {string} describe - One line, for the author-facing reference.
|
|
92
96
|
*/
|
|
93
97
|
|
|
@@ -119,6 +123,7 @@ export const ASSET_RECORD_FIELDS = Object.freeze([
|
|
|
119
123
|
Object.freeze({
|
|
120
124
|
name: "attribution",
|
|
121
125
|
from: "provenance",
|
|
126
|
+
required: true,
|
|
122
127
|
describe: "The person holding the rights, to whom attribution is legally due.",
|
|
123
128
|
}),
|
|
124
129
|
Object.freeze({
|
|
@@ -134,6 +139,7 @@ export const ASSET_RECORD_FIELDS = Object.freeze([
|
|
|
134
139
|
Object.freeze({
|
|
135
140
|
name: "license",
|
|
136
141
|
from: "provenance",
|
|
142
|
+
required: true,
|
|
137
143
|
describe: "The licence it is used under — an SPDX identifier, or terms.",
|
|
138
144
|
}),
|
|
139
145
|
Object.freeze({
|
|
@@ -159,6 +165,22 @@ export const PROVENANCE_KEYS = Object.freeze(
|
|
|
159
165
|
),
|
|
160
166
|
);
|
|
161
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The keys a provenance file must state.
|
|
170
|
+
*
|
|
171
|
+
* Derived from {@link ASSET_RECORD_FIELDS} for the same reason the key set is:
|
|
172
|
+
* the rule and the record cannot disagree about which keys are required.
|
|
173
|
+
*
|
|
174
|
+
* @type {ReadonlySet<string>}
|
|
175
|
+
*/
|
|
176
|
+
export const REQUIRED_PROVENANCE_KEYS = Object.freeze(
|
|
177
|
+
new Set(
|
|
178
|
+
ASSET_RECORD_FIELDS.filter((field) => field.from === "provenance" && field.required).map(
|
|
179
|
+
(field) => field.name,
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
|
|
162
184
|
/**
|
|
163
185
|
* Read one provenance file, reporting every key that is not a provenance key.
|
|
164
186
|
*
|
|
@@ -218,6 +240,25 @@ function readProvenanceFile(file, findings) {
|
|
|
218
240
|
}
|
|
219
241
|
out[key] = value == null ? "" : String(value);
|
|
220
242
|
}
|
|
243
|
+
|
|
244
|
+
// A record resolves wholesale — the nearest one is the whole answer, and no
|
|
245
|
+
// ancestor fills a key it leaves out. So an omitted `attribution` or
|
|
246
|
+
// `license` is not a value inherited from above; it is a file with no stated
|
|
247
|
+
// rights holder and no stated terms, which is the one thing a provenance
|
|
248
|
+
// record exists to prevent. Reported against the file rather than a line,
|
|
249
|
+
// because the fault is an absence and has no position.
|
|
250
|
+
for (const key of REQUIRED_PROVENANCE_KEYS) {
|
|
251
|
+
if (out[key]) continue;
|
|
252
|
+
findings.push({
|
|
253
|
+
file,
|
|
254
|
+
severity: "error",
|
|
255
|
+
message:
|
|
256
|
+
`a provenance record states \`${key}\`, and this one does not. ` +
|
|
257
|
+
"A record replaces rather than extends the one above it, so every " +
|
|
258
|
+
"file it covers would carry no " +
|
|
259
|
+
(key === "license" ? "licence" : key),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
221
262
|
return out;
|
|
222
263
|
}
|
|
223
264
|
|
|
@@ -239,6 +280,9 @@ function inheritedProvenance(dir, root, cache, findings) {
|
|
|
239
280
|
const own = path.join(dir, PROVENANCE_FILE);
|
|
240
281
|
let answer;
|
|
241
282
|
if (fs.existsSync(own)) {
|
|
283
|
+
// The nearest record is the whole answer. It is not merged with the one
|
|
284
|
+
// above it, so a record states every key it means to claim and a reader
|
|
285
|
+
// needs only the file in front of them to know what a file carries.
|
|
242
286
|
answer = readProvenanceFile(own, findings);
|
|
243
287
|
} else if (path.resolve(dir) === path.resolve(root)) {
|
|
244
288
|
// The search stops at the type root: `assets/` above it is the
|
package/engine/content-index.mjs
CHANGED
|
@@ -567,10 +567,13 @@ export function collectContentIndex(
|
|
|
567
567
|
// caller's omission, and `walkMarkdownTree` says so.
|
|
568
568
|
const walkOpts = { skipDirectories };
|
|
569
569
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
570
|
+
// A package may ship assets and no notes at all, in which case there is no
|
|
571
|
+
// tree to walk and the index is its asset records. The caller decides
|
|
572
|
+
// whether an absent tree is a mistake; by the time the walk is reached it
|
|
573
|
+
// is simply a package with nothing to compile.
|
|
574
|
+
const notes = fs.existsSync(contentBase) ? walkMarkdownTree(contentBase, walkOpts) : [];
|
|
575
|
+
|
|
576
|
+
for (const { frontmatter, body, bodyLine, absPath } of notes) {
|
|
574
577
|
const fm = frontmatter ?? {};
|
|
575
578
|
// The id the note's document is filed under, resolved before
|
|
576
579
|
// the record is built so the index publishes the address *and* the id
|
|
@@ -674,6 +677,20 @@ export function serializeContentIndex(records) {
|
|
|
674
677
|
return `${records.map((r) => JSON.stringify(r)).join("\n")}\n`;
|
|
675
678
|
}
|
|
676
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Whether an absent content tree is a mistake.
|
|
682
|
+
*
|
|
683
|
+
* A package that declares a pack has notes to compile into it, so a missing
|
|
684
|
+
* tree is a misconfigured path and the build says so. A package that declares
|
|
685
|
+
* none ships assets and nothing else, and its index is its asset records.
|
|
686
|
+
*
|
|
687
|
+
* @param {object} resolved - The resolved configuration.
|
|
688
|
+
* @returns {boolean} Whether a tree is required.
|
|
689
|
+
*/
|
|
690
|
+
function needsContentTree(resolved) {
|
|
691
|
+
return (resolved.packs ?? []).length > 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
677
694
|
/**
|
|
678
695
|
* The index records for a content tree, without writing anything.
|
|
679
696
|
*
|
|
@@ -711,7 +728,9 @@ export function indexRecordsFor({
|
|
|
711
728
|
} = {}) {
|
|
712
729
|
const resolved = config ?? loadPackConfig();
|
|
713
730
|
const tree = contentBase ?? resolved.paths.content;
|
|
714
|
-
if (!fs.existsSync(tree)
|
|
731
|
+
if (!fs.existsSync(tree) && needsContentTree(resolved)) {
|
|
732
|
+
throw new Error(`no content tree at ${tree}`);
|
|
733
|
+
}
|
|
715
734
|
return collectContentIndex(tree, {
|
|
716
735
|
contentPackage: resolved.contentPackage,
|
|
717
736
|
skipDirectories: skipDirectories ?? resolved.skipDirectories,
|
|
@@ -745,7 +764,7 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
745
764
|
const dir = outDir ?? resolved.paths.contentIndex;
|
|
746
765
|
const contentPackage = resolved.contentPackage;
|
|
747
766
|
|
|
748
|
-
if (!fs.existsSync(tree)) {
|
|
767
|
+
if (!fs.existsSync(tree) && needsContentTree(resolved)) {
|
|
749
768
|
throw new Error(`no content tree at ${tree}`);
|
|
750
769
|
}
|
|
751
770
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The files that belong to a deployment's **root** rather than to the rendered
|
|
9
|
+
* site.
|
|
10
|
+
*
|
|
11
|
+
* Hugo renders into `<out>/<package>/`, because the deployment carries the
|
|
12
|
+
* `/<package>/` prefix physically and the routing layer is a path-preserving
|
|
13
|
+
* pass-through. The directory that is *uploaded* is its parent, and Cloudflare
|
|
14
|
+
* Pages reads `_headers` and `_redirects` from there and nowhere else — a copy
|
|
15
|
+
* inside the prefix is published as a text file and never applied. Hugo owns
|
|
16
|
+
* everything under the prefix; this owns what sits beside it.
|
|
17
|
+
*
|
|
18
|
+
* **One implementation, because it is one policy.** What is indexable, where
|
|
19
|
+
* the prefix root sends a reader, and how long that answer is cached are
|
|
20
|
+
* decisions about the hosting rather than about any one package. Held in each
|
|
21
|
+
* consumer they are the same file with one constant changed, which is a file
|
|
22
|
+
* that drifts — and the drift is invisible, because nobody reads all of the
|
|
23
|
+
* copies at once.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The namespace the routing layer derives a package's origin in.
|
|
33
|
+
*
|
|
34
|
+
* `/<package>/` on the public host is proxied to
|
|
35
|
+
* `https://<package>.<suffix>/<package>/`, and {@link noindexHeaders} depends on
|
|
36
|
+
* that being a dedicated namespace.
|
|
37
|
+
*
|
|
38
|
+
* @type {string}
|
|
39
|
+
*/
|
|
40
|
+
export const ORIGIN_SUFFIX = "pkg.heroiclands.org";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a package's landing is served, now that it is an addressed page.
|
|
44
|
+
*
|
|
45
|
+
* The site build emits the homepage at its own address rather than as the
|
|
46
|
+
* site root's `_index.md`, so the prefix root is a redirect to it.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} pkg - The content package name.
|
|
49
|
+
* @returns {string} The landing's path.
|
|
50
|
+
*/
|
|
51
|
+
export function landingPath(pkg) {
|
|
52
|
+
return `/${pkg}/homepage-root/`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Suppress indexing of every address a deployment answers on but nobody
|
|
57
|
+
* advertises.
|
|
58
|
+
*
|
|
59
|
+
* Cloudflare Pages assigns three: the project's own `pages.dev`, a per-
|
|
60
|
+
* deployment `pages.dev`, and the custom domain the project carries so the
|
|
61
|
+
* routing layer has an origin to fetch. None is advertised, all answer with the
|
|
62
|
+
* same pages, and left alone they are indexed and compete with the canonical
|
|
63
|
+
* URL in search results.
|
|
64
|
+
*
|
|
65
|
+
* The third matters most: it is the address the routing layer fetches, so it is
|
|
66
|
+
* the host-assigned address a reader is most plausibly handed. The rules are
|
|
67
|
+
* **scoped to those hostnames**, which keeps this correct for anyone deploying
|
|
68
|
+
* the site under a domain of their own — there it is indexable, and only the
|
|
69
|
+
* host-assigned addresses are not.
|
|
70
|
+
*
|
|
71
|
+
* @returns {string[]} The header block's lines.
|
|
72
|
+
*/
|
|
73
|
+
export function noindexHeaders() {
|
|
74
|
+
return [
|
|
75
|
+
"https://:project.pages.dev/*",
|
|
76
|
+
" X-Robots-Tag: noindex",
|
|
77
|
+
"",
|
|
78
|
+
"https://:version.:project.pages.dev/*",
|
|
79
|
+
" X-Robots-Tag: noindex",
|
|
80
|
+
"",
|
|
81
|
+
`https://:package.${ORIGIN_SUFFIX}/*`,
|
|
82
|
+
" X-Robots-Tag: noindex",
|
|
83
|
+
"",
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The lifetime pinned on the prefix-root redirect, and why it is pinned.
|
|
89
|
+
*
|
|
90
|
+
* Cloudflare Pages sets no `Cache-Control` on a redirect it generates — those
|
|
91
|
+
* responses carry `location` and nothing else — and a 301 with no lifetime is
|
|
92
|
+
* cached by a browser indefinitely, on the most-linked URL there is. An hour
|
|
93
|
+
* keeps the 301's canonical signal without the permanence.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} pkg - The content package name.
|
|
96
|
+
* @returns {string[]} The header block's lines.
|
|
97
|
+
*/
|
|
98
|
+
export function cacheHeaders(pkg) {
|
|
99
|
+
return [
|
|
100
|
+
`/${pkg}/`,
|
|
101
|
+
" Cache-Control: max-age=3600",
|
|
102
|
+
"",
|
|
103
|
+
`/${pkg}`,
|
|
104
|
+
" Cache-Control: max-age=3600",
|
|
105
|
+
"",
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Both forms of the prefix root, because Pages matches the raw path.
|
|
111
|
+
*
|
|
112
|
+
* Redirect matching runs before any trailing-slash or `index.html` handling, so
|
|
113
|
+
* `/<pkg>` and `/<pkg>/` are distinct keys and a rule on one does not catch the
|
|
114
|
+
* other.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} pkg - The content package name.
|
|
117
|
+
* @returns {string} The `_redirects` file's contents.
|
|
118
|
+
*/
|
|
119
|
+
export function redirects(pkg) {
|
|
120
|
+
const to = landingPath(pkg);
|
|
121
|
+
return [`/${pkg}/ ${to} 301`, `/${pkg} ${to} 301`, ""].join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The `_headers` file's contents.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} pkg - The content package name.
|
|
128
|
+
* @returns {string} The file's contents.
|
|
129
|
+
*/
|
|
130
|
+
export function headers(pkg) {
|
|
131
|
+
return [...noindexHeaders(), ...cacheHeaders(pkg)].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write `_headers` and `_redirects` beside the rendered site.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} options - Options.
|
|
138
|
+
* @param {string} options.pkg - The content package name, which is also the
|
|
139
|
+
* directory Hugo rendered into.
|
|
140
|
+
* @param {string} options.out - The directory that is deployed.
|
|
141
|
+
* @returns {{files: string[]}} The files written.
|
|
142
|
+
* @throws {Error} When no rendered site is there, which means the site build
|
|
143
|
+
* has not run and writing root files would publish a deployment with nothing
|
|
144
|
+
* under the prefix.
|
|
145
|
+
*/
|
|
146
|
+
export function writeSiteRoot({ pkg, out }) {
|
|
147
|
+
const root = path.resolve(out);
|
|
148
|
+
const rendered = path.join(root, pkg);
|
|
149
|
+
if (!fs.existsSync(path.join(rendered, "index.html"))) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`${out}/${pkg}/ holds no rendered site — build the site before its root files`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const written = [];
|
|
156
|
+
for (const [name, body] of [
|
|
157
|
+
["_headers", headers(pkg)],
|
|
158
|
+
["_redirects", redirects(pkg)],
|
|
159
|
+
]) {
|
|
160
|
+
const file = path.join(root, name);
|
|
161
|
+
fs.writeFileSync(file, body);
|
|
162
|
+
written.push(file);
|
|
163
|
+
}
|
|
164
|
+
return { files: written };
|
|
165
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
* A staged SVG follows the reader's colour scheme.
|
|
16
|
+
*
|
|
17
|
+
* An icon drawn as black line art disappears against a dark background, and
|
|
18
|
+
* Foundry themes its own chrome. So a shape that is explicitly black, or black
|
|
19
|
+
* by default because it declares no `fill`, gets a rule that paints it the
|
|
20
|
+
* ink colour for the scheme in use.
|
|
21
|
+
*
|
|
22
|
+
* **Named rather than pathed.** `packageBuild.assetTransform: svg-theme`
|
|
23
|
+
* reaches this module. Every package that themes its icons themes them the same
|
|
24
|
+
* way — the ink colours are the system's text tokens — so the transform is one
|
|
25
|
+
* implementation here rather than a copy in each consumer, where the copies
|
|
26
|
+
* drift and nobody sees all of them at once.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Iron-gall ink, and cream against a dark ground.
|
|
35
|
+
*
|
|
36
|
+
* These mirror `--sohl-color-text-primary` in the system's
|
|
37
|
+
* `scss/abstracts/_tokens.scss`. Stated once here, so a token change is one
|
|
38
|
+
* edit rather than one per consuming repository.
|
|
39
|
+
*
|
|
40
|
+
* @type {string}
|
|
41
|
+
*/
|
|
42
|
+
const INK_LIGHT = "#211d16";
|
|
43
|
+
|
|
44
|
+
/** @type {string} */
|
|
45
|
+
const INK_DARK = "#ece3cf";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The shapes a theme rule may repaint.
|
|
49
|
+
*
|
|
50
|
+
* Explicitly black, or black by default for want of a `fill`. A shape painted
|
|
51
|
+
* some other colour is deliberate — a white highlight in a two-tone badge — and
|
|
52
|
+
* repainting it would destroy the drawing.
|
|
53
|
+
*
|
|
54
|
+
* @type {string}
|
|
55
|
+
*/
|
|
56
|
+
const SELECTOR = [
|
|
57
|
+
'[fill="#000"]',
|
|
58
|
+
'[fill="#000000"]',
|
|
59
|
+
'[fill="black"]',
|
|
60
|
+
"path:not([fill])",
|
|
61
|
+
"rect:not([fill])",
|
|
62
|
+
"circle:not([fill])",
|
|
63
|
+
"ellipse:not([fill])",
|
|
64
|
+
"polygon:not([fill])",
|
|
65
|
+
"polyline:not([fill])",
|
|
66
|
+
"line:not([fill])",
|
|
67
|
+
"g:not([fill])",
|
|
68
|
+
].join(",");
|
|
69
|
+
|
|
70
|
+
/** @type {string} */
|
|
71
|
+
const STYLE =
|
|
72
|
+
`<style>${SELECTOR}{fill:${INK_LIGHT}}` +
|
|
73
|
+
`@media(prefers-color-scheme:dark){${SELECTOR}{fill:${INK_DARK}}}</style>`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Every `style="…"` attribute, capturing its declarations.
|
|
77
|
+
*
|
|
78
|
+
* @type {RegExp}
|
|
79
|
+
*/
|
|
80
|
+
const STYLE_ATTR = /style\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A `fill` *declaration* — the property itself, at the start or after a `;`.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately not `\bfill\b`, which also matches `fill-rule`, `fill-opacity`
|
|
86
|
+
* and `paint-order: fill`, none of which set a colour.
|
|
87
|
+
*
|
|
88
|
+
* @type {RegExp}
|
|
89
|
+
*/
|
|
90
|
+
const FILL_DECL = /(?:^|;)\s*fill\s*:/i;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether any shape sets its fill inline.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} svg - The SVG source.
|
|
96
|
+
* @returns {boolean} Whether an inline `fill` declaration is present.
|
|
97
|
+
*/
|
|
98
|
+
function hasInlineFill(svg) {
|
|
99
|
+
for (const [, dq, sq] of svg.matchAll(STYLE_ATTR)) {
|
|
100
|
+
if (FILL_DECL.test(dq ?? sq ?? "")) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The SVG with a scheme-aware fill rule, or unchanged where one cannot apply.
|
|
107
|
+
*
|
|
108
|
+
* Three files are returned as they are. One already carrying a
|
|
109
|
+
* `prefers-color-scheme` rule is themed — by this pass on an earlier run, or by
|
|
110
|
+
* its author — and re-theming it would stack rules. One with an inline `fill`
|
|
111
|
+
* cannot be themed at all, because an inline declaration beats a `<style>` rule
|
|
112
|
+
* and the result would be a half-recoloured icon, which is worse than an
|
|
113
|
+
* unthemed one. One with no `<svg>` element is not an SVG.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} svg - The SVG source.
|
|
116
|
+
* @returns {string} The themed source, or the input unchanged.
|
|
117
|
+
*/
|
|
118
|
+
export function injectAdaptiveFill(svg) {
|
|
119
|
+
if (typeof svg !== "string") return svg;
|
|
120
|
+
if (svg.includes("prefers-color-scheme")) return svg;
|
|
121
|
+
if (hasInlineFill(svg)) return svg;
|
|
122
|
+
|
|
123
|
+
const open = svg.match(/<svg\b[^>]*>/i);
|
|
124
|
+
if (!open) return svg;
|
|
125
|
+
|
|
126
|
+
const at = open.index + open[0].length;
|
|
127
|
+
return svg.slice(0, at) + STYLE + svg.slice(at);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The staging hook: theme an SVG, and pass everything else through untouched.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} sourcePath - The file being staged.
|
|
134
|
+
* @returns {string|null} The themed source, or `null` to stage the file as it
|
|
135
|
+
* is — which is what every non-SVG asset gets.
|
|
136
|
+
*/
|
|
137
|
+
export function transform(sourcePath) {
|
|
138
|
+
if (!sourcePath.endsWith(".svg")) return null;
|
|
139
|
+
return injectAdaptiveFill(readFileSync(sourcePath, "utf8"));
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.0.
|
|
3
|
+
"version": "22.0.3",
|
|
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",
|
package/types/config.d.mts
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a declared `assetTransform` is loaded from.
|
|
3
|
+
*
|
|
4
|
+
* A built-in name resolves to the module this package ships. Anything else is a
|
|
5
|
+
* path, resolved against the repository root the way it always was — so a
|
|
6
|
+
* consumer with a transform of its own is unaffected.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} declared - The authored value.
|
|
9
|
+
* @param {string} rootDir - The repository root.
|
|
10
|
+
* @returns {string} An absolute path to import.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveAssetTransform(declared: string, rootDir: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a package-build configuration from an already-loaded shared one.
|
|
15
|
+
*
|
|
16
|
+
* Separate from {@link loadPackageBuildConfig} because this half is pure: it
|
|
17
|
+
* reads no file and touches no environment, so the validation rules can be
|
|
18
|
+
* described directly by a test instead of through a fixture repository on
|
|
19
|
+
* disk. {@link loadPackageBuildConfig} is the same function with the loading
|
|
20
|
+
* put back.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} shared - The resolved content configuration.
|
|
23
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
24
|
+
* @throws {TypeError} When the reserved section declares something malformed.
|
|
25
|
+
*/
|
|
26
|
+
export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuildConfig>;
|
|
27
|
+
/**
|
|
28
|
+
* The repository's resolved package-build configuration.
|
|
29
|
+
*
|
|
30
|
+
* Read on call rather than at import, exactly as content-build resolves its
|
|
31
|
+
* own: importing a module of this package must not require a configuration to
|
|
32
|
+
* exist anywhere above it.
|
|
33
|
+
*
|
|
34
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
35
|
+
* @throws {TypeError} When there is no configuration, or the reserved section
|
|
36
|
+
* declares something malformed.
|
|
37
|
+
*/
|
|
38
|
+
export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
|
|
39
|
+
/**
|
|
40
|
+
* Manifest keys a repository may **not** declare, because the build derives
|
|
41
|
+
* them and would only overwrite what was written.
|
|
42
|
+
*
|
|
43
|
+
* Silently overwriting is the failure this list exists to prevent: a
|
|
44
|
+
* `version` typed into the configuration would look authoritative, sit there
|
|
45
|
+
* unread, and disagree with the shipped package forever. Declaring one is an
|
|
46
|
+
* error naming the key and where the value actually comes from.
|
|
47
|
+
*
|
|
48
|
+
* @type {Readonly<Record<string, string>>}
|
|
49
|
+
*/
|
|
50
|
+
export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
1
51
|
/**
|
|
2
52
|
* The resolved `packageBuild` section, every optional half filled in.
|
|
3
53
|
*
|
|
@@ -65,43 +115,16 @@
|
|
|
65
115
|
* collections, as collection → source directory.
|
|
66
116
|
*/
|
|
67
117
|
/**
|
|
68
|
-
*
|
|
118
|
+
* The asset transforms this package ships, by the name a consumer writes.
|
|
69
119
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* put back.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} shared - The resolved content configuration.
|
|
77
|
-
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
78
|
-
* @throws {TypeError} When the reserved section declares something malformed.
|
|
79
|
-
*/
|
|
80
|
-
export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuildConfig>;
|
|
81
|
-
/**
|
|
82
|
-
* The repository's resolved package-build configuration.
|
|
83
|
-
*
|
|
84
|
-
* Read on call rather than at import, exactly as content-build resolves its
|
|
85
|
-
* own: importing a module of this package must not require a configuration to
|
|
86
|
-
* exist anywhere above it.
|
|
87
|
-
*
|
|
88
|
-
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
89
|
-
* @throws {TypeError} When there is no configuration, or the reserved section
|
|
90
|
-
* declares something malformed.
|
|
91
|
-
*/
|
|
92
|
-
export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
|
|
93
|
-
/**
|
|
94
|
-
* Manifest keys a repository may **not** declare, because the build derives
|
|
95
|
-
* them and would only overwrite what was written.
|
|
96
|
-
*
|
|
97
|
-
* Silently overwriting is the failure this list exists to prevent: a
|
|
98
|
-
* `version` typed into the configuration would look authoritative, sit there
|
|
99
|
-
* unread, and disagree with the shipped package forever. Declaring one is an
|
|
100
|
-
* error naming the key and where the value actually comes from.
|
|
120
|
+
* `packageBuild.assetTransform` takes either one of these names or a path to a
|
|
121
|
+
* module of the consumer's own. A name is the answer where every package wants
|
|
122
|
+
* the same behaviour — theming an icon to the reader's colour scheme is not a
|
|
123
|
+
* per-package decision, and a copy in each consumer is a copy that drifts.
|
|
101
124
|
*
|
|
102
125
|
* @type {Readonly<Record<string, string>>}
|
|
103
126
|
*/
|
|
104
|
-
export const
|
|
127
|
+
export const BUILT_IN_ASSET_TRANSFORMS: Readonly<Record<string, string>>;
|
|
105
128
|
/**
|
|
106
129
|
* One staging copy: a source path in the repository, and where it lands under
|
|
107
130
|
* the staged package root.
|
|
@@ -43,6 +43,10 @@ export const PROVENANCE_SIDECAR_SUFFIX: string;
|
|
|
43
43
|
* @property {string} name - The key inside `asset`.
|
|
44
44
|
* @property {"walk"|"provenance"} from - Where the value comes from: the walk
|
|
45
45
|
* itself, or the provenance record resolved for the address.
|
|
46
|
+
* @property {boolean} [required] - Whether a provenance record that exists must
|
|
47
|
+
* state this key. Omitting one is a finding rather than a blank, because a
|
|
48
|
+
* record resolves wholesale: the nearest one is the whole answer, so a key it
|
|
49
|
+
* leaves out is not inherited from above but simply absent.
|
|
46
50
|
* @property {string} describe - One line, for the author-facing reference.
|
|
47
51
|
*/
|
|
48
52
|
/**
|
|
@@ -72,6 +76,15 @@ export const ASSET_RECORD_FIELDS: readonly AssetRecordField[];
|
|
|
72
76
|
* @type {ReadonlySet<string>}
|
|
73
77
|
*/
|
|
74
78
|
export const PROVENANCE_KEYS: ReadonlySet<string>;
|
|
79
|
+
/**
|
|
80
|
+
* The keys a provenance file must state.
|
|
81
|
+
*
|
|
82
|
+
* Derived from {@link ASSET_RECORD_FIELDS} for the same reason the key set is:
|
|
83
|
+
* the rule and the record cannot disagree about which keys are required.
|
|
84
|
+
*
|
|
85
|
+
* @type {ReadonlySet<string>}
|
|
86
|
+
*/
|
|
87
|
+
export const REQUIRED_PROVENANCE_KEYS: ReadonlySet<string>;
|
|
75
88
|
/**
|
|
76
89
|
* One field of the `asset` block.
|
|
77
90
|
*/
|
|
@@ -85,6 +98,13 @@ export type AssetRecordField = {
|
|
|
85
98
|
* itself, or the provenance record resolved for the address.
|
|
86
99
|
*/
|
|
87
100
|
from: "walk" | "provenance";
|
|
101
|
+
/**
|
|
102
|
+
* - Whether a provenance record that exists must
|
|
103
|
+
* state this key. Omitting one is a finding rather than a blank, because a
|
|
104
|
+
* record resolves wholesale: the nearest one is the whole answer, so a key it
|
|
105
|
+
* leaves out is not inherited from above but simply absent.
|
|
106
|
+
*/
|
|
107
|
+
required?: boolean | undefined;
|
|
88
108
|
/**
|
|
89
109
|
* - One line, for the author-facing reference.
|
|
90
110
|
*/
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a package's landing is served, now that it is an addressed page.
|
|
3
|
+
*
|
|
4
|
+
* The site build emits the homepage at its own address rather than as the
|
|
5
|
+
* site root's `_index.md`, so the prefix root is a redirect to it.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} pkg - The content package name.
|
|
8
|
+
* @returns {string} The landing's path.
|
|
9
|
+
*/
|
|
10
|
+
export function landingPath(pkg: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Suppress indexing of every address a deployment answers on but nobody
|
|
13
|
+
* advertises.
|
|
14
|
+
*
|
|
15
|
+
* Cloudflare Pages assigns three: the project's own `pages.dev`, a per-
|
|
16
|
+
* deployment `pages.dev`, and the custom domain the project carries so the
|
|
17
|
+
* routing layer has an origin to fetch. None is advertised, all answer with the
|
|
18
|
+
* same pages, and left alone they are indexed and compete with the canonical
|
|
19
|
+
* URL in search results.
|
|
20
|
+
*
|
|
21
|
+
* The third matters most: it is the address the routing layer fetches, so it is
|
|
22
|
+
* the host-assigned address a reader is most plausibly handed. The rules are
|
|
23
|
+
* **scoped to those hostnames**, which keeps this correct for anyone deploying
|
|
24
|
+
* the site under a domain of their own — there it is indexable, and only the
|
|
25
|
+
* host-assigned addresses are not.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string[]} The header block's lines.
|
|
28
|
+
*/
|
|
29
|
+
export function noindexHeaders(): string[];
|
|
30
|
+
/**
|
|
31
|
+
* The lifetime pinned on the prefix-root redirect, and why it is pinned.
|
|
32
|
+
*
|
|
33
|
+
* Cloudflare Pages sets no `Cache-Control` on a redirect it generates — those
|
|
34
|
+
* responses carry `location` and nothing else — and a 301 with no lifetime is
|
|
35
|
+
* cached by a browser indefinitely, on the most-linked URL there is. An hour
|
|
36
|
+
* keeps the 301's canonical signal without the permanence.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} pkg - The content package name.
|
|
39
|
+
* @returns {string[]} The header block's lines.
|
|
40
|
+
*/
|
|
41
|
+
export function cacheHeaders(pkg: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Both forms of the prefix root, because Pages matches the raw path.
|
|
44
|
+
*
|
|
45
|
+
* Redirect matching runs before any trailing-slash or `index.html` handling, so
|
|
46
|
+
* `/<pkg>` and `/<pkg>/` are distinct keys and a rule on one does not catch the
|
|
47
|
+
* other.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} pkg - The content package name.
|
|
50
|
+
* @returns {string} The `_redirects` file's contents.
|
|
51
|
+
*/
|
|
52
|
+
export function redirects(pkg: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* The `_headers` file's contents.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} pkg - The content package name.
|
|
57
|
+
* @returns {string} The file's contents.
|
|
58
|
+
*/
|
|
59
|
+
export function headers(pkg: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Write `_headers` and `_redirects` beside the rendered site.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options - Options.
|
|
64
|
+
* @param {string} options.pkg - The content package name, which is also the
|
|
65
|
+
* directory Hugo rendered into.
|
|
66
|
+
* @param {string} options.out - The directory that is deployed.
|
|
67
|
+
* @returns {{files: string[]}} The files written.
|
|
68
|
+
* @throws {Error} When no rendered site is there, which means the site build
|
|
69
|
+
* has not run and writing root files would publish a deployment with nothing
|
|
70
|
+
* under the prefix.
|
|
71
|
+
*/
|
|
72
|
+
export function writeSiteRoot({ pkg, out }: {
|
|
73
|
+
pkg: string;
|
|
74
|
+
out: string;
|
|
75
|
+
}): {
|
|
76
|
+
files: string[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The namespace the routing layer derives a package's origin in.
|
|
80
|
+
*
|
|
81
|
+
* `/<package>/` on the public host is proxied to
|
|
82
|
+
* `https://<package>.<suffix>/<package>/`, and {@link noindexHeaders} depends on
|
|
83
|
+
* that being a dedicated namespace.
|
|
84
|
+
*
|
|
85
|
+
* @type {string}
|
|
86
|
+
*/
|
|
87
|
+
export const ORIGIN_SUFFIX: string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SVG with a scheme-aware fill rule, or unchanged where one cannot apply.
|
|
3
|
+
*
|
|
4
|
+
* Three files are returned as they are. One already carrying a
|
|
5
|
+
* `prefers-color-scheme` rule is themed — by this pass on an earlier run, or by
|
|
6
|
+
* its author — and re-theming it would stack rules. One with an inline `fill`
|
|
7
|
+
* cannot be themed at all, because an inline declaration beats a `<style>` rule
|
|
8
|
+
* and the result would be a half-recoloured icon, which is worse than an
|
|
9
|
+
* unthemed one. One with no `<svg>` element is not an SVG.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} svg - The SVG source.
|
|
12
|
+
* @returns {string} The themed source, or the input unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export function injectAdaptiveFill(svg: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* The staging hook: theme an SVG, and pass everything else through untouched.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} sourcePath - The file being staged.
|
|
19
|
+
* @returns {string|null} The themed source, or `null` to stage the file as it
|
|
20
|
+
* is — which is what every non-SVG asset gets.
|
|
21
|
+
*/
|
|
22
|
+
export function transform(sourcePath: string): string | null;
|