@hominis/fireforge 0.50.0 → 0.51.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/CHANGELOG.md +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Validation for `fireforge.json#patchLint.tokenScales`.
|
|
4
|
+
*
|
|
5
|
+
* Unknown keys are refused: a misspelt `allowInCalc` or `minFiles` would
|
|
6
|
+
* otherwise loosen or drop a scale without a word.
|
|
4
7
|
*/
|
|
5
8
|
import { ConfigError } from '../errors/config.js';
|
|
6
9
|
import { parseObject } from '../utils/parse.js';
|
|
10
|
+
const SCALE_KEYS = [
|
|
11
|
+
'properties',
|
|
12
|
+
'prefix',
|
|
13
|
+
'allowLiterals',
|
|
14
|
+
'allow',
|
|
15
|
+
'allowInCalc',
|
|
16
|
+
'offsetCap',
|
|
17
|
+
'exclude',
|
|
18
|
+
'minFiles',
|
|
19
|
+
];
|
|
20
|
+
const PREFIX_PATTERN = /^--[\w-]+$/;
|
|
7
21
|
function parseNames(raw, label, required) {
|
|
8
22
|
if (raw === undefined && !required)
|
|
9
23
|
return undefined;
|
|
@@ -14,6 +28,21 @@ function parseNames(raw, label, required) {
|
|
|
14
28
|
}
|
|
15
29
|
return raw;
|
|
16
30
|
}
|
|
31
|
+
function parsePrefix(raw, label) {
|
|
32
|
+
const prefixes = Array.isArray(raw) ? raw : [raw];
|
|
33
|
+
if (prefixes.length === 0 ||
|
|
34
|
+
prefixes.some((prefix) => typeof prefix !== 'string' || !PREFIX_PATTERN.test(prefix))) {
|
|
35
|
+
throw new ConfigError(`Config field "${label}.prefix" must be a custom-property prefix such as "--mybrowser-space-", or a non-empty list of them`);
|
|
36
|
+
}
|
|
37
|
+
return Array.isArray(raw) ? prefixes : raw;
|
|
38
|
+
}
|
|
39
|
+
function parseNumber(raw, label, integer) {
|
|
40
|
+
const valid = typeof raw === 'number' && Number.isFinite(raw) && (integer ? Number.isInteger(raw) : true);
|
|
41
|
+
if (!valid || raw < (integer ? 1 : 0)) {
|
|
42
|
+
throw new ConfigError(`Config field "${label}" must be ${integer ? 'a positive integer' : 'a non-negative number'}`);
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
17
46
|
function parseTokenScale(raw, label) {
|
|
18
47
|
let rec;
|
|
19
48
|
try {
|
|
@@ -22,12 +51,13 @@ function parseTokenScale(raw, label) {
|
|
|
22
51
|
catch {
|
|
23
52
|
throw new ConfigError(`Config field "${label}" must be an object`);
|
|
24
53
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
for (const key of Object.keys(raw)) {
|
|
55
|
+
if (!SCALE_KEYS.includes(key)) {
|
|
56
|
+
throw new ConfigError(`Config field "${label}" has unknown key "${key}"`);
|
|
57
|
+
}
|
|
29
58
|
}
|
|
30
|
-
const
|
|
59
|
+
const properties = parseNames(rec.raw('properties'), `${label}.properties`, true) ?? [];
|
|
60
|
+
const scale = { properties, prefix: parsePrefix(rec.raw('prefix'), label) };
|
|
31
61
|
const allowLiterals = parseNames(rec.raw('allowLiterals'), `${label}.allowLiterals`, false);
|
|
32
62
|
if (allowLiterals !== undefined)
|
|
33
63
|
scale.allowLiterals = allowLiterals;
|
|
@@ -39,6 +69,18 @@ function parseTokenScale(raw, label) {
|
|
|
39
69
|
}
|
|
40
70
|
scale.allow = allow;
|
|
41
71
|
}
|
|
72
|
+
const allowInCalc = parseNames(rec.raw('allowInCalc'), `${label}.allowInCalc`, false);
|
|
73
|
+
if (allowInCalc !== undefined)
|
|
74
|
+
scale.allowInCalc = allowInCalc;
|
|
75
|
+
const offsetCap = rec.raw('offsetCap');
|
|
76
|
+
if (offsetCap !== undefined)
|
|
77
|
+
scale.offsetCap = parseNumber(offsetCap, `${label}.offsetCap`, false);
|
|
78
|
+
const exclude = parseNames(rec.raw('exclude'), `${label}.exclude`, false);
|
|
79
|
+
if (exclude !== undefined)
|
|
80
|
+
scale.exclude = exclude;
|
|
81
|
+
const minFiles = rec.raw('minFiles');
|
|
82
|
+
if (minFiles !== undefined)
|
|
83
|
+
scale.minFiles = parseNumber(minFiles, `${label}.minFiles`, true);
|
|
42
84
|
return scale;
|
|
43
85
|
}
|
|
44
86
|
/** Parses and validates `patchLint.tokenScales`. */
|
|
@@ -471,6 +471,12 @@ function parseTypecheckBlock(rec) {
|
|
|
471
471
|
if (undefinedIdentifiers !== undefined) {
|
|
472
472
|
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
473
473
|
}
|
|
474
|
+
const litTypes = rec.raw('litTypes');
|
|
475
|
+
if (litTypes !== undefined && typeof litTypes !== 'boolean') {
|
|
476
|
+
throw new ConfigError('Config field "typecheck.litTypes" must be a boolean');
|
|
477
|
+
}
|
|
478
|
+
if (litTypes !== undefined)
|
|
479
|
+
out.litTypes = litTypes;
|
|
474
480
|
return out;
|
|
475
481
|
}
|
|
476
482
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { toError } from '../utils/errors.js';
|
|
15
15
|
import { verbose } from '../utils/logger.js';
|
|
16
|
-
import { collectStagingInputs, createCoverageRecord, findChangedInputs, hashEngineFile, isUnderPath, narrowAnchor, normalizeRequestPath, readEngineContentState, refreshAnchor, } from './coverage-records.js';
|
|
16
|
+
import { collectStagingInputs, createCoverageRecord, findChangedInputs, hashEngineFile, isUnderPath, narrowAnchor, normalizeRequestPath, readEngineContentState, refreshAnchor, widenWithRestagedManifests, } from './coverage-records.js';
|
|
17
17
|
export { hashEngineFile } from './coverage-records.js';
|
|
18
18
|
/** Filename of the generated mozconfig, relative to the engine directory. */
|
|
19
19
|
const MOZCONFIG_FILENAME = 'mozconfig';
|
|
@@ -96,7 +96,7 @@ export async function planCoverageRetention(engineDir, previous, requested) {
|
|
|
96
96
|
if (previous === undefined)
|
|
97
97
|
return emptyPlan();
|
|
98
98
|
const coverage = previous.testPackagingCoverage ?? 'full';
|
|
99
|
-
const rebuilt = requested.map(normalizeRequestPath).filter((path) => path !== '');
|
|
99
|
+
const rebuilt = await widenWithRestagedManifests(engineDir, requested.map(normalizeRequestPath).filter((path) => path !== ''));
|
|
100
100
|
let state;
|
|
101
101
|
try {
|
|
102
102
|
state = await readEngineContentState(engineDir);
|
|
@@ -67,6 +67,17 @@ export declare function findChangedInputs(state: EngineContentState, anchor: Cov
|
|
|
67
67
|
* cannot be read or the anchor cannot be compared.
|
|
68
68
|
*/
|
|
69
69
|
export declare function refreshAnchor<T extends CoverageAnchor>(state: EngineContentState, anchor: T, rebuilt: readonly string[]): Promise<T | undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* The request paths a scoped build restages, widened by the test manifests
|
|
72
|
+
* of every requested file: packaging one file of a manifest re-processes
|
|
73
|
+
* the manifest itself, so a manifest that gained an entry for that file is
|
|
74
|
+
* restaged too, and must not count as a changed input of the manifest's
|
|
75
|
+
* other files afterwards.
|
|
76
|
+
*
|
|
77
|
+
* @param engineDir - Path to the engine directory
|
|
78
|
+
* @param requested - Normalized request paths of the build
|
|
79
|
+
*/
|
|
80
|
+
export declare function widenWithRestagedManifests(engineDir: string, requested: readonly string[]): Promise<string[]>;
|
|
70
81
|
/**
|
|
71
82
|
* Engine-relative paths a manifest names outside its own directory. Bare
|
|
72
83
|
* names stay inside it. `!/` and `/` prefixes are engine-root relative, as
|
|
@@ -217,6 +217,40 @@ async function findManifestDirectory(engineDir, dir) {
|
|
|
217
217
|
return undefined;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* The request paths a scoped build restages, widened by the test manifests
|
|
222
|
+
* of every requested file: packaging one file of a manifest re-processes
|
|
223
|
+
* the manifest itself, so a manifest that gained an entry for that file is
|
|
224
|
+
* restaged too, and must not count as a changed input of the manifest's
|
|
225
|
+
* other files afterwards.
|
|
226
|
+
*
|
|
227
|
+
* @param engineDir - Path to the engine directory
|
|
228
|
+
* @param requested - Normalized request paths of the build
|
|
229
|
+
*/
|
|
230
|
+
export async function widenWithRestagedManifests(engineDir, requested) {
|
|
231
|
+
const widened = new Set(requested);
|
|
232
|
+
for (const path of requested) {
|
|
233
|
+
if (!TEST_FILE_PATTERN.test(posix.basename(path)))
|
|
234
|
+
continue;
|
|
235
|
+
const dir = await findManifestDirectory(engineDir, posix.dirname(path));
|
|
236
|
+
if (dir === undefined)
|
|
237
|
+
continue;
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = await readdir(join(engineDir, dir));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (TEST_MANIFEST_BASENAMES.has(entry))
|
|
247
|
+
widened.add(dir === '' ? entry : `${dir}/${entry}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...widened];
|
|
251
|
+
}
|
|
252
|
+
/** A test implementation file (`browser_*.js`, `test_*.js`, …), not a directory. */
|
|
253
|
+
const TEST_FILE_PATTERN = /^(?:browser|test)_.*\.m?js$/;
|
|
220
254
|
/** Test manifests at or beneath `dir`, bounded by {@link MAX_WALK_ENTRIES}. */
|
|
221
255
|
async function findManifestsBeneath(engineDir, dir) {
|
|
222
256
|
const manifests = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LockMode } from './file-lock.js';
|
|
1
2
|
import { type LockStatusSnapshot } from './file-lock.js';
|
|
2
3
|
export interface EngineSessionLockOptions {
|
|
3
4
|
/**
|
|
@@ -7,6 +8,11 @@ export interface EngineSessionLockOptions {
|
|
|
7
8
|
* fail-fast behaviour.
|
|
8
9
|
*/
|
|
9
10
|
waitLockSeconds?: number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* `shared` lets build-less test runs overlap each other; see
|
|
13
|
+
* `file-lock-shared.ts`. Every other caller holds the lock exclusively.
|
|
14
|
+
*/
|
|
15
|
+
mode?: LockMode | undefined;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* Formats the notice printed when the wait budget is extended because the
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { InconclusiveVerdictError } from '../errors/base.js';
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
5
|
import { info, warn } from '../utils/logger.js';
|
|
6
|
+
import { MAX_WAIT_LOCK_SECONDS } from '../utils/options.js';
|
|
6
7
|
import { sleep } from '../utils/sleep.js';
|
|
7
8
|
import { formatLockQueuePosition, readLockStatus, withFileLock, } from './file-lock.js';
|
|
8
9
|
import { git } from './git-base.js';
|
|
@@ -14,7 +15,7 @@ const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
|
14
15
|
* never gets one longer than the largest wait they could have asked for.
|
|
15
16
|
*/
|
|
16
17
|
const WAIT_EXTENSION_FACTOR = 4;
|
|
17
|
-
const MAX_WAIT_LOCK_MS =
|
|
18
|
+
const MAX_WAIT_LOCK_MS = MAX_WAIT_LOCK_SECONDS * 1000;
|
|
18
19
|
/**
|
|
19
20
|
* Formats the periodic operator-facing waiting line printed while `--wait-lock`
|
|
20
21
|
* polls a contended engine session lock. The holder identification comes from
|
|
@@ -66,6 +67,7 @@ export async function withEngineSessionLock(projectRoot, command, operation, opt
|
|
|
66
67
|
}
|
|
67
68
|
const { waitLockSeconds } = options;
|
|
68
69
|
return withFileLock(join(projectRoot, ENGINE_SESSION_LOCK_PATH), operation, {
|
|
70
|
+
...(options.mode === 'shared' ? { mode: 'shared' } : {}),
|
|
69
71
|
timeoutMs: waitLockSeconds !== undefined ? waitLockSeconds * 1000 : 1000,
|
|
70
72
|
...(waitLockSeconds !== undefined
|
|
71
73
|
? {
|
|
@@ -121,8 +123,13 @@ export async function readEngineSessionLockStatus(projectRoot) {
|
|
|
121
123
|
* Pure, so the wording is unit-testable without a lock on disk.
|
|
122
124
|
*/
|
|
123
125
|
export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
126
|
+
const shared = snapshot.sharedHolders !== undefined && snapshot.sharedHolders > 0
|
|
127
|
+
? `Shared holders: ${snapshot.sharedHolders} build-less test run(s) under a shared lock.`
|
|
128
|
+
: undefined;
|
|
124
129
|
if (!snapshot.held) {
|
|
125
|
-
return
|
|
130
|
+
return shared === undefined
|
|
131
|
+
? ['Engine session lock: free (no FireForge engine-mutating command is running).']
|
|
132
|
+
: ['Engine session lock: no exclusive holder.', shared];
|
|
126
133
|
}
|
|
127
134
|
const elapsed = snapshot.heldForMs === undefined
|
|
128
135
|
? 'unknown duration'
|
|
@@ -135,6 +142,7 @@ export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
|
135
142
|
const lines = [
|
|
136
143
|
`Engine session lock: ${identity}, for ${elapsed}.`,
|
|
137
144
|
`Queue depth: ${snapshot.queueDepth} waiter(s).`,
|
|
145
|
+
...(shared === undefined ? [] : [shared]),
|
|
138
146
|
];
|
|
139
147
|
// Liveness answers "does the process exist". It does not answer "is it
|
|
140
148
|
// getting anywhere". A holder alive for minutes having used a fraction of
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared holds on a FireForge directory lock.
|
|
3
|
+
*
|
|
4
|
+
* Every lock used to be exclusive, so two build-less `fireforge test` runs
|
|
5
|
+
* on one objdir, each with its own profile and (under auto ports) its own
|
|
6
|
+
* ports, still waited for each other: the gate's browser tiers ran strictly
|
|
7
|
+
* serially on a 14-core host. A shared hold lets such runs overlap while
|
|
8
|
+
* anything that writes (a build, a re-export, a test run that builds)
|
|
9
|
+
* still holds the lock alone.
|
|
10
|
+
*
|
|
11
|
+
* The scheme reuses the exclusive lock as a turnstile, so arrival order and
|
|
12
|
+
* stale-holder reaping come from the machinery that already exists:
|
|
13
|
+
*
|
|
14
|
+
* - A shared holder takes the exclusive lock in the ordinary way, records
|
|
15
|
+
* itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
|
|
16
|
+
* own), and releases the exclusive lock at once.
|
|
17
|
+
* - An exclusive holder takes the exclusive lock, then waits until no live
|
|
18
|
+
* shared holder is left. While it waits it already holds the turnstile,
|
|
19
|
+
* so no new shared holder can slip in ahead of it: a writer queued
|
|
20
|
+
* behind readers is served once they finish, never starved by newcomers.
|
|
21
|
+
*
|
|
22
|
+
* A shared holder whose process died is reaped by the same PID-first
|
|
23
|
+
* liveness check the exclusive lock uses.
|
|
24
|
+
*/
|
|
25
|
+
/** How a caller holds a lock. */
|
|
26
|
+
export type LockMode = 'exclusive' | 'shared';
|
|
27
|
+
/** The directory holding a lock's shared-holder records. */
|
|
28
|
+
export declare function sharedHoldersDir(lockPath: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Counts the live shared holders of a lock, reaping any whose process is
|
|
31
|
+
* gone. A missing directory means none.
|
|
32
|
+
*
|
|
33
|
+
* @param lockPath - The lock directory
|
|
34
|
+
* @param options - `reap: false` only counts (status reads)
|
|
35
|
+
*/
|
|
36
|
+
export declare function countLiveSharedHolders(lockPath: string, options?: {
|
|
37
|
+
reap?: boolean;
|
|
38
|
+
}): Promise<number>;
|
|
39
|
+
/** A registered shared hold. */
|
|
40
|
+
export interface SharedHold {
|
|
41
|
+
/** Removes the hold. Never throws. */
|
|
42
|
+
release(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Records a shared hold. Call while holding the lock's exclusive turnstile.
|
|
46
|
+
*
|
|
47
|
+
* @param lockPath - The lock directory
|
|
48
|
+
* @param metadata - Owner metadata lines (`command=…`)
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerSharedHold(lockPath: string, metadata: readonly string[] | undefined): Promise<SharedHold>;
|
|
51
|
+
/**
|
|
52
|
+
* Removes every shared hold this process registered, for the signal path.
|
|
53
|
+
* Never throws.
|
|
54
|
+
*
|
|
55
|
+
* @returns The holder paths removed
|
|
56
|
+
*/
|
|
57
|
+
export declare function releaseSharedHoldsForSignal(): Promise<string[]>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared holds on a FireForge directory lock.
|
|
4
|
+
*
|
|
5
|
+
* Every lock used to be exclusive, so two build-less `fireforge test` runs
|
|
6
|
+
* on one objdir, each with its own profile and (under auto ports) its own
|
|
7
|
+
* ports, still waited for each other: the gate's browser tiers ran strictly
|
|
8
|
+
* serially on a 14-core host. A shared hold lets such runs overlap while
|
|
9
|
+
* anything that writes (a build, a re-export, a test run that builds)
|
|
10
|
+
* still holds the lock alone.
|
|
11
|
+
*
|
|
12
|
+
* The scheme reuses the exclusive lock as a turnstile, so arrival order and
|
|
13
|
+
* stale-holder reaping come from the machinery that already exists:
|
|
14
|
+
*
|
|
15
|
+
* - A shared holder takes the exclusive lock in the ordinary way, records
|
|
16
|
+
* itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
|
|
17
|
+
* own), and releases the exclusive lock at once.
|
|
18
|
+
* - An exclusive holder takes the exclusive lock, then waits until no live
|
|
19
|
+
* shared holder is left. While it waits it already holds the turnstile,
|
|
20
|
+
* so no new shared holder can slip in ahead of it: a writer queued
|
|
21
|
+
* behind readers is served once they finish, never starved by newcomers.
|
|
22
|
+
*
|
|
23
|
+
* A shared holder whose process died is reaped by the same PID-first
|
|
24
|
+
* liveness check the exclusive lock uses.
|
|
25
|
+
*/
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { mkdir, readdir, rm } from 'node:fs/promises';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
30
|
+
import { verbose } from '../utils/logger.js';
|
|
31
|
+
import { isLockOwnerAlive, readLockOwner, writeLockOwner } from './file-lock-owner.js';
|
|
32
|
+
/** The directory holding a lock's shared-holder records. */
|
|
33
|
+
export function sharedHoldersDir(lockPath) {
|
|
34
|
+
return `${lockPath}.shared`;
|
|
35
|
+
}
|
|
36
|
+
/** Holder directories this process registered, for the signal sweep. */
|
|
37
|
+
const heldShares = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Counts the live shared holders of a lock, reaping any whose process is
|
|
40
|
+
* gone. A missing directory means none.
|
|
41
|
+
*
|
|
42
|
+
* @param lockPath - The lock directory
|
|
43
|
+
* @param options - `reap: false` only counts (status reads)
|
|
44
|
+
*/
|
|
45
|
+
export async function countLiveSharedHolders(lockPath, options = {}) {
|
|
46
|
+
const dir = sharedHoldersDir(lockPath);
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
53
|
+
return 0;
|
|
54
|
+
if (options.reap === false)
|
|
55
|
+
return 0;
|
|
56
|
+
throw toError(error);
|
|
57
|
+
}
|
|
58
|
+
let live = 0;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const holder = join(dir, entry);
|
|
61
|
+
const owner = await readLockOwner(holder);
|
|
62
|
+
if (owner.present && isLockOwnerAlive(owner)) {
|
|
63
|
+
live += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// A record still being written has no owner yet; only an owner that
|
|
67
|
+
// names a dead process is reaped, so a racing registration survives.
|
|
68
|
+
if (owner.present && options.reap !== false) {
|
|
69
|
+
verbose(`Reaping shared hold ${holder}: PID ${owner.pid} is gone.`);
|
|
70
|
+
await rm(holder, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return live;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Records a shared hold. Call while holding the lock's exclusive turnstile.
|
|
77
|
+
*
|
|
78
|
+
* @param lockPath - The lock directory
|
|
79
|
+
* @param metadata - Owner metadata lines (`command=…`)
|
|
80
|
+
*/
|
|
81
|
+
export async function registerSharedHold(lockPath, metadata) {
|
|
82
|
+
const holder = join(sharedHoldersDir(lockPath), randomUUID());
|
|
83
|
+
await mkdir(holder, { recursive: true });
|
|
84
|
+
await writeLockOwner(holder, randomUUID(), [...(metadata ?? []), 'mode=shared']);
|
|
85
|
+
heldShares.add(holder);
|
|
86
|
+
return {
|
|
87
|
+
async release() {
|
|
88
|
+
heldShares.delete(holder);
|
|
89
|
+
try {
|
|
90
|
+
await rm(holder, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
verbose(`Could not remove shared hold ${holder}: ${toError(error).message}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Removes every shared hold this process registered, for the signal path.
|
|
100
|
+
* Never throws.
|
|
101
|
+
*
|
|
102
|
+
* @returns The holder paths removed
|
|
103
|
+
*/
|
|
104
|
+
export async function releaseSharedHoldsForSignal() {
|
|
105
|
+
const removed = [];
|
|
106
|
+
for (const holder of [...heldShares]) {
|
|
107
|
+
heldShares.delete(holder);
|
|
108
|
+
try {
|
|
109
|
+
await rm(holder, { recursive: true, force: true });
|
|
110
|
+
removed.push(holder);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Shutdown path: nothing to do.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return removed;
|
|
117
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LOCK_PID_FILE } from './file-lock-owner.js';
|
|
2
2
|
import { type LockQueueState } from './file-lock-queue.js';
|
|
3
|
+
import { type LockMode } from './file-lock-shared.js';
|
|
3
4
|
export { LOCK_PID_FILE };
|
|
4
5
|
export { formatLockQueuePosition, type LockQueueState } from './file-lock-queue.js';
|
|
6
|
+
export type { LockMode } from './file-lock-shared.js';
|
|
5
7
|
/**
|
|
6
8
|
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
7
9
|
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
@@ -84,6 +86,11 @@ export interface FileLockOptions {
|
|
|
84
86
|
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
85
87
|
/** Extra owner-file lines for human diagnostics. Lock mechanics ignore them. */
|
|
86
88
|
ownerMetadata?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* `exclusive` (default) or `shared`. Shared holders overlap each other and
|
|
91
|
+
* exclude exclusive ones; see `file-lock-shared.ts`.
|
|
92
|
+
*/
|
|
93
|
+
mode?: LockMode | undefined;
|
|
87
94
|
}
|
|
88
95
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
89
96
|
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
@@ -97,6 +104,8 @@ export interface LockStatusSnapshot {
|
|
|
97
104
|
/** Milliseconds since the lock directory was created, when held. */
|
|
98
105
|
heldForMs?: number | undefined;
|
|
99
106
|
queueDepth: number;
|
|
107
|
+
/** Live shared holders (build-less test runs under a shared lock). */
|
|
108
|
+
sharedHolders?: number | undefined;
|
|
100
109
|
}
|
|
101
110
|
/**
|
|
102
111
|
* Inspects a lock without acquiring it. Read-only and fail-open: a missing
|
|
@@ -9,6 +9,7 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
9
9
|
import { sleep } from '../utils/sleep.js';
|
|
10
10
|
import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
|
|
11
11
|
import { readLockQueue, registerWaiter, } from './file-lock-queue.js';
|
|
12
|
+
import { countLiveSharedHolders, registerSharedHold, releaseSharedHoldsForSignal, } from './file-lock-shared.js';
|
|
12
13
|
export { LOCK_PID_FILE };
|
|
13
14
|
export { formatLockQueuePosition } from './file-lock-queue.js';
|
|
14
15
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
@@ -38,6 +39,8 @@ export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
|
38
39
|
*/
|
|
39
40
|
export async function readLockStatus(lockPath) {
|
|
40
41
|
const queue = await readLockQueue(lockPath);
|
|
42
|
+
const shared = await countLiveSharedHolders(lockPath, { reap: false });
|
|
43
|
+
const sharedPart = shared > 0 ? { sharedHolders: shared } : {};
|
|
41
44
|
let heldForMs;
|
|
42
45
|
try {
|
|
43
46
|
// Clamped: filesystem mtime resolution can round a just-created lock
|
|
@@ -46,13 +49,13 @@ export async function readLockStatus(lockPath) {
|
|
|
46
49
|
heldForMs = Math.max(0, Date.now() - (await stat(lockPath)).mtimeMs);
|
|
47
50
|
}
|
|
48
51
|
catch {
|
|
49
|
-
return { held: false, queueDepth: queue.depth };
|
|
52
|
+
return { held: false, queueDepth: queue.depth, ...sharedPart };
|
|
50
53
|
}
|
|
51
54
|
const owner = await readLockOwner(lockPath);
|
|
52
55
|
const holder = owner.present
|
|
53
56
|
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
54
57
|
: undefined;
|
|
55
|
-
return { held: true, holder, heldForMs, queueDepth: queue.depth };
|
|
58
|
+
return { held: true, holder, heldForMs, queueDepth: queue.depth, ...sharedPart };
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
58
61
|
* Reads the contended lock's owner and queue in one go. One probe per cycle
|
|
@@ -334,6 +337,7 @@ export async function forceReleaseHeldLocksForSignal() {
|
|
|
334
337
|
removed.push(lockPath);
|
|
335
338
|
}
|
|
336
339
|
}
|
|
340
|
+
removed.push(...(await releaseSharedHoldsForSignal()));
|
|
337
341
|
return removed;
|
|
338
342
|
}
|
|
339
343
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
@@ -467,8 +471,36 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
467
471
|
// `writeLockOwner` removes the just-created directory before throwing.
|
|
468
472
|
const ownerToken = randomUUID();
|
|
469
473
|
await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
|
|
474
|
+
return runWhileHeld(lockPath, ownerToken, operation, options, {
|
|
475
|
+
deadline: Math.max(deadline, Date.now() + pollMs),
|
|
476
|
+
pollMs,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Runs `operation` once the turnstile is ours: as a shared holder (turnstile
|
|
481
|
+
* released at once), or exclusively once every shared holder has drained.
|
|
482
|
+
*/
|
|
483
|
+
async function runWhileHeld(lockPath, ownerToken, operation, options, drain) {
|
|
484
|
+
if (options.mode === 'shared') {
|
|
485
|
+
// The exclusive lock was only the turnstile: record the shared hold and
|
|
486
|
+
// let the next waiter in. See file-lock-shared.ts.
|
|
487
|
+
let hold;
|
|
488
|
+
try {
|
|
489
|
+
hold = await registerSharedHold(lockPath, options.ownerMetadata);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
await releaseLock(lockPath, ownerToken);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
return await operation();
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await hold.release();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
470
501
|
heldLocks.add(lockPath);
|
|
471
502
|
try {
|
|
503
|
+
await waitForSharedHoldersToDrain(lockPath, drain.deadline, drain.pollMs);
|
|
472
504
|
return await operation();
|
|
473
505
|
}
|
|
474
506
|
finally {
|
|
@@ -476,3 +508,27 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
476
508
|
await releaseLock(lockPath, ownerToken);
|
|
477
509
|
}
|
|
478
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Holds an exclusive acquisition until every live shared holder has gone.
|
|
513
|
+
* The turnstile is already ours, so no new shared holder can join meanwhile.
|
|
514
|
+
*
|
|
515
|
+
* @throws LockContentionError when shared holders outlast the budget
|
|
516
|
+
*/
|
|
517
|
+
async function waitForSharedHoldersToDrain(lockPath, deadline, pollMs) {
|
|
518
|
+
let announced = false;
|
|
519
|
+
for (;;) {
|
|
520
|
+
const live = await countLiveSharedHolders(lockPath);
|
|
521
|
+
if (live === 0)
|
|
522
|
+
return;
|
|
523
|
+
if (Date.now() >= deadline) {
|
|
524
|
+
throw new LockContentionError(`Timed out waiting for file lock ${lockPath}: ${live} shared holder(s) (build-less ` +
|
|
525
|
+
'test runs under a shared lock) still hold it. Wait for them to finish, or retry ' +
|
|
526
|
+
'with a larger --wait-lock.');
|
|
527
|
+
}
|
|
528
|
+
if (!announced) {
|
|
529
|
+
verbose(`Waiting for ${live} shared holder(s) of ${lockPath} to finish.`);
|
|
530
|
+
announced = true;
|
|
531
|
+
}
|
|
532
|
+
await sleep(Math.max(pollMs, 100));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -17,7 +17,8 @@ import { topologicalSortCustom } from './furnace-graph-utils.js';
|
|
|
17
17
|
import { resolveFurnaceMarkerComment } from './furnace-marker.js';
|
|
18
18
|
import { recordFurnaceRollbackFailure } from './furnace-operation.js';
|
|
19
19
|
import { assertFurnaceEngineDirReady } from './furnace-precondition.js';
|
|
20
|
-
import { addJarMnEntries, removeCustomElementRegistration
|
|
20
|
+
import { addJarMnEntries, removeCustomElementRegistration } from './furnace-registration.js';
|
|
21
|
+
import { removeJarMnFileEntries } from './furnace-registration-jar-inplace.js';
|
|
21
22
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from './furnace-rollback.js';
|
|
22
23
|
import { blockingStepErrors, hasBlockingStepErrors } from './furnace-step-errors.js';
|
|
23
24
|
import { runPostApplyConsistencyChecks } from './furnace-validate-registration.js';
|
|
@@ -132,8 +133,8 @@ async function applyOverrideBatch(ctx) {
|
|
|
132
133
|
/**
|
|
133
134
|
* After undeploying deleted files, the in-engine jar.mn and
|
|
134
135
|
* customElements.js still carry entries for the removed files. Re-sync them
|
|
135
|
-
* by removing
|
|
136
|
-
*
|
|
136
|
+
* by removing only the deleted files' jar.mn lines in place and adding any
|
|
137
|
+
* live file that has none. If the .mjs itself was deleted, also drop
|
|
137
138
|
* the customElements.js registration. Snapshots are taken under the same
|
|
138
139
|
* journal so the undo path is symmetric with apply.
|
|
139
140
|
*/
|
|
@@ -144,18 +145,17 @@ async function reconcileCustomRegistrationAfterUndeploy(args) {
|
|
|
144
145
|
const deletedRegistrationFiles = deletedFiles.filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
145
146
|
if (deletedRegistrationFiles.length === 0)
|
|
146
147
|
return;
|
|
147
|
-
// jar.mn re-sync
|
|
148
|
-
//
|
|
149
|
-
//
|
|
148
|
+
// jar.mn re-sync: drop only the deleted files' lines, so the rest of the
|
|
149
|
+
// widget's block keeps its order and the owning patch carries no reorder
|
|
150
|
+
// hunks. addJarMnEntries is idempotent, so the add pass inserts only a
|
|
151
|
+
// live file that has no line yet. The journal snapshot before the
|
|
152
|
+
// mutation gives us a clean rollback target.
|
|
150
153
|
if (rollbackJournal) {
|
|
151
154
|
await snapshotFile(rollbackJournal, join(engineDir, JAR_MN));
|
|
152
155
|
}
|
|
153
|
-
await
|
|
156
|
+
await removeJarMnFileEntries(engineDir, name, deletedRegistrationFiles);
|
|
154
157
|
const liveJarFiles = Object.keys(currentChecksums).filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
155
158
|
if (liveJarFiles.length > 0) {
|
|
156
|
-
// applyCustomComponent has already added live entries, and the remove
|
|
157
|
-
// above dropped them too, so re-add now to leave jar.mn in the correct
|
|
158
|
-
// state.
|
|
159
159
|
await addJarMnEntries(engineDir, name, liveJarFiles);
|
|
160
160
|
}
|
|
161
161
|
filesAffected.push(JAR_MN);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
|
-
* Parses `validationExceptions[]`: `{ component, check, reason }`
|
|
4
|
-
* no other keys, every field a non-empty string, and no two
|
|
5
|
-
* same component and
|
|
3
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
4
|
+
* objects, no other keys, every field a non-empty string, and no two
|
|
5
|
+
* entries for the same component, check and tag.
|
|
6
6
|
*/
|
|
7
7
|
export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
|
|
8
|
+
/**
|
|
9
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
10
|
+
* list of partner paths or globs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTokenSiblingSheets(raw: unknown): Record<string, string[]>;
|
|
13
|
+
/** Parses `minComponents`: a positive integer. */
|
|
14
|
+
export declare function parseMinComponents(raw: unknown): number;
|
|
8
15
|
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
9
16
|
export declare function parseOptInChecks(raw: unknown): FurnaceOptInCheck[];
|