@heroiclands/package-build 22.0.2 → 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 +28 -0
- package/bin/package-build.mjs +33 -0
- package/config.mjs +36 -2
- package/content-config.mjs +11 -4
- package/docs/api.md +2 -0
- package/docs/commands.md +54 -0
- package/docs/configuration.md +3 -1
- 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/site-root.d.mts +87 -0
- package/types/engine/svg-theme.d.mts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
3
31
|
## 22.0.2
|
|
4
32
|
|
|
5
33
|
### 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
|
@@ -1488,6 +1488,8 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
|
|
|
1488
1488
|
| ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1489
1489
|
| `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
|
|
1490
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 |
|
|
1491
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 |
|
|
1492
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/` |
|
|
1493
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/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.
|
|
@@ -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;
|