@heroiclands/package-build 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/bin/package-build.mjs +363 -0
- package/config.mjs +318 -0
- package/package.json +17 -4
- package/types/config.d.mts +110 -0
package/README.md
CHANGED
|
@@ -56,6 +56,104 @@ The whole of assemble → validate → ship, one subpath each:
|
|
|
56
56
|
- **`text`** — locating a literal inside a file, so a finding names the line and
|
|
57
57
|
column it is about.
|
|
58
58
|
|
|
59
|
+
## Configure
|
|
60
|
+
|
|
61
|
+
A repository declares its build in **one** file — `content-build.config.yaml`,
|
|
62
|
+
the same one `content-build` reads — and this package takes its settings from
|
|
63
|
+
the reserved `packageBuild:` section:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
# Read from the top level, not restated below.
|
|
67
|
+
packageKind: modules
|
|
68
|
+
foundryPackage: sohl-thalorna
|
|
69
|
+
|
|
70
|
+
packageBuild:
|
|
71
|
+
# Where the package is assembled. Every `to:` below is relative to it, so a
|
|
72
|
+
# table reads `lang`, not `build/stage/lang`.
|
|
73
|
+
stageDir: build/stage
|
|
74
|
+
|
|
75
|
+
assets:
|
|
76
|
+
- { from: lang, to: lang }
|
|
77
|
+
- { from: assets/icons, to: assets/icons }
|
|
78
|
+
- { from: LICENSE.md, to: LICENSE.md }
|
|
79
|
+
|
|
80
|
+
# Optional. A module exporting `transform(sourcePath) -> string | null`,
|
|
81
|
+
# applied to every staged file — `null` copies it verbatim. This is the one
|
|
82
|
+
# genuine piece of code in staging, and it stays the repository's: SoHL
|
|
83
|
+
# rewrites each SVG's hard-coded fill so icons follow the Foundry theme.
|
|
84
|
+
assetTransform: ./utils/svg-theme.mjs
|
|
85
|
+
|
|
86
|
+
clean:
|
|
87
|
+
# Beyond the conventional build artifacts, which the library already knows.
|
|
88
|
+
extra: [site/content, site/public, site/resources]
|
|
89
|
+
|
|
90
|
+
lang:
|
|
91
|
+
sources: lang/*.json
|
|
92
|
+
# Printed after a failure — where this repository documents its key rules.
|
|
93
|
+
help: See kb/dev-docs/reference/localization-keys.md.
|
|
94
|
+
|
|
95
|
+
deploy:
|
|
96
|
+
# Prefix of the shared SFTP override variables. Default `SOHL`.
|
|
97
|
+
envPrefix: SOHL
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Why one file and not two.** Two of the values this package needs —
|
|
101
|
+
`packageKind` and `foundryPackage` — are already declared for `content-build`. A
|
|
102
|
+
second config file would restate them, which is two places for one fact; that is
|
|
103
|
+
exactly what every consumer's `push-stage.mjs` did, hard-coding
|
|
104
|
+
`packageKind: "systems"` and `packageId: "sohl"` beside a configuration that
|
|
105
|
+
already said both.
|
|
106
|
+
|
|
107
|
+
`content-build` checks only that `packageBuild:` is a mapping and hands it back
|
|
108
|
+
frozen. Everything inside it is validated here, so neither package learns the
|
|
109
|
+
other's schema — they split by input, and the dependency runs one way.
|
|
110
|
+
|
|
111
|
+
**What is derived, not stated:**
|
|
112
|
+
|
|
113
|
+
| Field | Derived from |
|
|
114
|
+
| -------------------------- | -------------------------------------------------------------------- |
|
|
115
|
+
| the repository root | the configuration file's own location |
|
|
116
|
+
| `packageKind`, `packageId` | the shared configuration's top level |
|
|
117
|
+
| the release artifact | `packageKind` — a system ships `system.json`, a module `module.json` |
|
|
118
|
+
|
|
119
|
+
## Command line
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
npx package-build clean [--distclean]
|
|
123
|
+
npx package-build assets
|
|
124
|
+
npx package-build lang check
|
|
125
|
+
npx package-build release
|
|
126
|
+
npx package-build deploy <stage>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Wrapped as npm scripts — SoHL spells them:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"clean": "package-build clean",
|
|
134
|
+
"distclean": "package-build clean --distclean",
|
|
135
|
+
"build:assets": "package-build assets",
|
|
136
|
+
"lint:lang": "package-build lang check",
|
|
137
|
+
"build:pack-release": "package-build release",
|
|
138
|
+
"push:qa": "package-build deploy qa"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Why the CLI exists.** This package was library-only, so every consuming
|
|
143
|
+
repository wrote a wrapper script per job — six of them in the SoHL repository,
|
|
144
|
+
441 lines that between them contained no logic. `clean.mjs` was 47 lines that
|
|
145
|
+
computed a `repoRoot` from `import.meta.url`, read one flag, and made one call.
|
|
146
|
+
Every copy had drifted from its sibling in the other repositories, because
|
|
147
|
+
copies do: `clean.mjs` was 47 lines in one and 48 in another, `copy-assets.mjs`
|
|
148
|
+
71 and 76.
|
|
149
|
+
|
|
150
|
+
It is the same shape the configuration had before it became data — not logic,
|
|
151
|
+
but the boilerplate a code file needs in order to state a literal. The literals
|
|
152
|
+
moved into configuration; the boilerplate lives in the CLI, once.
|
|
153
|
+
|
|
154
|
+
`--version` and `--help` answer in a directory with no configuration at all.
|
|
155
|
+
Running an actual command resolves it, and fails loudly when it is missing.
|
|
156
|
+
|
|
59
157
|
## Design
|
|
60
158
|
|
|
61
159
|
**The rules are pure, and I/O is confined to functions named for it.** A rule
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
4
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
5
|
+
*
|
|
6
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
7
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
8
|
+
*
|
|
9
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
10
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `package-build` command line — clean, stage, check, package, deploy.
|
|
17
|
+
*
|
|
18
|
+
* **Why this exists.** This package was library-only, so every consuming
|
|
19
|
+
* repository wrote a wrapper script per job: six of them in the Song of Heroic
|
|
20
|
+
* Lands repository, 441 lines that between them contained no logic. `clean.mjs`
|
|
21
|
+
* was 47 lines that computed a `repoRoot` from `import.meta.url`, read one
|
|
22
|
+
* flag, and made one call. `push-stage.mjs` hard-coded `packageKind: "systems"`
|
|
23
|
+
* and `packageId: "sohl"` beside a configuration that already declared both.
|
|
24
|
+
* Each copy had drifted from its sibling in the other repositories, because
|
|
25
|
+
* copies do.
|
|
26
|
+
*
|
|
27
|
+
* It is the same shape the configuration had before it became data: not logic,
|
|
28
|
+
* but the boilerplate a code file needs in order to state a literal. So the
|
|
29
|
+
* literals move into `content-build.config.yaml`'s reserved `packageBuild:`
|
|
30
|
+
* section, and the boilerplate lives here, once.
|
|
31
|
+
*
|
|
32
|
+
* **Every side effect lives in this file.** argv parsing, the environment,
|
|
33
|
+
* writing to the filesystem, and the process exit code. The library modules
|
|
34
|
+
* stay import-safe, so a consuming repository's build — or a test — can call
|
|
35
|
+
* them without any of it happening.
|
|
36
|
+
*
|
|
37
|
+
* The side effects that need *configuration* live inside the command handlers,
|
|
38
|
+
* never at module scope, so `--version` and `--help` answer in a directory with
|
|
39
|
+
* no configuration at all. Running an actual command still resolves it, and
|
|
40
|
+
* still fails loudly when it is missing.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* npx package-build clean [--distclean]
|
|
44
|
+
* npx package-build assets
|
|
45
|
+
* npx package-build lang check
|
|
46
|
+
* npx package-build release
|
|
47
|
+
* npx package-build deploy <stage>
|
|
48
|
+
*
|
|
49
|
+
* In a consuming repository, wrapped as npm scripts — SoHL spells them:
|
|
50
|
+
* npm run clean // → … clean
|
|
51
|
+
* npm run build:assets // → … assets
|
|
52
|
+
* npm run lint:lang // → … lang check
|
|
53
|
+
* npm run build:pack-release // → … release
|
|
54
|
+
* npm run push:qa // → … deploy qa
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import fs from "node:fs";
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
import process from "node:process";
|
|
60
|
+
import { globSync } from "glob";
|
|
61
|
+
import yargs from "yargs";
|
|
62
|
+
import { hideBin } from "yargs/helpers";
|
|
63
|
+
|
|
64
|
+
import { loadPackageBuildConfig } from "../config.mjs";
|
|
65
|
+
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
66
|
+
import { validateLangSource } from "../lang.mjs";
|
|
67
|
+
import { packRelease } from "../release.mjs";
|
|
68
|
+
import { deployStage } from "../deploy.mjs";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This package's own version, for `--version`.
|
|
72
|
+
*
|
|
73
|
+
* Read from this package's manifest rather than left to yargs, which defaults
|
|
74
|
+
* to the *nearest* `package.json` walking up from the working directory —
|
|
75
|
+
* inside a consuming repository that is the consumer's, so the CLI would report
|
|
76
|
+
* the consumer's version instead of the toolchain's.
|
|
77
|
+
*
|
|
78
|
+
* @returns {string} The `version` field of this package's manifest.
|
|
79
|
+
*/
|
|
80
|
+
function ownVersion() {
|
|
81
|
+
return JSON.parse(
|
|
82
|
+
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
83
|
+
).version;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Report a failure the way a build should: one line, no stack, non-zero exit.
|
|
88
|
+
*
|
|
89
|
+
* @param {unknown} err - What went wrong.
|
|
90
|
+
* @returns {never}
|
|
91
|
+
*/
|
|
92
|
+
function die(err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
console.error(`package-build: ${message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Wrap a command handler so every failure is reported the same way.
|
|
100
|
+
*
|
|
101
|
+
* yargs' own `.fail()` sees a *synchronous* handler's throw but not an async
|
|
102
|
+
* one's rejection, so without this a `clean` failure printed one clean line and
|
|
103
|
+
* a `deploy` failure printed a stack trace. A build's diagnostics should not
|
|
104
|
+
* depend on whether the command it ran happened to await something.
|
|
105
|
+
*
|
|
106
|
+
* @param {(args: object) => unknown} run - The handler body.
|
|
107
|
+
* @returns {(args: object) => Promise<void>} The wrapped handler.
|
|
108
|
+
*/
|
|
109
|
+
function handler(run) {
|
|
110
|
+
return async (args) => {
|
|
111
|
+
try {
|
|
112
|
+
await run(args);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
die(err);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `clean` — remove this repository's build artifacts.
|
|
121
|
+
*
|
|
122
|
+
* The conventional artifact directories are the library's; a repository that
|
|
123
|
+
* generates more (a site's `content/`, `public/` and `resources/`) names them
|
|
124
|
+
* in `packageBuild.clean.extra` rather than reimplementing the walk, which is
|
|
125
|
+
* what every consumer's `clean.mjs` did.
|
|
126
|
+
*
|
|
127
|
+
* @returns {object} The yargs command module.
|
|
128
|
+
*/
|
|
129
|
+
function cleanCommand() {
|
|
130
|
+
return {
|
|
131
|
+
command: "clean",
|
|
132
|
+
describe: "Remove build artifacts",
|
|
133
|
+
builder: (y) =>
|
|
134
|
+
y.option("distclean", {
|
|
135
|
+
type: "boolean",
|
|
136
|
+
default: false,
|
|
137
|
+
describe: "Also remove node_modules",
|
|
138
|
+
}),
|
|
139
|
+
handler: handler((args) => {
|
|
140
|
+
const config = loadPackageBuildConfig();
|
|
141
|
+
const removed = cleanBuildArtifacts(config.rootDir, {
|
|
142
|
+
includeNodeModules: args.distclean,
|
|
143
|
+
extra: config.cleanExtra,
|
|
144
|
+
});
|
|
145
|
+
for (const dir of removed) console.log(`Removed ${dir}`);
|
|
146
|
+
if (!removed.length) console.log("Nothing to clean.");
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `assets` — stage the repository's static files into the package root.
|
|
153
|
+
*
|
|
154
|
+
* The table is data (`packageBuild.assets`). A repository that has to *change*
|
|
155
|
+
* a file on the way — SoHL rewrites each SVG's hard-coded fill so icons follow
|
|
156
|
+
* the Foundry theme — names a module in `packageBuild.assetTransform`, whose
|
|
157
|
+
* `transform(sourcePath)` returns replacement text or `null` to copy verbatim.
|
|
158
|
+
* That is the one genuine piece of code in the job, and it stays the
|
|
159
|
+
* repository's.
|
|
160
|
+
*
|
|
161
|
+
* @returns {object} The yargs command module.
|
|
162
|
+
*/
|
|
163
|
+
function assetsCommand() {
|
|
164
|
+
return {
|
|
165
|
+
command: "assets",
|
|
166
|
+
describe: "Stage static assets into the package root",
|
|
167
|
+
builder: (y) => y,
|
|
168
|
+
handler: handler(async () => {
|
|
169
|
+
const config = loadPackageBuildConfig();
|
|
170
|
+
if (!config.assets.length) {
|
|
171
|
+
console.log(
|
|
172
|
+
"package-build: no `packageBuild.assets` declared; nothing to stage.",
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let transform;
|
|
178
|
+
if (config.assetTransform) {
|
|
179
|
+
const module = await import(
|
|
180
|
+
`file://${config.assetTransform}`
|
|
181
|
+
).catch((err) =>
|
|
182
|
+
die(
|
|
183
|
+
`cannot load \`packageBuild.assetTransform\` ` +
|
|
184
|
+
`(${config.assetTransform}): ${err.message}`,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
transform = module.transform;
|
|
188
|
+
if (typeof transform !== "function") {
|
|
189
|
+
die(
|
|
190
|
+
`\`packageBuild.assetTransform\` ` +
|
|
191
|
+
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
192
|
+
`function. It must export ` +
|
|
193
|
+
`\`transform(sourcePath) -> string | null\`.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `to:` is relative to the staged package root, so a
|
|
199
|
+
// repository's table reads `lang`, not `build/stage/lang`.
|
|
200
|
+
const entries = config.assets.map(({ from, to }) => [
|
|
201
|
+
from,
|
|
202
|
+
path.join(config.stageDir, to),
|
|
203
|
+
]);
|
|
204
|
+
const { entries: count, files } = stageAssets(entries, {
|
|
205
|
+
cwd: config.rootDir,
|
|
206
|
+
transform,
|
|
207
|
+
});
|
|
208
|
+
console.log(
|
|
209
|
+
`✅ Static assets staged (${count} entries, ${files} files).`,
|
|
210
|
+
);
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `lang check` — verify every localization file survives `expandObject`.
|
|
217
|
+
*
|
|
218
|
+
* A dotted-prefix collision makes `foundry.utils.expandObject` throw, and
|
|
219
|
+
* Foundry then drops the whole translation file silently. The rule is the
|
|
220
|
+
* library's; the glob and any repository-specific guidance are data.
|
|
221
|
+
*
|
|
222
|
+
* @returns {object} The yargs command module.
|
|
223
|
+
*/
|
|
224
|
+
function langCommand() {
|
|
225
|
+
return {
|
|
226
|
+
command: "lang <action>",
|
|
227
|
+
describe: "Localization checks",
|
|
228
|
+
builder: (y) =>
|
|
229
|
+
y.positional("action", {
|
|
230
|
+
choices: ["check"],
|
|
231
|
+
describe: "check: verify the files are expandObject-safe",
|
|
232
|
+
}),
|
|
233
|
+
handler: handler(() => {
|
|
234
|
+
const config = loadPackageBuildConfig();
|
|
235
|
+
const files = globSync(config.langSources, {
|
|
236
|
+
cwd: config.rootDir,
|
|
237
|
+
absolute: true,
|
|
238
|
+
});
|
|
239
|
+
if (!files.length) {
|
|
240
|
+
die(
|
|
241
|
+
`no localization files matched ` +
|
|
242
|
+
`\`${config.langSources}\` under ${config.rootDir}.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let total = 0;
|
|
247
|
+
for (const file of files.sort()) {
|
|
248
|
+
const relative = path.relative(config.rootDir, file);
|
|
249
|
+
for (const finding of validateLangSource(
|
|
250
|
+
fs.readFileSync(file, "utf8"),
|
|
251
|
+
)) {
|
|
252
|
+
total++;
|
|
253
|
+
// The diagnostics contract: the path starts the line, and a
|
|
254
|
+
// field is dropped rather than guessed.
|
|
255
|
+
const at = [relative, finding.line, finding.column]
|
|
256
|
+
.filter((part) => part !== undefined && part !== null)
|
|
257
|
+
.join(":");
|
|
258
|
+
console.error(
|
|
259
|
+
`${at}: ${finding.severity ?? "error"}: ${finding.message}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (total) {
|
|
265
|
+
if (config.langHelp) console.error(`\n${config.langHelp}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log(
|
|
269
|
+
`package-build: ${files.length} localization file(s) are ` +
|
|
270
|
+
`expandObject-safe.`,
|
|
271
|
+
);
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* `release` — zip the staged package for a GitHub release.
|
|
278
|
+
*
|
|
279
|
+
* The artifact name is derived from `packageKind`: Foundry installs a system
|
|
280
|
+
* from `system.json` and a module from `module.json`, so the kind already
|
|
281
|
+
* decides it and no repository states it.
|
|
282
|
+
*
|
|
283
|
+
* @returns {object} The yargs command module.
|
|
284
|
+
*/
|
|
285
|
+
function releaseCommand() {
|
|
286
|
+
return {
|
|
287
|
+
command: "release",
|
|
288
|
+
describe: "Package the staged build for release",
|
|
289
|
+
builder: (y) => y,
|
|
290
|
+
handler: handler(async () => {
|
|
291
|
+
const config = loadPackageBuildConfig();
|
|
292
|
+
const { zip, version, bytes } = await packRelease({
|
|
293
|
+
artifact: config.artifact,
|
|
294
|
+
});
|
|
295
|
+
console.log(
|
|
296
|
+
`✅ Packaged ${version} for release: ` +
|
|
297
|
+
`${path.relative(config.rootDir, zip)} ` +
|
|
298
|
+
`(${(bytes / 1024 / 1024).toFixed(1)} MB)`,
|
|
299
|
+
);
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `deploy <stage>` — push the staged package to a Foundry data directory.
|
|
306
|
+
*
|
|
307
|
+
* `packageKind` and `packageId` come from the shared configuration, where they
|
|
308
|
+
* were already declared. Every consumer's `push-stage.mjs` hard-coded them a
|
|
309
|
+
* second time, which is two places for one fact and exactly the drift this
|
|
310
|
+
* command removes.
|
|
311
|
+
*
|
|
312
|
+
* @returns {object} The yargs command module.
|
|
313
|
+
*/
|
|
314
|
+
function deployCommand() {
|
|
315
|
+
return {
|
|
316
|
+
command: "deploy <stage>",
|
|
317
|
+
describe: "Deploy the staged package to a stage",
|
|
318
|
+
builder: (y) =>
|
|
319
|
+
y.positional("stage", {
|
|
320
|
+
type: "string",
|
|
321
|
+
describe: "Target stage (e.g. dev, qa, prod, test)",
|
|
322
|
+
}),
|
|
323
|
+
handler: handler(async (args) => {
|
|
324
|
+
const config = loadPackageBuildConfig();
|
|
325
|
+
|
|
326
|
+
// Loaded here rather than at module scope: `--help` must answer in
|
|
327
|
+
// a repository that has no environment file at all.
|
|
328
|
+
const dotenv = await import("dotenv");
|
|
329
|
+
dotenv.config({
|
|
330
|
+
path: path.join(config.rootDir, ".env.local"),
|
|
331
|
+
quiet: true,
|
|
332
|
+
});
|
|
333
|
+
dotenv.config({
|
|
334
|
+
path: path.join(config.rootDir, ".env"),
|
|
335
|
+
quiet: true,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const { stage } = await deployStage({
|
|
339
|
+
stage: args.stage,
|
|
340
|
+
source: path.join(config.rootDir, config.stageDir),
|
|
341
|
+
packageKind: config.packageKind,
|
|
342
|
+
packageId: config.packageId,
|
|
343
|
+
prefix: config.envPrefix,
|
|
344
|
+
log: (message) => console.log(message),
|
|
345
|
+
});
|
|
346
|
+
console.log(`Deployed stage '${stage}' successfully.`);
|
|
347
|
+
}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
yargs(hideBin(process.argv))
|
|
352
|
+
.scriptName("package-build")
|
|
353
|
+
.command(cleanCommand())
|
|
354
|
+
.command(assetsCommand())
|
|
355
|
+
.command(langCommand())
|
|
356
|
+
.command(releaseCommand())
|
|
357
|
+
.command(deployCommand())
|
|
358
|
+
.demandCommand(1, "Name a command.")
|
|
359
|
+
.strict()
|
|
360
|
+
.version(ownVersion())
|
|
361
|
+
.help()
|
|
362
|
+
.alias("help", "h")
|
|
363
|
+
.fail((message, err) => die(err ?? message)).argv;
|
package/config.mjs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
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
|
+
* The per-repository configuration this package reads.
|
|
16
|
+
*
|
|
17
|
+
* **One repository, one configuration file.** A repository already declares
|
|
18
|
+
* itself in `content-build.config.yaml`, and two of the values this package
|
|
19
|
+
* needs — `packageKind` and `foundryPackage` — are already in it. A second file
|
|
20
|
+
* would restate them, which is two places for one fact; that is precisely what
|
|
21
|
+
* every consumer's `push-stage.mjs` did, hard-coding `packageKind: "systems"`
|
|
22
|
+
* and `packageId: "sohl"` beside a config that already said both.
|
|
23
|
+
*
|
|
24
|
+
* So this package reads the *same* file, through content-build's loader, and
|
|
25
|
+
* takes its own settings from the reserved `packageBuild:` section. The two
|
|
26
|
+
* packages split by **input** — content-build reads the content tree, this one
|
|
27
|
+
* reads `lang/`, `styles/`, `src/`, the assets and the manifest template — and
|
|
28
|
+
* neither validates the other's keys. content-build checks that the section is
|
|
29
|
+
* a mapping and hands it back frozen; everything inside it is validated here.
|
|
30
|
+
*
|
|
31
|
+
* **The dependency runs one way.** This package depends on content-build;
|
|
32
|
+
* content-build must never depend on this one. It is the same direction the
|
|
33
|
+
* loader already implies, and keeping it means content-build stays usable by a
|
|
34
|
+
* repository that ships content and no Foundry package at all.
|
|
35
|
+
*
|
|
36
|
+
* ```yaml
|
|
37
|
+
* # content-build.config.yaml
|
|
38
|
+
* packageKind: systems # read from the top level, not restated below
|
|
39
|
+
* foundryPackage: sohl
|
|
40
|
+
*
|
|
41
|
+
* packageBuild:
|
|
42
|
+
* assets:
|
|
43
|
+
* - { from: lang, to: lang }
|
|
44
|
+
* - { from: assets/icons, to: assets/icons }
|
|
45
|
+
* assetTransform: ./utils/svg-theme.mjs
|
|
46
|
+
* stageDir: build/stage
|
|
47
|
+
* clean:
|
|
48
|
+
* extra: [site/content, site/public]
|
|
49
|
+
* lang:
|
|
50
|
+
* sources: lang/*.json
|
|
51
|
+
* deploy:
|
|
52
|
+
* envPrefix: SOHL
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @module
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
import { loadPackConfig } from "@heroiclands/content-build/engine/pack-config";
|
|
60
|
+
|
|
61
|
+
/** Keys the reserved section may declare. */
|
|
62
|
+
const SECTION_KEYS = [
|
|
63
|
+
"stageDir",
|
|
64
|
+
"assets",
|
|
65
|
+
"assetTransform",
|
|
66
|
+
"clean",
|
|
67
|
+
"lang",
|
|
68
|
+
"deploy",
|
|
69
|
+
"release",
|
|
70
|
+
];
|
|
71
|
+
const ASSET_KEYS = ["from", "to"];
|
|
72
|
+
const CLEAN_KEYS = ["extra"];
|
|
73
|
+
const LANG_KEYS = ["sources", "help"];
|
|
74
|
+
const DEPLOY_KEYS = ["envPrefix"];
|
|
75
|
+
const RELEASE_KEYS = ["artifact"];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The artifact name each package kind ships, so no repository states it.
|
|
79
|
+
*
|
|
80
|
+
* Foundry installs a system from `system.json` and a module from `module.json`;
|
|
81
|
+
* the kind already says which, so `pack-release.mjs` passing
|
|
82
|
+
* `{ artifact: "system" }` by hand was restating `packageKind`.
|
|
83
|
+
*/
|
|
84
|
+
const ARTIFACT_OF_KIND = Object.freeze({
|
|
85
|
+
systems: "system",
|
|
86
|
+
modules: "module",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} where - Dotted path of the offending key.
|
|
91
|
+
* @param {string} problem - What is wrong with it.
|
|
92
|
+
* @returns {never}
|
|
93
|
+
*/
|
|
94
|
+
function fail(where, problem) {
|
|
95
|
+
throw new TypeError(`package-build config: \`${where}\` ${problem}.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} value
|
|
100
|
+
* @returns {boolean} Whether it is a plain mapping.
|
|
101
|
+
*/
|
|
102
|
+
function isMapping(value) {
|
|
103
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Record<string, unknown>} object - The mapping to check.
|
|
108
|
+
* @param {readonly string[]} allowed - The keys it may declare.
|
|
109
|
+
* @param {string} prefix - Dotted path prefix for the error.
|
|
110
|
+
*/
|
|
111
|
+
function rejectUnknownKeys(object, allowed, prefix) {
|
|
112
|
+
for (const key of Object.keys(object)) {
|
|
113
|
+
if (!allowed.includes(key)) {
|
|
114
|
+
fail(
|
|
115
|
+
`${prefix}${key}`,
|
|
116
|
+
`is not a recognised key (expected one of: ${allowed.join(", ")})`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {unknown} value
|
|
124
|
+
* @param {string} where
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function requireNonEmptyString(value, where) {
|
|
128
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
129
|
+
fail(where, "must be a non-empty string");
|
|
130
|
+
}
|
|
131
|
+
return /** @type {string} */ (value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One staging copy: a source path in the repository, and where it lands under
|
|
136
|
+
* the staged package root.
|
|
137
|
+
*
|
|
138
|
+
* @typedef {object} AssetSpec
|
|
139
|
+
* @property {string} from Source path, relative to the repository root.
|
|
140
|
+
* @property {string} to Destination, relative to the staged package root.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {unknown} value
|
|
145
|
+
* @param {number} index
|
|
146
|
+
* @returns {Readonly<AssetSpec>}
|
|
147
|
+
*/
|
|
148
|
+
function normalizeAsset(value, index) {
|
|
149
|
+
const where = `packageBuild.assets[${index}]`;
|
|
150
|
+
if (!isMapping(value)) fail(where, "must be a mapping");
|
|
151
|
+
const asset = /** @type {Record<string, unknown>} */ (value);
|
|
152
|
+
rejectUnknownKeys(asset, ASSET_KEYS, `${where}.`);
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
from: requireNonEmptyString(asset.from, `${where}.from`),
|
|
155
|
+
to: requireNonEmptyString(asset.to, `${where}.to`),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The resolved `packageBuild` section, every optional half filled in.
|
|
161
|
+
*
|
|
162
|
+
* @typedef {object} PackageBuildConfig
|
|
163
|
+
* @property {string} rootDir The repository root, from content-build.
|
|
164
|
+
* @property {string} packageKind `systems` or `modules`.
|
|
165
|
+
* @property {string} packageId The Foundry package id.
|
|
166
|
+
* @property {string} artifact Derived: `system` or `module`.
|
|
167
|
+
* @property {string} stageDir The staged package root, relative to
|
|
168
|
+
* `rootDir`. Every asset `to:` lands under it.
|
|
169
|
+
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
170
|
+
* @property {string|null} assetTransform Module to load a `transform` from,
|
|
171
|
+
* resolved against `rootDir`. `null` when
|
|
172
|
+
* the repository stages assets verbatim.
|
|
173
|
+
* @property {readonly string[]} cleanExtra Directories to remove beyond the
|
|
174
|
+
* conventional build artifacts.
|
|
175
|
+
* @property {string} langSources Glob for the localization files to check.
|
|
176
|
+
* @property {string|null} langHelp Extra guidance printed after a failure.
|
|
177
|
+
* @property {string} envPrefix Prefix of the deploy environment variables.
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve a package-build configuration from an already-loaded shared one.
|
|
182
|
+
*
|
|
183
|
+
* Separate from {@link loadPackageBuildConfig} because this half is pure: it
|
|
184
|
+
* reads no file and touches no environment, so the validation rules can be
|
|
185
|
+
* described directly by a test instead of through a fixture repository on
|
|
186
|
+
* disk. {@link loadPackageBuildConfig} is the same function with the loading
|
|
187
|
+
* put back.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} shared - The resolved content-build configuration.
|
|
190
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
191
|
+
* @throws {TypeError} When the reserved section declares something malformed.
|
|
192
|
+
*/
|
|
193
|
+
export function resolvePackageBuildConfig(shared) {
|
|
194
|
+
const section = /** @type {Record<string, unknown>} */ (
|
|
195
|
+
shared.packageBuild ?? {}
|
|
196
|
+
);
|
|
197
|
+
rejectUnknownKeys(section, SECTION_KEYS, "packageBuild.");
|
|
198
|
+
|
|
199
|
+
if (section.assets !== undefined && !Array.isArray(section.assets)) {
|
|
200
|
+
fail("packageBuild.assets", "must be a list");
|
|
201
|
+
}
|
|
202
|
+
const assets = (section.assets ?? []).map(normalizeAsset);
|
|
203
|
+
|
|
204
|
+
const clean = section.clean ?? {};
|
|
205
|
+
if (!isMapping(clean)) fail("packageBuild.clean", "must be a mapping");
|
|
206
|
+
rejectUnknownKeys(
|
|
207
|
+
/** @type {Record<string, unknown>} */ (clean),
|
|
208
|
+
CLEAN_KEYS,
|
|
209
|
+
"packageBuild.clean.",
|
|
210
|
+
);
|
|
211
|
+
const extra = /** @type {Record<string, unknown>} */ (clean).extra ?? [];
|
|
212
|
+
if (!Array.isArray(extra)) {
|
|
213
|
+
fail("packageBuild.clean.extra", "must be a list");
|
|
214
|
+
}
|
|
215
|
+
const cleanExtra = extra.map((dir, i) =>
|
|
216
|
+
requireNonEmptyString(dir, `packageBuild.clean.extra[${i}]`),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const lang = section.lang ?? {};
|
|
220
|
+
if (!isMapping(lang)) fail("packageBuild.lang", "must be a mapping");
|
|
221
|
+
rejectUnknownKeys(
|
|
222
|
+
/** @type {Record<string, unknown>} */ (lang),
|
|
223
|
+
LANG_KEYS,
|
|
224
|
+
"packageBuild.lang.",
|
|
225
|
+
);
|
|
226
|
+
const langInput = /** @type {Record<string, unknown>} */ (lang);
|
|
227
|
+
|
|
228
|
+
const deploy = section.deploy ?? {};
|
|
229
|
+
if (!isMapping(deploy)) fail("packageBuild.deploy", "must be a mapping");
|
|
230
|
+
rejectUnknownKeys(
|
|
231
|
+
/** @type {Record<string, unknown>} */ (deploy),
|
|
232
|
+
DEPLOY_KEYS,
|
|
233
|
+
"packageBuild.deploy.",
|
|
234
|
+
);
|
|
235
|
+
const deployInput = /** @type {Record<string, unknown>} */ (deploy);
|
|
236
|
+
|
|
237
|
+
const release = section.release ?? {};
|
|
238
|
+
if (!isMapping(release)) fail("packageBuild.release", "must be a mapping");
|
|
239
|
+
rejectUnknownKeys(
|
|
240
|
+
/** @type {Record<string, unknown>} */ (release),
|
|
241
|
+
RELEASE_KEYS,
|
|
242
|
+
"packageBuild.release.",
|
|
243
|
+
);
|
|
244
|
+
const releaseInput = /** @type {Record<string, unknown>} */ (release);
|
|
245
|
+
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
rootDir: shared.rootDir,
|
|
248
|
+
// Where the package is assembled before it is zipped or deployed. Every
|
|
249
|
+
// asset destination is relative to it, so a repository's table says
|
|
250
|
+
// `lang`, not `build/stage/lang` — the latter is what each consumer's
|
|
251
|
+
// `copy-assets.mjs` spelled out on every row.
|
|
252
|
+
stageDir:
|
|
253
|
+
section.stageDir === undefined ?
|
|
254
|
+
"build/stage"
|
|
255
|
+
: requireNonEmptyString(
|
|
256
|
+
section.stageDir,
|
|
257
|
+
"packageBuild.stageDir",
|
|
258
|
+
),
|
|
259
|
+
packageKind: shared.packageKind,
|
|
260
|
+
packageId: shared.foundryPackage,
|
|
261
|
+
// Derived from the kind, which already decides it. Stating it was one
|
|
262
|
+
// more literal every consumer's release script carried.
|
|
263
|
+
artifact:
|
|
264
|
+
releaseInput.artifact === undefined ?
|
|
265
|
+
ARTIFACT_OF_KIND[
|
|
266
|
+
/** @type {"systems"|"modules"} */ (shared.packageKind)
|
|
267
|
+
]
|
|
268
|
+
: requireNonEmptyString(
|
|
269
|
+
releaseInput.artifact,
|
|
270
|
+
"packageBuild.release.artifact",
|
|
271
|
+
),
|
|
272
|
+
assets: Object.freeze(assets),
|
|
273
|
+
assetTransform:
|
|
274
|
+
section.assetTransform === undefined ?
|
|
275
|
+
null
|
|
276
|
+
: path.resolve(
|
|
277
|
+
shared.rootDir,
|
|
278
|
+
requireNonEmptyString(
|
|
279
|
+
section.assetTransform,
|
|
280
|
+
"packageBuild.assetTransform",
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
cleanExtra: Object.freeze(cleanExtra),
|
|
284
|
+
langSources:
|
|
285
|
+
langInput.sources === undefined ?
|
|
286
|
+
"lang/*.json"
|
|
287
|
+
: requireNonEmptyString(
|
|
288
|
+
langInput.sources,
|
|
289
|
+
"packageBuild.lang.sources",
|
|
290
|
+
),
|
|
291
|
+
langHelp:
|
|
292
|
+
langInput.help === undefined ?
|
|
293
|
+
null
|
|
294
|
+
: requireNonEmptyString(langInput.help, "packageBuild.lang.help"),
|
|
295
|
+
envPrefix:
|
|
296
|
+
deployInput.envPrefix === undefined ?
|
|
297
|
+
"SOHL"
|
|
298
|
+
: requireNonEmptyString(
|
|
299
|
+
deployInput.envPrefix,
|
|
300
|
+
"packageBuild.deploy.envPrefix",
|
|
301
|
+
),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The repository's resolved package-build configuration.
|
|
307
|
+
*
|
|
308
|
+
* Read on call rather than at import, exactly as content-build resolves its
|
|
309
|
+
* own: importing a module of this package must not require a configuration to
|
|
310
|
+
* exist anywhere above it.
|
|
311
|
+
*
|
|
312
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
313
|
+
* @throws {TypeError} When there is no configuration, or the reserved section
|
|
314
|
+
* declares something malformed.
|
|
315
|
+
*/
|
|
316
|
+
export function loadPackageBuildConfig() {
|
|
317
|
+
return resolvePackageBuildConfig(loadPackConfig());
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"types": "./types/text.d.mts",
|
|
40
40
|
"import": "./text.mjs"
|
|
41
41
|
},
|
|
42
|
-
"./package.json": "./package.json"
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
"./config": {
|
|
44
|
+
"types": "./types/config.d.mts",
|
|
45
|
+
"import": "./config.mjs"
|
|
46
|
+
}
|
|
43
47
|
},
|
|
44
48
|
"files": [
|
|
45
49
|
"bundle.mjs",
|
|
@@ -51,7 +55,9 @@
|
|
|
51
55
|
"stage.mjs",
|
|
52
56
|
"text.mjs",
|
|
53
57
|
"types",
|
|
54
|
-
"README.md"
|
|
58
|
+
"README.md",
|
|
59
|
+
"bin",
|
|
60
|
+
"config.mjs"
|
|
55
61
|
],
|
|
56
62
|
"scripts": {
|
|
57
63
|
"test": "vitest run",
|
|
@@ -63,9 +69,13 @@
|
|
|
63
69
|
"prepare": "git config core.hooksPath .githooks || true"
|
|
64
70
|
},
|
|
65
71
|
"dependencies": {
|
|
72
|
+
"@heroiclands/content-build": "^0.15.0",
|
|
66
73
|
"acorn": "^8.18.0",
|
|
67
74
|
"archiver": "^8.0.0",
|
|
68
|
-
"
|
|
75
|
+
"dotenv": "^17.2.3",
|
|
76
|
+
"glob": "^11.0.3",
|
|
77
|
+
"ssh2-sftp-client": "^12.1.1",
|
|
78
|
+
"yargs": "^18.1.0"
|
|
69
79
|
},
|
|
70
80
|
"devDependencies": {
|
|
71
81
|
"@types/node": "^26.2.0",
|
|
@@ -95,5 +105,8 @@
|
|
|
95
105
|
},
|
|
96
106
|
"bugs": {
|
|
97
107
|
"url": "https://github.com/HeroicLands/package-build/issues"
|
|
108
|
+
},
|
|
109
|
+
"bin": {
|
|
110
|
+
"package-build": "./bin/package-build.mjs"
|
|
98
111
|
}
|
|
99
112
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolved `packageBuild` section, every optional half filled in.
|
|
3
|
+
*
|
|
4
|
+
* @typedef {object} PackageBuildConfig
|
|
5
|
+
* @property {string} rootDir The repository root, from content-build.
|
|
6
|
+
* @property {string} packageKind `systems` or `modules`.
|
|
7
|
+
* @property {string} packageId The Foundry package id.
|
|
8
|
+
* @property {string} artifact Derived: `system` or `module`.
|
|
9
|
+
* @property {string} stageDir The staged package root, relative to
|
|
10
|
+
* `rootDir`. Every asset `to:` lands under it.
|
|
11
|
+
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
12
|
+
* @property {string|null} assetTransform Module to load a `transform` from,
|
|
13
|
+
* resolved against `rootDir`. `null` when
|
|
14
|
+
* the repository stages assets verbatim.
|
|
15
|
+
* @property {readonly string[]} cleanExtra Directories to remove beyond the
|
|
16
|
+
* conventional build artifacts.
|
|
17
|
+
* @property {string} langSources Glob for the localization files to check.
|
|
18
|
+
* @property {string|null} langHelp Extra guidance printed after a failure.
|
|
19
|
+
* @property {string} envPrefix Prefix of the deploy environment variables.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a package-build configuration from an already-loaded shared one.
|
|
23
|
+
*
|
|
24
|
+
* Separate from {@link loadPackageBuildConfig} because this half is pure: it
|
|
25
|
+
* reads no file and touches no environment, so the validation rules can be
|
|
26
|
+
* described directly by a test instead of through a fixture repository on
|
|
27
|
+
* disk. {@link loadPackageBuildConfig} is the same function with the loading
|
|
28
|
+
* put back.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} shared - The resolved content-build configuration.
|
|
31
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
32
|
+
* @throws {TypeError} When the reserved section declares something malformed.
|
|
33
|
+
*/
|
|
34
|
+
export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuildConfig>;
|
|
35
|
+
/**
|
|
36
|
+
* The repository's resolved package-build configuration.
|
|
37
|
+
*
|
|
38
|
+
* Read on call rather than at import, exactly as content-build resolves its
|
|
39
|
+
* own: importing a module of this package must not require a configuration to
|
|
40
|
+
* exist anywhere above it.
|
|
41
|
+
*
|
|
42
|
+
* @returns {Readonly<PackageBuildConfig>} The frozen configuration.
|
|
43
|
+
* @throws {TypeError} When there is no configuration, or the reserved section
|
|
44
|
+
* declares something malformed.
|
|
45
|
+
*/
|
|
46
|
+
export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
|
|
47
|
+
/**
|
|
48
|
+
* One staging copy: a source path in the repository, and where it lands under
|
|
49
|
+
* the staged package root.
|
|
50
|
+
*/
|
|
51
|
+
export type AssetSpec = {
|
|
52
|
+
/**
|
|
53
|
+
* Source path, relative to the repository root.
|
|
54
|
+
*/
|
|
55
|
+
from: string;
|
|
56
|
+
/**
|
|
57
|
+
* Destination, relative to the staged package root.
|
|
58
|
+
*/
|
|
59
|
+
to: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The resolved `packageBuild` section, every optional half filled in.
|
|
63
|
+
*/
|
|
64
|
+
export type PackageBuildConfig = {
|
|
65
|
+
/**
|
|
66
|
+
* The repository root, from content-build.
|
|
67
|
+
*/
|
|
68
|
+
rootDir: string;
|
|
69
|
+
/**
|
|
70
|
+
* `systems` or `modules`.
|
|
71
|
+
*/
|
|
72
|
+
packageKind: string;
|
|
73
|
+
/**
|
|
74
|
+
* The Foundry package id.
|
|
75
|
+
*/
|
|
76
|
+
packageId: string;
|
|
77
|
+
/**
|
|
78
|
+
* Derived: `system` or `module`.
|
|
79
|
+
*/
|
|
80
|
+
artifact: string;
|
|
81
|
+
/**
|
|
82
|
+
* The staged package root, relative to
|
|
83
|
+
* `rootDir`. Every asset `to:` lands under it.
|
|
84
|
+
*/
|
|
85
|
+
stageDir: string;
|
|
86
|
+
assets: readonly Readonly<AssetSpec>[];
|
|
87
|
+
/**
|
|
88
|
+
* Module to load a `transform` from,
|
|
89
|
+
* resolved against `rootDir`. `null` when
|
|
90
|
+
* the repository stages assets verbatim.
|
|
91
|
+
*/
|
|
92
|
+
assetTransform: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Directories to remove beyond the
|
|
95
|
+
* conventional build artifacts.
|
|
96
|
+
*/
|
|
97
|
+
cleanExtra: readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* Glob for the localization files to check.
|
|
100
|
+
*/
|
|
101
|
+
langSources: string;
|
|
102
|
+
/**
|
|
103
|
+
* Extra guidance printed after a failure.
|
|
104
|
+
*/
|
|
105
|
+
langHelp: string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Prefix of the deploy environment variables.
|
|
108
|
+
*/
|
|
109
|
+
envPrefix: string;
|
|
110
|
+
};
|