@heroiclands/package-build 22.4.4 → 22.4.6

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.
@@ -0,0 +1,138 @@
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
+ * GraphViz, as `content-build map` reaches it.
16
+ *
17
+ * GraphViz is a build-time tool of the map command alone: nothing else in the
18
+ * toolchain draws, so nothing else depends on it, and a package that never
19
+ * runs `map` never needs it installed. The command finds the engine it needs
20
+ * once, names what to install when it is absent, and runs it over a `.dot`
21
+ * file it has already written — the `.dot` is always kept beside the `.svg`,
22
+ * so a drawing can be re-rendered, inspected or diffed without re-reading the
23
+ * content index.
24
+ *
25
+ * @module
26
+ */
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { spawnSync } from "node:child_process";
31
+
32
+ /**
33
+ * The layout engines the map command may ask for. `dot` draws a tree by
34
+ * rank, `twopi` the same tree radially, `neato` a graph by its edge lengths —
35
+ * and, with every node pinned, the map from a place.
36
+ *
37
+ * @type {readonly string[]}
38
+ */
39
+ export const GRAPHVIZ_ENGINES = Object.freeze(["dot", "twopi", "neato"]);
40
+
41
+ /**
42
+ * Where a package manager puts GraphViz when `PATH` does not say: Homebrew
43
+ * on Apple silicon, Homebrew on Intel, and the system directories.
44
+ *
45
+ * @type {readonly string[]}
46
+ */
47
+ const KNOWN_DIRECTORIES = Object.freeze([
48
+ "/opt/homebrew/bin",
49
+ "/usr/local/bin",
50
+ "/usr/bin",
51
+ "/opt/local/bin",
52
+ ]);
53
+
54
+ /**
55
+ * Find a GraphViz engine's binary.
56
+ *
57
+ * `PATH` first, then the directories a package manager installs into, so a
58
+ * shell whose profile does not export Homebrew's directory still finds it.
59
+ * The result is a path to a file that exists; whether it runs is the render's
60
+ * business.
61
+ *
62
+ * @param {string} engine - One of {@link GRAPHVIZ_ENGINES}.
63
+ * @param {object} [opts]
64
+ * @param {Record<string, string|undefined>} [opts.env] - The environment to
65
+ * read `PATH` from, defaulting to the process's.
66
+ * @param {readonly string[]} [opts.fallbacks] - The directories tried after
67
+ * `PATH`, defaulting to the package managers' ones.
68
+ * @returns {string|undefined} The binary, or `undefined` when none is found.
69
+ * @throws {Error} When `engine` is not a GraphViz engine this command uses.
70
+ */
71
+ export function findGraphviz(engine, { env = process.env, fallbacks = KNOWN_DIRECTORIES } = {}) {
72
+ if (!GRAPHVIZ_ENGINES.includes(engine)) {
73
+ throw new Error(
74
+ `"${engine}" is not a GraphViz engine the map command uses; ` +
75
+ `choose ${GRAPHVIZ_ENGINES.join(", ")}`,
76
+ );
77
+ }
78
+ const onPath = String(env.PATH ?? "")
79
+ .split(path.delimiter)
80
+ .filter(Boolean);
81
+ for (const dir of [...onPath, ...fallbacks]) {
82
+ const candidate = path.join(dir, engine);
83
+ try {
84
+ if (fs.statSync(candidate).isFile()) return candidate;
85
+ } catch {
86
+ // Not here; the next directory may have it.
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * What to say when an engine is not installed: the name, and what installs
94
+ * it on the two platforms a contributor is likely to be on.
95
+ *
96
+ * @param {string} engine - The engine that was wanted.
97
+ * @returns {string} The message.
98
+ */
99
+ export function graphvizMissingMessage(engine) {
100
+ return (
101
+ `GraphViz's \`${engine}\` is not installed, and \`content-build map\` draws with it; ` +
102
+ `install GraphViz (\`brew install graphviz\` on macOS, ` +
103
+ `\`apt-get install graphviz\` on Debian or Ubuntu) and run the command again`
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Render a `.dot` file with a GraphViz engine.
109
+ *
110
+ * The engine is run over the file rather than fed the text, so the file the
111
+ * command keeps is exactly what was drawn. Warnings GraphViz prints — an
112
+ * unknown font, an overlapping label — are returned rather than swallowed,
113
+ * and a non-zero exit is an error naming the file.
114
+ *
115
+ * @param {string} dotPath - The `.dot` file.
116
+ * @param {string} outPath - Where to write the rendering.
117
+ * @param {object} opts
118
+ * @param {string} opts.binary - The engine's binary, from {@link findGraphviz}.
119
+ * @param {string} [opts.format="svg"] - The `-T` format.
120
+ * @param {string[]} [opts.args] - Further arguments, before the file.
121
+ * @returns {{warnings: string}} What the engine printed on stderr.
122
+ * @throws {Error} When the engine exits non-zero or cannot be run.
123
+ */
124
+ export function renderDot(dotPath, outPath, { binary, format = "svg", args = [] }) {
125
+ const result = spawnSync(binary, [`-T${format}`, ...args, dotPath, "-o", outPath], {
126
+ encoding: "utf8",
127
+ });
128
+ if (result.error) {
129
+ throw new Error(`${binary} could not be run over ${dotPath}: ${result.error.message}`);
130
+ }
131
+ if (result.status !== 0) {
132
+ throw new Error(
133
+ `${binary} failed over ${dotPath} (exit ${result.status}): ` +
134
+ `${String(result.stderr ?? "").trim()}`,
135
+ );
136
+ }
137
+ return { warnings: String(result.stderr ?? "").trim() };
138
+ }