@heroiclands/package-build 25.6.2 → 25.6.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 25.6.3
4
+
5
+ **Rendering and sites**
6
+
7
+ - Build a preview at another address with `site build --base-url` and serve its images from another asset root with `--assets`. Pages, links, script fonts and search use the preview prefix, while the package's authored configuration stays unchanged.
8
+
3
9
  ## 25.6.2
4
10
 
5
11
  **Languages and scripts**
@@ -46,7 +46,12 @@ import log from "loglevel";
46
46
  import prefix from "loglevel-plugin-prefix";
47
47
  import { readStdin } from "../engine/read-stdin.mjs";
48
48
  import { compilePacks, cleanPacks, unpackPacks } from "../engine/compendiums.mjs";
49
- import { compilesFoundryDocuments, publishesContentPages, siteBase } from "../content-config.mjs";
49
+ import {
50
+ compilesFoundryDocuments,
51
+ publishesContentPages,
52
+ siteBase,
53
+ siteBuildConfig,
54
+ } from "../content-config.mjs";
50
55
  import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
51
56
  import { buildPdf, listBookAssets } from "../engine/pdf-build.mjs";
52
57
  import { summaryLines } from "../engine/pdf-coverage.mjs";
@@ -115,7 +120,7 @@ import {
115
120
  } from "../engine/encoding-lexicon.mjs";
116
121
  import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
117
122
  import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
118
- import { loadPackageBuildConfig } from "../config.mjs";
123
+ import { checkHomepage, loadPackageBuildConfig } from "../config.mjs";
119
124
  import {
120
125
  emitContentIndex,
121
126
  prepareContentIndex,
@@ -520,7 +525,7 @@ function withIndexPreflight(command, shouldCheck = () => true) {
520
525
  handler: async (argv) => {
521
526
  let config;
522
527
  try {
523
- config = loadPackConfig();
528
+ config = argv[CONTENT_CONFIG] ?? loadPackConfig();
524
529
  if (shouldCheck(config, argv)) cachedMetadataIndexes(config);
525
530
  } catch (err) {
526
531
  if (config) {
@@ -2279,9 +2284,11 @@ function siteCommand() {
2279
2284
  builder: (yargs) =>
2280
2285
  yargs
2281
2286
  .command(
2282
- withIndexPreflight(
2283
- withBodyPreflight(siteBuildCommand()),
2284
- publishesContentPages,
2287
+ withSiteBuildConfig(
2288
+ withIndexPreflight(
2289
+ withBodyPreflight(siteBuildCommand()),
2290
+ publishesContentPages,
2291
+ ),
2285
2292
  ),
2286
2293
  )
2287
2294
  .command(
@@ -2294,6 +2301,29 @@ function siteCommand() {
2294
2301
  };
2295
2302
  }
2296
2303
 
2304
+ /** Apply runtime site settings before the index and body preflights. */
2305
+ function withSiteBuildConfig(command) {
2306
+ const handler = command.handler;
2307
+ return {
2308
+ ...command,
2309
+ handler: async (argv) => {
2310
+ if (argv.baseUrl === undefined && argv.assets === undefined) return handler(argv);
2311
+ try {
2312
+ const authored = argv[CONTENT_CONFIG] ?? loadPackConfig();
2313
+ checkHomepage(authored.homepage, authored.contentPackage, authored.site.base);
2314
+ const config = siteBuildConfig(authored, {
2315
+ baseUrl: argv.baseUrl,
2316
+ assets: argv.assets,
2317
+ });
2318
+ return await handler({ ...argv, [CONTENT_CONFIG]: config });
2319
+ } catch (err) {
2320
+ reportFailure(err);
2321
+ process.exitCode = 1;
2322
+ }
2323
+ },
2324
+ };
2325
+ }
2326
+
2297
2327
  /**
2298
2328
  * Report what the layouts, or the build's section gate, found about a site.
2299
2329
  *
@@ -2342,10 +2372,21 @@ function siteBuildCommand() {
2342
2372
  command: "build",
2343
2373
  describe: "Render the content tree as a website and index it for search",
2344
2374
  builder: (yargs) =>
2345
- yargs.option("out", {
2346
- type: "string",
2347
- describe: `The directory that is deployed (default: ${DEPLOY_ROOT})`,
2348
- }),
2375
+ yargs
2376
+ .option("out", {
2377
+ type: "string",
2378
+ describe: `The directory that is deployed (default: ${DEPLOY_ROOT})`,
2379
+ })
2380
+ .option("base-url", {
2381
+ type: "string",
2382
+ requiresArg: true,
2383
+ describe: "Absolute HTTP(S) site URL for this build, ending in /",
2384
+ })
2385
+ .option("assets", {
2386
+ type: "string",
2387
+ requiresArg: true,
2388
+ describe: "Absolute HTTP(S) asset root for this build",
2389
+ }),
2349
2390
  handler: async (argv) => {
2350
2391
  try {
2351
2392
  const config = argv[CONTENT_CONFIG] ?? loadPackConfig();
@@ -228,6 +228,78 @@ export function siteBase(config) {
228
228
  return config.site.base || `/${config.contentPackage}/`;
229
229
  }
230
230
 
231
+ /**
232
+ * Apply validated runtime site URLs to a resolved configuration, without changing it.
233
+ * The caller checks the authored homepage before applying these build options.
234
+ *
235
+ * @param {ContentBuildConfig} config - The resolved authored configuration.
236
+ * @param {object} options - Runtime options.
237
+ * @param {string} [options.baseUrl] - Absolute HTTP(S) site URL ending in `/`.
238
+ * @param {string} [options.assets] - Absolute HTTP(S) asset root.
239
+ * @returns {ContentBuildConfig} The effective configuration, frozen.
240
+ */
241
+ export function siteBuildConfig(config, { baseUrl, assets }) {
242
+ if (baseUrl === undefined && assets === undefined) return config;
243
+ const homepage = baseUrl === undefined ? config.homepage : buildUrl(baseUrl, "--base-url").href;
244
+ const base =
245
+ baseUrl === undefined ? config.site.base : normalizeSiteBase(new URL(homepage).pathname);
246
+ const assetRoot =
247
+ assets === undefined ?
248
+ config.site.assets
249
+ : normalizeSiteAssets(buildUrl(assets, "--assets").href);
250
+ checkSiteSitemaps(config.site.sitemaps, base, config.contentPackage);
251
+ return Object.freeze({
252
+ ...config,
253
+ homepage,
254
+ site: Object.freeze({ ...config.site, base, assets: assetRoot }),
255
+ });
256
+ }
257
+
258
+ /**
259
+ * A runtime web root must survive URL joining and mapping to a local directory.
260
+ *
261
+ * @param {string} value - The command-line value.
262
+ * @param {string} option - The option naming it.
263
+ * @returns {URL} The checked URL.
264
+ */
265
+ function buildUrl(value, option) {
266
+ // URL parsing consumes both literal and escaped dot segments.
267
+ const rawPath = /^https?:\/\/[^/?#]+([^?#]*)/i.exec(value)?.[1] ?? "";
268
+ if (
269
+ rawPath.includes("%") ||
270
+ rawPath.split("/").some((segment) => segment === "." || segment === "..")
271
+ ) {
272
+ throw new TypeError(
273
+ `${option} must use a plain path without percent escapes or dot segments`,
274
+ );
275
+ }
276
+ let url;
277
+ try {
278
+ url = new URL(value);
279
+ } catch {
280
+ throw new TypeError(`${option} must be an absolute HTTP(S) URL`);
281
+ }
282
+ if (
283
+ !/^https?:\/\/[^/]+/i.test(value) ||
284
+ /[\s\\]/.test(value) ||
285
+ !["http:", "https:"].includes(url.protocol) ||
286
+ url.username ||
287
+ url.password ||
288
+ value.includes("?") ||
289
+ value.includes("#") ||
290
+ url.pathname.includes("//") ||
291
+ url.pathname.includes("%")
292
+ ) {
293
+ throw new TypeError(
294
+ `${option} must be an absolute HTTP(S) URL with a plain path and no credentials, query or fragment`,
295
+ );
296
+ }
297
+ if (option === "--base-url" && !url.pathname.endsWith("/")) {
298
+ throw new TypeError(`${option} must end with /`);
299
+ }
300
+ return url;
301
+ }
302
+
231
303
  /**
232
304
  * Whether this package publishes the pages its content tree compiles to.
233
305
  *
@@ -1874,6 +1946,23 @@ function packageSiteList(value, field, contentPackage) {
1874
1946
  );
1875
1947
  }
1876
1948
 
1949
+ /**
1950
+ * @param {readonly string[]} sitemaps - The configured package sitemaps.
1951
+ * @param {string} base - The site path.
1952
+ * @param {string} contentPackage - The package id.
1953
+ * @returns {void}
1954
+ */
1955
+ function checkSiteSitemaps(sitemaps, base, contentPackage) {
1956
+ if (sitemaps.length && base !== "/") {
1957
+ fail(
1958
+ "site.sitemaps",
1959
+ "names the package sites a sitemap index lists, and a sitemap index and " +
1960
+ "`robots.txt` belong at the root of an origin; this site is served at " +
1961
+ `\`${base || `/${contentPackage}/`}\`, so declare \`site.base: /\` or drop the key`,
1962
+ );
1963
+ }
1964
+ }
1965
+
1877
1966
  /**
1878
1967
  * The `site` section — how this repository frames the website it publishes.
1879
1968
  *
@@ -1939,14 +2028,7 @@ function normalizeSite(value, contentPackage) {
1939
2028
  const base = normalizeSiteBase(input.base);
1940
2029
  const search = optionalBoolean(input.search, "site.search", true);
1941
2030
  const sitemaps = packageSiteList(input.sitemaps, "site.sitemaps", contentPackage);
1942
- if (sitemaps.length && base !== "/") {
1943
- fail(
1944
- "site.sitemaps",
1945
- "names the package sites a sitemap index lists, and a sitemap index and " +
1946
- "`robots.txt` belong at the root of an origin; this site is served at " +
1947
- `\`${base || `/${contentPackage}/`}\`, so declare \`site.base: /\` or drop the key`,
1948
- );
1949
- }
2031
+ checkSiteSitemaps(sitemaps, base, contentPackage);
1950
2032
  const mergeIndexes = packageSiteList(input.mergeIndexes, "site.mergeIndexes", contentPackage);
1951
2033
  if (mergeIndexes.length && !search) {
1952
2034
  fail(
package/docs/api.md CHANGED
@@ -2193,20 +2193,21 @@ const config = defineConfig({
2193
2193
  });
2194
2194
  ```
2195
2195
 
2196
- | Export | Signature | Returns | Use it when |
2197
- | -------------------------- | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2198
- | `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
2199
- | `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
2200
- | `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
2201
- | `AUTHOR_ROLES` | `const AUTHOR_ROLES` | `readonly string[]` | reading the closed list an `authors` entry's `role` is drawn from — the configuration check refuses any other value |
2202
- | `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
2203
- | `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
2204
- | `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
2205
- | `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
2206
- | `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
2207
- | `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
2208
- | `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether the authored content tree contains notes beyond its homepage, shared by site, PDF, index, and Foundry address emission |
2209
- | `siteBase` | `siteBase(config)` | `string` | reading the path a package's site is served at — `site.base` where the configuration states one, `/<contentPackage>/` otherwise |
2196
+ | Export | Signature | Returns | Use it when |
2197
+ | -------------------------- | ---------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2198
+ | `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
2199
+ | `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
2200
+ | `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
2201
+ | `AUTHOR_ROLES` | `const AUTHOR_ROLES` | `readonly string[]` | reading the closed list an `authors` entry's `role` is drawn from — the configuration check refuses any other value |
2202
+ | `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
2203
+ | `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
2204
+ | `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
2205
+ | `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
2206
+ | `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
2207
+ | `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
2208
+ | `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether the authored content tree contains notes beyond its homepage, shared by site, PDF, index, and Foundry address emission |
2209
+ | `siteBase` | `siteBase(config)` | `string` | reading the path a package's site is served at — `site.base` where the configuration states one, `/<contentPackage>/` otherwise |
2210
+ | `siteBuildConfig` | `siteBuildConfig(config, { baseUrl, assets })` | `ContentBuildConfig` | applying optional runtime site URLs to a frozen effective configuration; the caller first validates the authored homepage with `checkHomepage`; the CLI uses it before index/body preflight |
2210
2211
 
2211
2212
  ## `./config`
2212
2213
 
package/docs/commands.md CHANGED
@@ -2685,7 +2685,7 @@ it for search.
2685
2685
  **SYNOPSIS**
2686
2686
 
2687
2687
  ```
2688
- package-build site build [--out <dir>]
2688
+ package-build site build [--out <dir>] [--base-url <url>] [--assets <url>]
2689
2689
  ```
2690
2690
 
2691
2691
  **DESCRIPTION**
@@ -2760,9 +2760,27 @@ run fails before the search index is built.
2760
2760
 
2761
2761
  `--out <dir>` — the directory that is deployed. Defaults to `build/site`.
2762
2762
 
2763
+ `--base-url <url>` — the absolute HTTP(S) address of this build. Its pathname
2764
+ becomes the effective `site.base`, and must end with `/`; `/` serves the site
2765
+ at the origin's root. Pages, local links, theme files, script fonts and search
2766
+ all use that prefix. Output lands under `<out>/<pathname>/`.
2767
+
2768
+ `--assets <url>` — the absolute HTTP(S) asset root for this build. It may
2769
+ include a path; trailing slashes are trimmed.
2770
+
2771
+ Both options are optional and independent. Without them, the build uses the
2772
+ authored `homepage`, `site.base` and `site.assets`. Overrides apply in memory
2773
+ before preflight and rendering; they never rewrite `package.json` or
2774
+ `package-build.config.yaml`. The authored homepage must still pass its usual
2775
+ validation. Both URLs require plain paths without percent escapes, dot
2776
+ segments (`.` or `..`), or doubled slashes, and reject credentials, queries,
2777
+ fragments, whitespace and backslashes.
2778
+ Invalid overrides fail before output is replaced. A site declaring
2779
+ `site.sitemaps` must still be served at `/`.
2780
+
2763
2781
  **EXIT STATUS**
2764
2782
 
2765
- 1 if `package.json` declares no `homepage`, or one that does not end
2783
+ 1 if a runtime URL override is invalid. 1 if `package.json` declares no `homepage`, or one that does not end
2766
2784
  `/<contentPackage>/`; if the site declares no title, description or `assets`
2767
2785
  host; or if the navigation has not been fetched (`package-build deps fetch`
2768
2786
  fills the cache and is named in the message). 1 if any gate fires — no
@@ -2774,6 +2792,15 @@ findings of severity `warning` included.
2774
2792
 
2775
2793
  **EXAMPLES**
2776
2794
 
2795
+ ```bash
2796
+ package-build site build \
2797
+ --base-url https://www.internal.heroiclands.org/thalorna/ \
2798
+ --assets https://cdn.internal.heroiclands.org
2799
+ package-build site build \
2800
+ --base-url https://www.internal.heroiclands.org/pr/thalorna/123/thalorna/ \
2801
+ --assets https://cdn.internal.heroiclands.org
2802
+ ```
2803
+
2777
2804
  ```
2778
2805
  $ package-build site build
2779
2806
  […] indexing content
@@ -1284,6 +1284,11 @@ site:
1284
1284
  mergeIndexes: [sohl, thalorna, kethira, harnensemble]
1285
1285
  ```
1286
1286
 
1287
+ The optional [`site build --base-url` and `--assets` flags](commands.md#package-build-site-build)
1288
+ apply runtime overrides to these resolved settings. The base URL supplies the
1289
+ effective homepage and `site.base`; the asset URL supplies effective
1290
+ `site.assets`. The authored configuration stays unchanged and must be valid.
1291
+
1287
1292
  ### What the site build reads
1288
1293
 
1289
1294
  `package-build site build` composes the site's settings from sources the
@@ -306,6 +306,36 @@ The terminal reports indexing, rendering, writing and search indexing as they
306
306
  start, including page counts where available. Progress is on stdout;
307
307
  diagnostics retain their file, line and column on stderr.
308
308
 
309
+ For an internal main preview, supply the build's address and asset root:
310
+
311
+ ```bash
312
+ package-build site build \
313
+ --base-url https://www.internal.heroiclands.org/thalorna/ \
314
+ --assets https://cdn.internal.heroiclands.org
315
+ ```
316
+
317
+ For a pull request preview, include the whole prefix:
318
+
319
+ ```bash
320
+ package-build site build \
321
+ --base-url https://www.internal.heroiclands.org/pr/thalorna/123/thalorna/ \
322
+ --assets https://cdn.internal.heroiclands.org
323
+ ```
324
+
325
+ The second build writes to `build/site/pr/thalorna/123/thalorna/`. Its local
326
+ links, theme files, script fonts and search stay under that prefix; its images
327
+ use the supplied asset root. `--out` changes the deployment directory above the
328
+ prefix. Both URL options are optional and independent, and require absolute
329
+ HTTP(S) URLs. The base URL's path ends with `/`; the asset root may include a
330
+ path and has trailing slashes trimmed. Use plain paths without percent escapes,
331
+ dot segments (`.` or `..`), or doubled slashes, credentials, queries or fragments.
332
+
333
+ The options apply to this build only. Authored `package.json` and YAML stay
334
+ unchanged, and the authored homepage still has to match the configured site.
335
+ Invalid overrides fail before replacing output. A site with `site.sitemaps`
336
+ requires a base URL at the origin's root. See the
337
+ [command reference](../commands.md#package-build-site-build) for URL validation.
338
+
309
339
  ```bash
310
340
  package-build site serve
311
341
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "25.6.2",
3
+ "version": "25.6.3",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -25,6 +25,20 @@ export function siteBase(config: {
25
25
  base: string;
26
26
  };
27
27
  }): string;
28
+ /**
29
+ * Apply validated runtime site URLs to a resolved configuration, without changing it.
30
+ * The caller checks the authored homepage before applying these build options.
31
+ *
32
+ * @param {ContentBuildConfig} config - The resolved authored configuration.
33
+ * @param {object} options - Runtime options.
34
+ * @param {string} [options.baseUrl] - Absolute HTTP(S) site URL ending in `/`.
35
+ * @param {string} [options.assets] - Absolute HTTP(S) asset root.
36
+ * @returns {ContentBuildConfig} The effective configuration, frozen.
37
+ */
38
+ export function siteBuildConfig(config: ContentBuildConfig, { baseUrl, assets }: {
39
+ baseUrl?: string | undefined;
40
+ assets?: string | undefined;
41
+ }): ContentBuildConfig;
28
42
  /**
29
43
  * Whether this package publishes the pages its content tree compiles to.
30
44
  *