@heroiclands/package-build 0.5.0 → 0.6.1
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 +123 -0
- package/README.md +238 -2
- package/bin/package-build.mjs +482 -49
- package/bin/report.mjs +13 -4
- package/config.mjs +380 -1
- package/container.mjs +699 -0
- package/coverage.mjs +626 -0
- package/e2e.mjs +876 -0
- package/index.mjs +12 -0
- package/package.json +36 -13
- package/templates.mjs +224 -0
- package/types/config.d.mts +129 -0
- package/types/container.d.mts +359 -0
- package/types/coverage.d.mts +182 -0
- package/types/e2e.d.mts +351 -0
- package/types/index.d.mts +4 -0
- package/types/templates.d.mts +74 -0
package/bin/package-build.mjs
CHANGED
|
@@ -51,14 +51,20 @@
|
|
|
51
51
|
* npx package-build assets
|
|
52
52
|
* npx package-build manifest
|
|
53
53
|
* npx package-build lang check
|
|
54
|
+
* npx package-build lang coverage [--unused]
|
|
55
|
+
* npx package-build lang hardcoded
|
|
54
56
|
* npx package-build bundle check
|
|
55
57
|
* npx package-build release
|
|
56
58
|
* npx package-build deploy <stage>
|
|
59
|
+
* npx package-build container <stage> <action>
|
|
60
|
+
* npx package-build e2e <seed|run|open|fast|sweep>
|
|
57
61
|
*
|
|
58
62
|
* In a consuming repository, wrapped as npm scripts — SoHL spells them:
|
|
59
63
|
* npm run clean // → … clean
|
|
60
64
|
* npm run build:assets // → … assets
|
|
61
65
|
* npm run lint:lang // → … lang check
|
|
66
|
+
* npm run lint:lang-coverage // → … lang coverage
|
|
67
|
+
* npm run lint:lang-hardcoded // → … lang hardcoded
|
|
62
68
|
* npm run lint:bundle-globals // → … bundle check
|
|
63
69
|
* npm run build:pack-release // → … release
|
|
64
70
|
* npm run push:qa // → … deploy qa
|
|
@@ -75,12 +81,36 @@ import { loadPackageBuildConfig } from "../config.mjs";
|
|
|
75
81
|
import { loadPackConfig } from "@heroiclands/content-build/engine/pack-config";
|
|
76
82
|
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
77
83
|
import { validateLangSource } from "../lang.mjs";
|
|
84
|
+
import {
|
|
85
|
+
analyzeCoverage,
|
|
86
|
+
collectScriptReferences,
|
|
87
|
+
collectTemplateReferences,
|
|
88
|
+
keyRootsOf,
|
|
89
|
+
mergeReferences,
|
|
90
|
+
} from "../coverage.mjs";
|
|
91
|
+
import { findHardcodedText, findTemplateSyntaxErrors } from "../templates.mjs";
|
|
78
92
|
import { checkBundleLoading } from "../bundle.mjs";
|
|
79
93
|
import { packRelease } from "../release.mjs";
|
|
80
94
|
import { writeManifest } from "../manifest.mjs";
|
|
81
95
|
import { deployStage } from "../deploy.mjs";
|
|
96
|
+
import { CONTAINER_ACTIONS, containerAction } from "../container.mjs";
|
|
97
|
+
import {
|
|
98
|
+
E2E_MODES,
|
|
99
|
+
e2eFast,
|
|
100
|
+
e2eRun,
|
|
101
|
+
e2eSweep,
|
|
102
|
+
seedTestWorld,
|
|
103
|
+
} from "../e2e.mjs";
|
|
82
104
|
import { reportFindings } from "./report.mjs";
|
|
83
105
|
|
|
106
|
+
/**
|
|
107
|
+
* How many unreferenced keys a run prints before it summarizes the rest.
|
|
108
|
+
*
|
|
109
|
+
* Enough to see the shape of the problem, few enough that the errors above
|
|
110
|
+
* them are still on the screen.
|
|
111
|
+
*/
|
|
112
|
+
const ADVISORY_PREVIEW = 20;
|
|
113
|
+
|
|
84
114
|
/**
|
|
85
115
|
* This package's own version, for `--version`.
|
|
86
116
|
*
|
|
@@ -130,6 +160,55 @@ function handler(run) {
|
|
|
130
160
|
};
|
|
131
161
|
}
|
|
132
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Load the repository's environment files.
|
|
165
|
+
*
|
|
166
|
+
* Done inside a handler rather than at module scope: `--help` and `--version`
|
|
167
|
+
* must answer in a directory with no environment file — or no configuration —
|
|
168
|
+
* at all.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} config - The resolved package-build configuration.
|
|
171
|
+
* @returns {Promise<void>} Once `.env.local` and `.env` have been applied.
|
|
172
|
+
*/
|
|
173
|
+
async function loadEnvironment(config) {
|
|
174
|
+
const dotenv = await import("dotenv");
|
|
175
|
+
dotenv.config({
|
|
176
|
+
path: path.join(config.rootDir, ".env.local"),
|
|
177
|
+
quiet: true,
|
|
178
|
+
});
|
|
179
|
+
dotenv.config({ path: path.join(config.rootDir, ".env"), quiet: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The repository's own `package.json`.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} config - The resolved package-build configuration.
|
|
186
|
+
* @returns {object} The parsed manifest.
|
|
187
|
+
*/
|
|
188
|
+
function readPackageJson(config) {
|
|
189
|
+
return JSON.parse(
|
|
190
|
+
fs.readFileSync(path.join(config.rootDir, "package.json"), "utf8"),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Everything the user typed after an `e2e` action, verbatim.
|
|
196
|
+
*
|
|
197
|
+
* Read from the raw arguments rather than from yargs, because most of it is not
|
|
198
|
+
* this command line's to interpret: `--spec`, `--browser`, a `--` passthrough
|
|
199
|
+
* and whatever else belongs to the repository's suite. Parsing it here would
|
|
200
|
+
* mean this package learning the flags of a runner it deliberately knows
|
|
201
|
+
* nothing about.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} action - The action that was run.
|
|
204
|
+
* @returns {string[]} The arguments after it.
|
|
205
|
+
*/
|
|
206
|
+
function e2eTail(action) {
|
|
207
|
+
const raw = hideBin(process.argv);
|
|
208
|
+
const at = raw.indexOf(action);
|
|
209
|
+
return at === -1 ? [] : raw.slice(at + 1);
|
|
210
|
+
}
|
|
211
|
+
|
|
133
212
|
/**
|
|
134
213
|
* `clean` — remove this repository's build artifacts.
|
|
135
214
|
*
|
|
@@ -246,12 +325,7 @@ function manifestCommand() {
|
|
|
246
325
|
handler: handler(async () => {
|
|
247
326
|
const config = loadPackageBuildConfig();
|
|
248
327
|
const shared = loadPackConfig();
|
|
249
|
-
const packageJson =
|
|
250
|
-
fs.readFileSync(
|
|
251
|
-
path.join(config.rootDir, "package.json"),
|
|
252
|
-
"utf8",
|
|
253
|
-
),
|
|
254
|
-
);
|
|
328
|
+
const packageJson = readPackageJson(config);
|
|
255
329
|
|
|
256
330
|
let flags;
|
|
257
331
|
if (config.manifestFlags) {
|
|
@@ -290,6 +364,26 @@ function manifestCommand() {
|
|
|
290
364
|
};
|
|
291
365
|
}
|
|
292
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Every file matching a glob, as `{ path, text }` with the path relative to the
|
|
369
|
+
* repository root — the form both the rules and the findings want.
|
|
370
|
+
*
|
|
371
|
+
* @param {string|readonly string[]} globs - Glob or globs, relative to the root.
|
|
372
|
+
* @param {string} rootDir - The repository root.
|
|
373
|
+
* @returns {{path: string, text: string}[]} The files, in path order.
|
|
374
|
+
*/
|
|
375
|
+
function readMatching(globs, rootDir) {
|
|
376
|
+
return globSync([...(Array.isArray(globs) ? globs : [globs])], {
|
|
377
|
+
cwd: rootDir,
|
|
378
|
+
absolute: true,
|
|
379
|
+
})
|
|
380
|
+
.sort()
|
|
381
|
+
.map((file) => ({
|
|
382
|
+
path: path.relative(rootDir, file),
|
|
383
|
+
text: fs.readFileSync(file, "utf8"),
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
293
387
|
/**
|
|
294
388
|
* `lang check` — verify every localization file survives `expandObject`.
|
|
295
389
|
*
|
|
@@ -297,6 +391,251 @@ function manifestCommand() {
|
|
|
297
391
|
* Foundry then drops the whole translation file silently. The rule is the
|
|
298
392
|
* library's; the glob and any repository-specific guidance are data.
|
|
299
393
|
*
|
|
394
|
+
* @param {Readonly<import("../config.mjs").PackageBuildConfig>} config
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
function langCheck(config) {
|
|
398
|
+
const files = readMatching(config.langSources, config.rootDir);
|
|
399
|
+
if (!files.length) {
|
|
400
|
+
die(
|
|
401
|
+
`no localization files matched \`${config.langSources}\` under ` +
|
|
402
|
+
`${config.rootDir}.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let total = 0;
|
|
407
|
+
for (const file of files) {
|
|
408
|
+
total += reportFindings(validateLangSource(file.text), {
|
|
409
|
+
file: file.path,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (total) {
|
|
414
|
+
if (config.langHelp) console.error(`\n${config.langHelp}`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
console.log(
|
|
418
|
+
`package-build: ${files.length} localization file(s) are ` +
|
|
419
|
+
`expandObject-safe.`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* `lang coverage` — does every key the package references exist, and is every
|
|
425
|
+
* key it declares referenced?
|
|
426
|
+
*
|
|
427
|
+
* The two halves are not the same severity. A referenced key that is missing
|
|
428
|
+
* renders to a player as its own raw key string, so it fails the run; a key
|
|
429
|
+
* nothing references is reported and does not, because no scan can see every
|
|
430
|
+
* way a key is reached and a guard that fails over one teaches people to switch
|
|
431
|
+
* it off.
|
|
432
|
+
*
|
|
433
|
+
* A repository that *generates* keys by a convention of its own names a module
|
|
434
|
+
* in `packageBuild.lang.references`, exporting
|
|
435
|
+
* `references(context) -> ReferenceSet`. That is the same shape as
|
|
436
|
+
* `assetTransform` and `manifestFlags`, and for the same reason: only that
|
|
437
|
+
* repository can know its rule, and only this package can compare the result
|
|
438
|
+
* against the file.
|
|
439
|
+
*
|
|
440
|
+
* @param {Readonly<import("../config.mjs").PackageBuildConfig>} config
|
|
441
|
+
* @param {object} args - The parsed argv.
|
|
442
|
+
* @returns {Promise<void>}
|
|
443
|
+
*/
|
|
444
|
+
async function langCoverage(config, args) {
|
|
445
|
+
const langFile = config.langPrimary;
|
|
446
|
+
const langPath = path.join(config.rootDir, langFile);
|
|
447
|
+
if (!fs.existsSync(langPath)) {
|
|
448
|
+
die(
|
|
449
|
+
`no localization file at ${langFile} — name the one this package ` +
|
|
450
|
+
`authors in \`packageBuild.lang.primary\`.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const langSource = fs.readFileSync(langPath, "utf8");
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Report and exit, so the two exits — nothing to compare against, and a
|
|
457
|
+
* comparison that failed — read the same way.
|
|
458
|
+
*
|
|
459
|
+
* @param {object} analysis - What {@link analyzeCoverage} returned.
|
|
460
|
+
* @returns {void}
|
|
461
|
+
*/
|
|
462
|
+
const finish = ({ findings, unreferenced, stats }) => {
|
|
463
|
+
const errors = reportFindings(findings, {});
|
|
464
|
+
// Capped by default: the advisory half of a large package is pages
|
|
465
|
+
// long, and pages of warnings on every build is how a guard stops being
|
|
466
|
+
// read at all. The count is always stated, so nothing is hidden.
|
|
467
|
+
const shown =
|
|
468
|
+
args.unused ? unreferenced : (
|
|
469
|
+
unreferenced.slice(0, ADVISORY_PREVIEW)
|
|
470
|
+
);
|
|
471
|
+
reportFindings(shown, {});
|
|
472
|
+
if (shown.length < unreferenced.length) {
|
|
473
|
+
console.error(
|
|
474
|
+
`package-build: ${unreferenced.length - shown.length} further ` +
|
|
475
|
+
`unreferenced key(s) not shown — run with --unused.`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
console.log(
|
|
480
|
+
`package-build: ${stats.declared} key(s) declared in ${langFile} · ` +
|
|
481
|
+
`${stats.referenced} referenced · ` +
|
|
482
|
+
`${stats.namespaces} namespace(s) · ` +
|
|
483
|
+
`${stats.patterns} dynamic shape(s) · ` +
|
|
484
|
+
`${stats.missing} missing · ` +
|
|
485
|
+
`${stats.unreferenced} unreferenced`,
|
|
486
|
+
);
|
|
487
|
+
if (errors) process.exit(1);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
let declaredKeys;
|
|
491
|
+
try {
|
|
492
|
+
declaredKeys = Object.keys(JSON.parse(langSource));
|
|
493
|
+
} catch {
|
|
494
|
+
// Scanning would be pointless: with nothing to compare against, every
|
|
495
|
+
// key the package references reports as missing. `analyzeCoverage`
|
|
496
|
+
// says the one true thing instead.
|
|
497
|
+
finish(
|
|
498
|
+
analyzeCoverage({
|
|
499
|
+
langSource,
|
|
500
|
+
langFile,
|
|
501
|
+
references: {
|
|
502
|
+
keys: [],
|
|
503
|
+
namespaces: [],
|
|
504
|
+
patterns: [],
|
|
505
|
+
findings: [],
|
|
506
|
+
},
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const roots = config.langKeyRoots ?? keyRootsOf(declaredKeys);
|
|
513
|
+
const scripts = readMatching(config.langScripts, config.rootDir);
|
|
514
|
+
const templates = readMatching(config.langTemplates, config.rootDir);
|
|
515
|
+
// Named rather than left to pass: with nothing scanned, every key reports
|
|
516
|
+
// as unreferenced and nothing reports as missing, so a run that looked at
|
|
517
|
+
// no files at all would exit zero and prove nothing.
|
|
518
|
+
if (!scripts.length && !templates.length) {
|
|
519
|
+
die(
|
|
520
|
+
`no sources matched \`${config.langScripts.join("`, `")}\` or ` +
|
|
521
|
+
`\`${config.langTemplates.join("`, `")}\` under ` +
|
|
522
|
+
`${config.rootDir}.`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const sets = [
|
|
526
|
+
...scripts.map((file) =>
|
|
527
|
+
collectScriptReferences(file.text, { file: file.path, roots }),
|
|
528
|
+
),
|
|
529
|
+
...templates.map((file) =>
|
|
530
|
+
collectTemplateReferences(file.text, { file: file.path, roots }),
|
|
531
|
+
),
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
if (config.langReferences) {
|
|
535
|
+
const module = await import(`file://${config.langReferences}`).catch(
|
|
536
|
+
(err) =>
|
|
537
|
+
die(
|
|
538
|
+
`cannot load \`packageBuild.lang.references\` ` +
|
|
539
|
+
`(${config.langReferences}): ${err.message}`,
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
if (typeof module.references !== "function") {
|
|
543
|
+
die(
|
|
544
|
+
`\`packageBuild.lang.references\` ` +
|
|
545
|
+
`(${config.langReferences}) exports no \`references\` ` +
|
|
546
|
+
`function. It must export ` +
|
|
547
|
+
`\`references(context) -> ReferenceSet\`.`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
sets.push(
|
|
551
|
+
await module.references({
|
|
552
|
+
config: loadPackConfig(),
|
|
553
|
+
rootDir: config.rootDir,
|
|
554
|
+
roots,
|
|
555
|
+
// The sources, already read, so the contributor sees exactly
|
|
556
|
+
// the text the built-in scan saw.
|
|
557
|
+
files: scripts,
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
finish(
|
|
563
|
+
analyzeCoverage({
|
|
564
|
+
langSource,
|
|
565
|
+
langFile,
|
|
566
|
+
references: mergeReferences(sets),
|
|
567
|
+
retained: config.langRetained,
|
|
568
|
+
roots,
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* `lang hardcoded` — does the markup's user-visible text go through
|
|
575
|
+
* localization, and does each template still compile?
|
|
576
|
+
*
|
|
577
|
+
* The reverse of `lang coverage`, which walks key → file and is blind to a
|
|
578
|
+
* template that mentions no key at all.
|
|
579
|
+
*
|
|
580
|
+
* @param {Readonly<import("../config.mjs").PackageBuildConfig>} config
|
|
581
|
+
* @returns {void}
|
|
582
|
+
*/
|
|
583
|
+
function langHardcoded(config) {
|
|
584
|
+
const templates = readMatching(config.langTemplates, config.rootDir);
|
|
585
|
+
if (!templates.length) {
|
|
586
|
+
die(
|
|
587
|
+
`no templates matched ` +
|
|
588
|
+
`\`${config.langTemplates.join("`, `")}\` under ` +
|
|
589
|
+
`${config.rootDir}.`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let literals = 0;
|
|
594
|
+
let broken = 0;
|
|
595
|
+
for (const file of templates) {
|
|
596
|
+
literals += reportFindings(
|
|
597
|
+
findHardcodedText(file.text, { allow: config.langAllow }),
|
|
598
|
+
{ file: file.path },
|
|
599
|
+
);
|
|
600
|
+
broken += reportFindings(findTemplateSyntaxErrors(file.text), {
|
|
601
|
+
file: file.path,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (literals || broken) {
|
|
606
|
+
if (literals) {
|
|
607
|
+
console.error(
|
|
608
|
+
`\npackage-build: ${literals} user-visible literal(s) are not ` +
|
|
609
|
+
`localized. Replace each with a {{localize}} call and add ` +
|
|
610
|
+
`the key, or record it in \`packageBuild.lang.allow\` with ` +
|
|
611
|
+
`the reason it is not prose.`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (broken) {
|
|
615
|
+
console.error(
|
|
616
|
+
`\npackage-build: ${broken} template(s) do not compile. A ` +
|
|
617
|
+
`{{localize …}} nested inside another mustache is legal in ` +
|
|
618
|
+
`an HTML attribute but a parse error inside a helper's ` +
|
|
619
|
+
`hash — use a (localize …) subexpression there.`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(
|
|
626
|
+
`package-build: ${templates.length} template(s) fully localized and ` +
|
|
627
|
+
`compiling.`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* `lang <action>` — the three localization guards.
|
|
633
|
+
*
|
|
634
|
+
* They are three questions about one subject, and each is blind to what the
|
|
635
|
+
* others see: `check` asks whether the file will load at all, `coverage`
|
|
636
|
+
* whether the keys and the code agree, `hardcoded` whether the markup ever
|
|
637
|
+
* asks for a key in the first place.
|
|
638
|
+
*
|
|
300
639
|
* @returns {object} The yargs command module.
|
|
301
640
|
*/
|
|
302
641
|
function langCommand() {
|
|
@@ -304,39 +643,25 @@ function langCommand() {
|
|
|
304
643
|
command: "lang <action>",
|
|
305
644
|
describe: "Localization checks",
|
|
306
645
|
builder: (y) =>
|
|
307
|
-
y
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
646
|
+
y
|
|
647
|
+
.positional("action", {
|
|
648
|
+
choices: ["check", "coverage", "hardcoded"],
|
|
649
|
+
describe:
|
|
650
|
+
"check: the files are expandObject-safe · " +
|
|
651
|
+
"coverage: keys and code agree · " +
|
|
652
|
+
"hardcoded: templates localize their text",
|
|
653
|
+
})
|
|
654
|
+
.option("unused", {
|
|
655
|
+
type: "boolean",
|
|
656
|
+
default: false,
|
|
657
|
+
describe:
|
|
658
|
+
"coverage: list every unreferenced key, not a preview",
|
|
659
|
+
}),
|
|
660
|
+
handler: handler(async (args) => {
|
|
312
661
|
const config = loadPackageBuildConfig();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
317
|
-
if (!files.length) {
|
|
318
|
-
die(
|
|
319
|
-
`no localization files matched ` +
|
|
320
|
-
`\`${config.langSources}\` under ${config.rootDir}.`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
let total = 0;
|
|
325
|
-
for (const file of files.sort()) {
|
|
326
|
-
total += reportFindings(
|
|
327
|
-
validateLangSource(fs.readFileSync(file, "utf8")),
|
|
328
|
-
{ file: path.relative(config.rootDir, file) },
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (total) {
|
|
333
|
-
if (config.langHelp) console.error(`\n${config.langHelp}`);
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
console.log(
|
|
337
|
-
`package-build: ${files.length} localization file(s) are ` +
|
|
338
|
-
`expandObject-safe.`,
|
|
339
|
-
);
|
|
662
|
+
if (args.action === "check") return langCheck(config);
|
|
663
|
+
if (args.action === "hardcoded") return langHardcoded(config);
|
|
664
|
+
return langCoverage(config, args);
|
|
340
665
|
}),
|
|
341
666
|
};
|
|
342
667
|
}
|
|
@@ -457,17 +782,7 @@ function deployCommand() {
|
|
|
457
782
|
handler: handler(async (args) => {
|
|
458
783
|
const config = loadPackageBuildConfig();
|
|
459
784
|
|
|
460
|
-
|
|
461
|
-
// a repository that has no environment file at all.
|
|
462
|
-
const dotenv = await import("dotenv");
|
|
463
|
-
dotenv.config({
|
|
464
|
-
path: path.join(config.rootDir, ".env.local"),
|
|
465
|
-
quiet: true,
|
|
466
|
-
});
|
|
467
|
-
dotenv.config({
|
|
468
|
-
path: path.join(config.rootDir, ".env"),
|
|
469
|
-
quiet: true,
|
|
470
|
-
});
|
|
785
|
+
await loadEnvironment(config);
|
|
471
786
|
|
|
472
787
|
const { stage } = await deployStage({
|
|
473
788
|
stage: args.stage,
|
|
@@ -482,6 +797,122 @@ function deployCommand() {
|
|
|
482
797
|
};
|
|
483
798
|
}
|
|
484
799
|
|
|
800
|
+
/**
|
|
801
|
+
* `container <stage> <action>` — run a stage's Foundry in a container.
|
|
802
|
+
*
|
|
803
|
+
* The seam is the deploy's own: `deploy <stage>` installs the staged package
|
|
804
|
+
* into `FOUNDRYVTT_<STAGE>_DATA`, and this mounts that same directory and
|
|
805
|
+
* serves it. Nothing about the destination is stated twice.
|
|
806
|
+
*
|
|
807
|
+
* Every action shares one shape — a stage and an action — so this is a single
|
|
808
|
+
* command with a closed set of choices rather than eight of them. The stage
|
|
809
|
+
* leads, as it does in `deploy <stage>`, which is also what lets a consumer
|
|
810
|
+
* wrap it once per stage: `npm run container:dev start`.
|
|
811
|
+
*
|
|
812
|
+
* @returns {object} The yargs command module.
|
|
813
|
+
*/
|
|
814
|
+
function containerCommand() {
|
|
815
|
+
return {
|
|
816
|
+
command: "container <stage> <action>",
|
|
817
|
+
describe: "Run a stage's Foundry in a container",
|
|
818
|
+
builder: (y) =>
|
|
819
|
+
y
|
|
820
|
+
.positional("stage", {
|
|
821
|
+
type: "string",
|
|
822
|
+
describe: "Target stage (e.g. dev, qa, prod, test)",
|
|
823
|
+
})
|
|
824
|
+
.positional("action", {
|
|
825
|
+
choices: [...CONTAINER_ACTIONS],
|
|
826
|
+
describe: "What to do with the stage's container",
|
|
827
|
+
}),
|
|
828
|
+
handler: handler(async (args) => {
|
|
829
|
+
const config = loadPackageBuildConfig();
|
|
830
|
+
await loadEnvironment(config);
|
|
831
|
+
const status = containerAction({
|
|
832
|
+
action: String(args.action),
|
|
833
|
+
stage: String(args.stage).trim().toLowerCase(),
|
|
834
|
+
config,
|
|
835
|
+
log: (message) => console.log(message),
|
|
836
|
+
});
|
|
837
|
+
process.exit(status);
|
|
838
|
+
}),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** What `package-build e2e` can be asked to do. */
|
|
843
|
+
const E2E_ACTIONS = ["seed", ...E2E_MODES, "fast", "sweep"];
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* `e2e <action>` — stand a Foundry world up and drive a suite against it.
|
|
847
|
+
*
|
|
848
|
+
* **The suite itself is never this package's.** Seeding a disposable world,
|
|
849
|
+
* waiting for it to become *active* rather than merely reachable, and tearing
|
|
850
|
+
* it down again are nobody's local problem; what runs against it is entirely
|
|
851
|
+
* the repository's, and is named in `packageBuild.e2e.suite`.
|
|
852
|
+
*
|
|
853
|
+
* The actions answer different questions. `seed` writes the world.
|
|
854
|
+
* `run` and `open` are the from-scratch path — deploy, reseed, recreate, wait,
|
|
855
|
+
* run — and are the only ones that may change Foundry build, because a seeded
|
|
856
|
+
* world is stamped with the build that created it. `fast` is the iteration
|
|
857
|
+
* loop. `sweep` is the same full run against a build the repository does not
|
|
858
|
+
* pin, so `compatibility.verified` can be evidence rather than hope.
|
|
859
|
+
*
|
|
860
|
+
* Everything after the action is the suite's, and is passed through untouched.
|
|
861
|
+
*
|
|
862
|
+
* @returns {object} The yargs command module.
|
|
863
|
+
*/
|
|
864
|
+
function e2eCommand() {
|
|
865
|
+
return {
|
|
866
|
+
command: "e2e <action>",
|
|
867
|
+
describe: "Run a suite against a served Foundry world",
|
|
868
|
+
builder: (y) =>
|
|
869
|
+
y
|
|
870
|
+
.positional("action", {
|
|
871
|
+
choices: E2E_ACTIONS,
|
|
872
|
+
describe:
|
|
873
|
+
"seed: write the world · run/open: from scratch · " +
|
|
874
|
+
"fast: rebuild and re-run · sweep: another Foundry build",
|
|
875
|
+
})
|
|
876
|
+
// Everything after the action belongs to the suite or to the
|
|
877
|
+
// fast loop, so this command line must not judge it.
|
|
878
|
+
.strict(false),
|
|
879
|
+
handler: handler(async (args) => {
|
|
880
|
+
const action = String(args.action);
|
|
881
|
+
const config = loadPackageBuildConfig();
|
|
882
|
+
await loadEnvironment(config);
|
|
883
|
+
const log = (message) => console.log(message);
|
|
884
|
+
const tail = e2eTail(action);
|
|
885
|
+
|
|
886
|
+
if (action === "seed") {
|
|
887
|
+
await seedTestWorld({
|
|
888
|
+
config,
|
|
889
|
+
packageJson: readPackageJson(config),
|
|
890
|
+
log,
|
|
891
|
+
});
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const status =
|
|
896
|
+
action === "fast" ? await e2eFast({ config, argv: tail, log })
|
|
897
|
+
: action === "sweep" ?
|
|
898
|
+
await e2eSweep({
|
|
899
|
+
config,
|
|
900
|
+
packageJson: readPackageJson(config),
|
|
901
|
+
argv: tail,
|
|
902
|
+
log,
|
|
903
|
+
})
|
|
904
|
+
: await e2eRun({
|
|
905
|
+
config,
|
|
906
|
+
packageJson: readPackageJson(config),
|
|
907
|
+
mode: /** @type {"run"|"open"} */ (action),
|
|
908
|
+
suiteArgs: tail,
|
|
909
|
+
log,
|
|
910
|
+
});
|
|
911
|
+
process.exit(status);
|
|
912
|
+
}),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
485
916
|
yargs(hideBin(process.argv))
|
|
486
917
|
.scriptName("package-build")
|
|
487
918
|
.command(cleanCommand())
|
|
@@ -491,6 +922,8 @@ yargs(hideBin(process.argv))
|
|
|
491
922
|
.command(bundleCommand())
|
|
492
923
|
.command(releaseCommand())
|
|
493
924
|
.command(deployCommand())
|
|
925
|
+
.command(containerCommand())
|
|
926
|
+
.command(e2eCommand())
|
|
494
927
|
.demandCommand(1, "Name a command.")
|
|
495
928
|
.strict()
|
|
496
929
|
.version(ownVersion())
|
package/bin/report.mjs
CHANGED
|
@@ -42,6 +42,9 @@ import { emitDiagnostic } from "@heroiclands/content-build/engine/diagnostics";
|
|
|
42
42
|
*
|
|
43
43
|
* @typedef {object} Finding
|
|
44
44
|
* @property {string} message What is wrong, in one sentence.
|
|
45
|
+
* @property {string} [file] The file it is about, when the
|
|
46
|
+
* rule knows — a rule spanning many files does, and one handed a single
|
|
47
|
+
* file's text does not.
|
|
45
48
|
* @property {"warning"|"error"} [severity] Defaults to `error`.
|
|
46
49
|
* @property {number} [line] 1-based line, when known.
|
|
47
50
|
* @property {number} [column] 1-based column, when known.
|
|
@@ -59,11 +62,16 @@ import { emitDiagnostic } from "@heroiclands/content-build/engine/diagnostics";
|
|
|
59
62
|
* top of the file every time and reads exactly like a real position. A column
|
|
60
63
|
* without a line is dropped for the same reason: it locates nothing on its own.
|
|
61
64
|
*
|
|
65
|
+
* **Unless the finding names its own.** A rule handed one file's text cannot;
|
|
66
|
+
* a rule handed a whole repository — coverage, which compares the localization
|
|
67
|
+
* file against every source that references it — can name nothing else, since
|
|
68
|
+
* no single path is right for both halves of what it found.
|
|
69
|
+
*
|
|
62
70
|
* @param {Finding[]} findings - What the rule returned.
|
|
63
71
|
* @param {object} opts
|
|
64
|
-
* @param {string} opts.file - Path to the file, relative to the working
|
|
72
|
+
* @param {string} [opts.file] - Path to the file, relative to the working
|
|
65
73
|
* directory, so the emitted line is one an editor or `cc`-style parser can
|
|
66
|
-
* open.
|
|
74
|
+
* open. Optional only for a rule whose every finding carries its own.
|
|
67
75
|
* @returns {Array<{file: string, line?: number, column?: number,
|
|
68
76
|
* severity: "warning"|"error", message: string}>} The diagnostics, in the
|
|
69
77
|
* order the rule reported them.
|
|
@@ -74,7 +82,7 @@ export function toDiagnostics(findings, { file }) {
|
|
|
74
82
|
const hasColumn =
|
|
75
83
|
finding.column !== undefined && finding.column !== null;
|
|
76
84
|
return {
|
|
77
|
-
file,
|
|
85
|
+
file: finding.file ?? file,
|
|
78
86
|
...(hasLine ? { line: finding.line } : {}),
|
|
79
87
|
// Only alongside a line: `formatLocator` ignores a lone column, and
|
|
80
88
|
// carrying it anyway would invite a reader to trust it.
|
|
@@ -93,7 +101,8 @@ export function toDiagnostics(findings, { file }) {
|
|
|
93
101
|
*
|
|
94
102
|
* @param {Finding[]} findings - What the rule returned.
|
|
95
103
|
* @param {object} opts
|
|
96
|
-
* @param {string} opts.file - Path to the file, relative to the working dir.
|
|
104
|
+
* @param {string} [opts.file] - Path to the file, relative to the working dir.
|
|
105
|
+
* Optional only when every finding carries its own.
|
|
97
106
|
* @param {(d: object) => void} [opts.emit] - How to emit one diagnostic.
|
|
98
107
|
* Injectable so a test can capture the emitted shape without reaching for
|
|
99
108
|
* the console; defaults to content-build's `emitDiagnostic`, which writes
|