@hominis/fireforge 0.46.2 → 0.47.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/config.js +1 -2
  4. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +59 -0
  5. package/dist/src/commands/furnace/deploy-fragment-includers.js +120 -0
  6. package/dist/src/commands/furnace/deploy.js +36 -3
  7. package/dist/src/commands/furnace/validation-output.d.ts +5 -0
  8. package/dist/src/commands/furnace/validation-output.js +35 -9
  9. package/dist/src/commands/patch/delete-gap-notice.d.ts +24 -0
  10. package/dist/src/commands/patch/delete-gap-notice.js +46 -0
  11. package/dist/src/commands/patch/delete.js +11 -0
  12. package/dist/src/commands/re-export-lint.d.ts +26 -1
  13. package/dist/src/commands/re-export-lint.js +60 -20
  14. package/dist/src/commands/re-export-scan.d.ts +8 -0
  15. package/dist/src/commands/re-export-scan.js +12 -2
  16. package/dist/src/commands/re-export-single.js +24 -5
  17. package/dist/src/commands/run.js +42 -17
  18. package/dist/src/commands/test-harness-env.d.ts +24 -0
  19. package/dist/src/commands/test-harness-env.js +61 -0
  20. package/dist/src/commands/test-register.js +6 -2
  21. package/dist/src/commands/test-verdict.d.ts +3 -1
  22. package/dist/src/commands/test-verdict.js +17 -2
  23. package/dist/src/commands/test.js +9 -16
  24. package/dist/src/core/build-jar-escalation.d.ts +1 -4
  25. package/dist/src/core/build-jar-escalation.js +1 -4
  26. package/dist/src/core/build-prepare.js +72 -55
  27. package/dist/src/core/firefox-cache.js +1 -2
  28. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  29. package/dist/src/core/furnace-apply-output.js +19 -4
  30. package/dist/src/core/furnace-css-fragments.d.ts +7 -0
  31. package/dist/src/core/furnace-css-fragments.js +12 -3
  32. package/dist/src/core/furnace-fragment-includers.d.ts +56 -0
  33. package/dist/src/core/furnace-fragment-includers.js +122 -0
  34. package/dist/src/core/git-diff-new-file.js +1 -2
  35. package/dist/src/core/git-file-ops.js +1 -2
  36. package/dist/src/core/git.js +1 -2
  37. package/dist/src/core/mach-build-artifacts.js +1 -2
  38. package/dist/src/core/mochitest-server-port.d.ts +43 -4
  39. package/dist/src/core/mochitest-server-port.js +64 -11
  40. package/dist/src/core/patch-lint-imports.js +4 -4
  41. package/dist/src/core/smoke-patterns.d.ts +24 -0
  42. package/dist/src/core/smoke-patterns.js +50 -5
  43. package/dist/src/types/commands/options.d.ts +6 -0
  44. package/docs/configuration.md +8 -0
  45. package/docs/exit-codes.md +19 -19
  46. package/docs/machine-output.md +22 -10
  47. package/docs/testing.md +13 -1
  48. package/package.json +5 -5
@@ -16,6 +16,7 @@ import { evaluateJarManifestEscalation, formatJarEscalationNotice, } from './bui
16
16
  import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
17
17
  import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
18
18
  import { applyAllComponents } from './furnace-apply.js';
19
+ import { logApplyWarnings } from './furnace-apply-output.js';
19
20
  import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
20
21
  import { runFurnaceMutation } from './furnace-operation.js';
21
22
  import { countEntriesWithBlockingStepErrors } from './furnace-step-errors.js';
@@ -114,15 +115,14 @@ function buildConfigureFailureError(captured) {
114
115
  *
115
116
  * @param engineDir - Absolute engine directory
116
117
  * @param changed - Build inputs changed since the last successful build
117
- * @param baseline - Last successful build's baseline
118
118
  */
119
- async function decideJarEscalation(engineDir, changed, baseline) {
119
+ async function decideJarEscalation(engineDir, changed) {
120
120
  // Packaging manifests whose graph/destination directories may need a full
121
121
  // build. Path-shape only: `evaluateJarManifestEscalation` decides whether a
122
122
  // given changed manifest actually forces one (a new manifest, or one that
123
123
  // redirects its install base directory), so an entry added to an existing
124
124
  // `dist/bin` manifest no longer costs a ~10-minute build.
125
- const decision = await evaluateJarManifestEscalation(engineDir, changed.filter(isJarManifestPath), baseline);
125
+ const decision = await evaluateJarManifestEscalation(engineDir, changed.filter(isJarManifestPath));
126
126
  if (decision.cleared.length > 0) {
127
127
  verbose(`jar escalation: ${decision.cleared.length} changed jar.mn add entries to an existing dist/bin manifest; not escalating for ${decision.cleared.join(', ')}.`);
128
128
  }
@@ -164,6 +164,73 @@ async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options
164
164
  }
165
165
  notice(message);
166
166
  }
167
+ /**
168
+ * Applies Furnace components before the build when `furnace.json` exists.
169
+ * Prints every warning the apply computed, then the loud sync banner.
170
+ *
171
+ * @param projectRoot - Project root
172
+ * @returns Number of components written to engine/
173
+ */
174
+ async function applyFurnaceBeforeBuild(projectRoot) {
175
+ if (!(await furnaceConfigExists(projectRoot)))
176
+ return 0;
177
+ const furnaceConfig = await loadFurnaceConfig(projectRoot);
178
+ const hasComponents = Object.keys(furnaceConfig.overrides).length > 0 || Object.keys(furnaceConfig.custom).length > 0;
179
+ if (!hasComponents)
180
+ return 0;
181
+ const furnaceSpinner = spinner('Applying Furnace components...');
182
+ let result;
183
+ try {
184
+ result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }));
185
+ }
186
+ catch (error) {
187
+ furnaceSpinner.error('Failed to apply Furnace components');
188
+ throw error;
189
+ }
190
+ const furnaceApplied = result.applied.length;
191
+ // Count entries that were "applied" but recorded step-level errors
192
+ // mid-apply (e.g. a post-step failure after file writes succeeded).
193
+ // These are distinct from `result.errors`, which captures
194
+ // components that failed before reaching the applied list at all.
195
+ // The sum of the two is the total count of failed components.
196
+ const appliedWithStepErrorsCount = countEntriesWithBlockingStepErrors(result.applied);
197
+ const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
198
+ if (totalApplyFailures > 0) {
199
+ furnaceSpinner.error('Failed to apply Furnace components');
200
+ for (const err of result.errors) {
201
+ warn(`Furnace: ${err.name} — ${err.error}`);
202
+ }
203
+ for (const applied of result.applied) {
204
+ if (applied.stepErrors && applied.stepErrors.length > 0) {
205
+ for (const stepErr of applied.stepErrors) {
206
+ warn(`Furnace: ${applied.name} [${stepErr.step}] ${stepErr.error}`);
207
+ }
208
+ }
209
+ }
210
+ throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
211
+ }
212
+ // The apply computed these (patch-owned engine files whose content
213
+ // differed from components/ and were replaced). `furnace apply`
214
+ // prints them; this path used to drop them, so a negative control
215
+ // that edited a deployed Furnace file ran against the shipped code
216
+ // with a green result and only a checksum could tell.
217
+ const overwriteCount = logApplyWarnings(result.warnings);
218
+ if (overwriteCount > 0) {
219
+ notice(`Furnace: ${overwriteCount} Furnace-managed engine file${overwriteCount === 1 ? '' : 's'} differed from components/ and ${overwriteCount === 1 ? 'was' : 'were'} REPLACED before this build. An engine-side edit to a Furnace-managed file (for example a negative control) did not run; edit the component source under components/ instead, or restore the edit after the build.`);
220
+ }
221
+ if (furnaceApplied > 0) {
222
+ const appliedNames = result.applied.map((entry) => entry.name).join(', ');
223
+ furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
224
+ // Loud banner: the build operator needs to see that engine/ was
225
+ // updated before this build, otherwise a silent re-apply is
226
+ // indistinguishable from a build that shipped stale components.
227
+ notice(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
228
+ }
229
+ else {
230
+ furnaceSpinner.stop('Components up to date');
231
+ }
232
+ return furnaceApplied;
233
+ }
167
234
  /**
168
235
  * Runs the shared pre-flight steps for build and package commands:
169
236
  * 1. Warns (or refuses) about unexported engine drift the writes below
@@ -202,7 +269,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
202
269
  if (options.previousBaseline) {
203
270
  const changed = await collectChangedBuildInputs(paths.engine, options.previousBaseline);
204
271
  const invalidating = changed.filter(isBackendInvalidatingFile);
205
- const jarEscalation = await decideJarEscalation(paths.engine, changed, options.previousBaseline);
272
+ const jarEscalation = await decideJarEscalation(paths.engine, changed);
206
273
  fullBuildRequired = jarEscalation !== undefined;
207
274
  fullBuildReason = jarEscalation;
208
275
  if (invalidating.length > 0) {
@@ -263,57 +330,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
263
330
  throw error;
264
331
  }
265
332
  }
266
- // Apply Furnace components if furnace.json exists
267
- let furnaceApplied = 0;
268
- if (await furnaceConfigExists(projectRoot)) {
269
- const furnaceConfig = await loadFurnaceConfig(projectRoot);
270
- const hasComponents = Object.keys(furnaceConfig.overrides).length > 0 ||
271
- Object.keys(furnaceConfig.custom).length > 0;
272
- if (hasComponents) {
273
- const furnaceSpinner = spinner('Applying Furnace components...');
274
- let result;
275
- try {
276
- result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }));
277
- }
278
- catch (error) {
279
- furnaceSpinner.error('Failed to apply Furnace components');
280
- throw error;
281
- }
282
- furnaceApplied = result.applied.length;
283
- // Count entries that were "applied" but recorded step-level errors
284
- // mid-apply (e.g. a post-step failure after file writes succeeded).
285
- // These are distinct from `result.errors`, which captures
286
- // components that failed before reaching the applied list at all.
287
- // The sum of the two is the total count of failed components.
288
- const appliedWithStepErrorsCount = countEntriesWithBlockingStepErrors(result.applied);
289
- const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
290
- if (totalApplyFailures > 0) {
291
- furnaceSpinner.error('Failed to apply Furnace components');
292
- for (const err of result.errors) {
293
- warn(`Furnace: ${err.name} — ${err.error}`);
294
- }
295
- for (const applied of result.applied) {
296
- if (applied.stepErrors && applied.stepErrors.length > 0) {
297
- for (const stepErr of applied.stepErrors) {
298
- warn(`Furnace: ${applied.name} [${stepErr.step}] ${stepErr.error}`);
299
- }
300
- }
301
- }
302
- throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
303
- }
304
- if (furnaceApplied > 0) {
305
- const appliedNames = result.applied.map((entry) => entry.name).join(', ');
306
- furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
307
- // Loud banner: the build operator needs to see that engine/ was
308
- // updated before this build, otherwise a silent re-apply is
309
- // indistinguishable from a build that shipped stale components.
310
- notice(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
311
- }
312
- else {
313
- furnaceSpinner.stop('Components up to date');
314
- }
315
- }
316
- }
333
+ const furnaceApplied = await applyFurnaceBeforeBuild(projectRoot);
317
334
  // Generate mozconfig
318
335
  const mozconfigSpinner = spinner('Generating mozconfig...');
319
336
  try {
@@ -242,8 +242,7 @@ async function removeArchivePartFiles(archive, cacheDir) {
242
242
  .filter((name) => name.startsWith(partPrefix))
243
243
  .map((name) => removeFile(join(cacheDir, name))));
244
244
  }
245
- catch (error) {
246
- void error;
245
+ catch {
247
246
  // Cache dir may not exist yet. That's fine.
248
247
  }
249
248
  }
@@ -13,4 +13,14 @@ type ApplyResultWithActions = ApplyResult & {
13
13
  * @param isDryRun - Whether apply was invoked with dryRun=true
14
14
  */
15
15
  export declare function logApplyResult(result: ApplyResultWithActions, isDryRun: boolean): void;
16
+ /**
17
+ * Prints the operator-facing apply warnings (patch-owned overwrites) one
18
+ * per line and returns how many were printed. Shared by `furnace apply`
19
+ * and the pre-build source → engine sync, so a warning the apply computed
20
+ * is never dropped on one path and printed on the other.
21
+ *
22
+ * @param warnings - `result.warnings` from applyAllComponents
23
+ * @returns Number of warning lines printed
24
+ */
25
+ export declare function logApplyWarnings(warnings: readonly string[] | undefined): number;
16
26
  export {};
@@ -55,12 +55,27 @@ export function logApplyResult(result, isDryRun) {
55
55
  // outcome, including the rolled-back branch, where the overwrite
56
56
  // happened before the rollback restored it and the operator still needs
57
57
  // to know the deployed copy was momentarily replaced.
58
- if (!isDryRun && result.warnings !== undefined) {
59
- for (const line of result.warnings) {
60
- warn(line);
61
- }
58
+ if (!isDryRun) {
59
+ logApplyWarnings(result.warnings);
62
60
  }
63
61
  for (const err of result.errors) {
64
62
  error(`${err.name} — ${err.error}`);
65
63
  }
66
64
  }
65
+ /**
66
+ * Prints the operator-facing apply warnings (patch-owned overwrites) one
67
+ * per line and returns how many were printed. Shared by `furnace apply`
68
+ * and the pre-build source → engine sync, so a warning the apply computed
69
+ * is never dropped on one path and printed on the other.
70
+ *
71
+ * @param warnings - `result.warnings` from applyAllComponents
72
+ * @returns Number of warning lines printed
73
+ */
74
+ export function logApplyWarnings(warnings) {
75
+ if (warnings === undefined)
76
+ return 0;
77
+ for (const line of warnings) {
78
+ warn(line);
79
+ }
80
+ return warnings.length;
81
+ }
@@ -20,6 +20,13 @@
20
20
  */
21
21
  import type { ValidationIssue } from '../types/furnace.js';
22
22
  export { SHARED_FRAGMENTS_DIR } from './furnace-config.js';
23
+ /**
24
+ * Reads a shared fragment as the exact body an expansion inlines: the
25
+ * trailing newline is dropped so the fenced block ends on the end marker.
26
+ * One definition, so every consumer (expansion, validation, the stale
27
+ * includer probe) compares the same bytes.
28
+ */
29
+ export declare function readFragmentSource(sharedDir: string, name: string): Promise<string>;
23
30
  /** Returns the fragment names referenced by `@fireforge-include` directives. */
24
31
  export declare function listFragmentIncludes(css: string): string[];
25
32
  /**
@@ -29,6 +29,15 @@ export { SHARED_FRAGMENTS_DIR } from './furnace-config.js';
29
29
  import { SHARED_FRAGMENTS_DIR } from './furnace-config.js';
30
30
  const INCLUDE_PATTERN = /^\s*\/\*\s*@fireforge-include\s+([\w./-]+)\s*\*\/\s*$/;
31
31
  const END_INCLUDE_PATTERN = /^\s*\/\*\s*@fireforge-end-include\s+([\w./-]+)\s*\*\/\s*$/;
32
+ /**
33
+ * Reads a shared fragment as the exact body an expansion inlines: the
34
+ * trailing newline is dropped so the fenced block ends on the end marker.
35
+ * One definition, so every consumer (expansion, validation, the stale
36
+ * includer probe) compares the same bytes.
37
+ */
38
+ export async function readFragmentSource(sharedDir, name) {
39
+ return (await readText(join(sharedDir, name))).replace(/\n$/, '');
40
+ }
32
41
  /** Returns the fragment names referenced by `@fireforge-include` directives. */
33
42
  export function listFragmentIncludes(css) {
34
43
  const names = [];
@@ -93,12 +102,12 @@ export async function expandCssFragments(css, sharedDir) {
93
102
  throw new FurnaceError(`CSS fragment "${name}" not found in components/${SHARED_FRAGMENTS_DIR}/. ` +
94
103
  'Create the fragment file or remove the @fireforge-include directive.');
95
104
  }
96
- const content = await readText(fragmentPath);
105
+ const content = await readFragmentSource(sharedDir, name);
97
106
  if (listFragmentIncludes(content).length > 0) {
98
107
  throw new FurnaceError(`CSS fragment "${name}" contains an @fireforge-include directive of its own; ` +
99
108
  'nested fragment includes are not supported.');
100
109
  }
101
- fragments.set(name, content.replace(/\n$/, ''));
110
+ fragments.set(name, content);
102
111
  }
103
112
  const out = [];
104
113
  for (const line of stripped.split('\n')) {
@@ -226,7 +235,7 @@ export async function validateCssFragments(componentDir, tagName, sharedDir, eng
226
235
  }
227
236
  if (deployedBodies === null)
228
237
  continue;
229
- const fragmentContent = (await readText(fragmentPath)).replace(/\n$/, '');
238
+ const fragmentContent = await readFragmentSource(sharedDir, include);
230
239
  const deployed = deployedBodies.get(include);
231
240
  if (deployed === undefined || deployed !== fragmentContent) {
232
241
  issues.push({
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Reverse index from shared CSS fragments to the custom components that
3
+ * include them, plus the staleness probe a targeted `furnace deploy <tag>`
4
+ * runs over the OTHER includers of the fragments it just refreshed.
5
+ *
6
+ * Fragment staleness was only ever a validator finding
7
+ * (`stale-fragment-expansion` in {@link validateCssFragments}), printed per
8
+ * includer by `furnace validate` and `deploy --dry-run`. A targeted deploy
9
+ * validates only the component it was asked for, so the one command that
10
+ * changes a fragment's deployed state for SOME includers is the one that
11
+ * never mentioned the rest: after a fragment edit, `deploy <a> <b>` left
12
+ * every other consumer carrying the old expansion, and nothing said so
13
+ * until a `grep` over the engine found it.
14
+ *
15
+ * Only custom components are indexed. Override components are copied
16
+ * byte-for-byte and never expand fragments.
17
+ */
18
+ import type { CustomComponentConfig } from '../types/furnace.js';
19
+ import type { FurnacePaths } from './furnace-config.js';
20
+ /** One includer whose deployed sheets carry a stale expansion. */
21
+ export interface StaleFragmentIncluder {
22
+ /** Custom component tag. */
23
+ tag: string;
24
+ /** Fragments (relative to `components/shared/`) whose expansion is stale. */
25
+ fragments: string[];
26
+ }
27
+ /**
28
+ * Maps every fragment name to the custom tags whose workspace sheets
29
+ * include it.
30
+ *
31
+ * @param customDir - `components/custom/` directory
32
+ * @param custom - The `custom` map from furnace.json
33
+ */
34
+ export declare function collectFragmentIncluders(customDir: string, custom: Readonly<Record<string, CustomComponentConfig>>): Promise<Map<string, Set<string>>>;
35
+ /**
36
+ * Finds the other DEPLOYED includers of the fragments `targetTag` includes
37
+ * whose engine sheets do not carry the current fragment source.
38
+ *
39
+ * Only fragments the target includes are considered: a fragment unrelated
40
+ * to this deploy is not its business. An includer whose engine sheet does
41
+ * not exist is not deployed and is skipped. A deployed sheet with no
42
+ * expansion at all counts as stale, the same rule `validateCssFragments`
43
+ * applies.
44
+ *
45
+ * @returns The fragments the target includes, and the stale includers
46
+ * (sorted by tag), each naming the fragments that are stale for it
47
+ */
48
+ export declare function findStaleIncludersOfTargetFragments(args: {
49
+ targetTag: string;
50
+ custom: Readonly<Record<string, CustomComponentConfig>>;
51
+ furnacePaths: FurnacePaths;
52
+ engineDir: string;
53
+ }): Promise<{
54
+ fragments: string[];
55
+ stale: StaleFragmentIncluder[];
56
+ }>;
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Reverse index from shared CSS fragments to the custom components that
4
+ * include them, plus the staleness probe a targeted `furnace deploy <tag>`
5
+ * runs over the OTHER includers of the fragments it just refreshed.
6
+ *
7
+ * Fragment staleness was only ever a validator finding
8
+ * (`stale-fragment-expansion` in {@link validateCssFragments}), printed per
9
+ * includer by `furnace validate` and `deploy --dry-run`. A targeted deploy
10
+ * validates only the component it was asked for, so the one command that
11
+ * changes a fragment's deployed state for SOME includers is the one that
12
+ * never mentioned the rest: after a fragment edit, `deploy <a> <b>` left
13
+ * every other consumer carrying the old expansion, and nothing said so
14
+ * until a `grep` over the engine found it.
15
+ *
16
+ * Only custom components are indexed. Override components are copied
17
+ * byte-for-byte and never expand fragments.
18
+ */
19
+ import { readdir } from 'node:fs/promises';
20
+ import { join } from 'node:path';
21
+ import { pathExists, readText } from '../utils/fs.js';
22
+ import { extractExpandedFragmentBodies, listFragmentIncludes, readFragmentSource, } from './furnace-css-fragments.js';
23
+ /** Workspace `.css` sheets of one custom component, with their includes. */
24
+ async function listComponentSheetIncludes(componentDir) {
25
+ if (!(await pathExists(componentDir)))
26
+ return [];
27
+ let entries;
28
+ try {
29
+ entries = await readdir(componentDir);
30
+ }
31
+ catch {
32
+ // An unreadable component directory includes nothing. Same degradation
33
+ // as validateCssFragments: the index must never crash a deploy.
34
+ return [];
35
+ }
36
+ const sheets = [];
37
+ for (const fileName of entries.sort()) {
38
+ if (!fileName.endsWith('.css'))
39
+ continue;
40
+ const includes = listFragmentIncludes(await readText(join(componentDir, fileName)));
41
+ if (includes.length > 0)
42
+ sheets.push({ fileName, includes });
43
+ }
44
+ return sheets;
45
+ }
46
+ /**
47
+ * Maps every fragment name to the custom tags whose workspace sheets
48
+ * include it.
49
+ *
50
+ * @param customDir - `components/custom/` directory
51
+ * @param custom - The `custom` map from furnace.json
52
+ */
53
+ export async function collectFragmentIncluders(customDir, custom) {
54
+ const index = new Map();
55
+ for (const tag of Object.keys(custom).sort()) {
56
+ for (const sheet of await listComponentSheetIncludes(join(customDir, tag))) {
57
+ for (const fragment of sheet.includes) {
58
+ const tags = index.get(fragment) ?? new Set();
59
+ tags.add(tag);
60
+ index.set(fragment, tags);
61
+ }
62
+ }
63
+ }
64
+ return index;
65
+ }
66
+ /**
67
+ * Finds the other DEPLOYED includers of the fragments `targetTag` includes
68
+ * whose engine sheets do not carry the current fragment source.
69
+ *
70
+ * Only fragments the target includes are considered: a fragment unrelated
71
+ * to this deploy is not its business. An includer whose engine sheet does
72
+ * not exist is not deployed and is skipped. A deployed sheet with no
73
+ * expansion at all counts as stale, the same rule `validateCssFragments`
74
+ * applies.
75
+ *
76
+ * @returns The fragments the target includes, and the stale includers
77
+ * (sorted by tag), each naming the fragments that are stale for it
78
+ */
79
+ export async function findStaleIncludersOfTargetFragments(args) {
80
+ const { targetTag, custom, furnacePaths, engineDir } = args;
81
+ const index = await collectFragmentIncluders(furnacePaths.customDir, custom);
82
+ const fragments = [...index]
83
+ .filter(([, tags]) => tags.has(targetTag))
84
+ .map(([fragment]) => fragment)
85
+ .sort();
86
+ if (fragments.length === 0)
87
+ return { fragments, stale: [] };
88
+ const targetFragments = new Set(fragments);
89
+ const sources = new Map();
90
+ const stale = [];
91
+ for (const tag of Object.keys(custom).sort()) {
92
+ if (tag === targetTag)
93
+ continue;
94
+ const componentConfig = custom[tag];
95
+ if (!componentConfig)
96
+ continue;
97
+ const engineTargetDir = join(engineDir, componentConfig.targetPath);
98
+ const staleFragments = new Set();
99
+ for (const sheet of await listComponentSheetIncludes(join(furnacePaths.customDir, tag))) {
100
+ const relevant = sheet.includes.filter((f) => targetFragments.has(f));
101
+ if (relevant.length === 0)
102
+ continue;
103
+ const destPath = join(engineTargetDir, sheet.fileName);
104
+ if (!(await pathExists(destPath)))
105
+ continue;
106
+ const bodies = extractExpandedFragmentBodies(await readText(destPath));
107
+ for (const fragment of relevant) {
108
+ let source = sources.get(fragment);
109
+ if (source === undefined) {
110
+ source = await readFragmentSource(furnacePaths.sharedDir, fragment);
111
+ sources.set(fragment, source);
112
+ }
113
+ if (bodies.get(fragment) !== source)
114
+ staleFragments.add(fragment);
115
+ }
116
+ }
117
+ if (staleFragments.size > 0) {
118
+ stale.push({ tag, fragments: [...staleFragments].sort() });
119
+ }
120
+ }
121
+ return { fragments, stale };
122
+ }
@@ -31,8 +31,7 @@ function decodeStrictUtf8(bytes) {
31
31
  try {
32
32
  return strictUtf8.decode(bytes);
33
33
  }
34
- catch (error) {
35
- void error;
34
+ catch {
36
35
  return undefined;
37
36
  }
38
37
  }
@@ -199,8 +199,7 @@ export async function isBinaryFile(repoDir, filePath) {
199
199
  await fh.close();
200
200
  }
201
201
  }
202
- catch (error) {
203
- void error;
202
+ catch {
204
203
  return false;
205
204
  }
206
205
  }
@@ -340,8 +340,7 @@ async function getLockAgeMs(lockPath) {
340
340
  const stats = await stat(lockPath);
341
341
  return Math.max(0, Date.now() - stats.mtimeMs);
342
342
  }
343
- catch (error) {
344
- void error;
343
+ catch {
345
344
  return undefined;
346
345
  }
347
346
  }
@@ -140,8 +140,7 @@ export async function hasBuildArtifacts(engineDir) {
140
140
  }
141
141
  return { exists: true, objDir: selectedObjDir };
142
142
  }
143
- catch (error) {
144
- void error;
143
+ catch {
145
144
  return { exists: false };
146
145
  }
147
146
  }
@@ -12,6 +12,36 @@ export declare const DEFAULT_MOCHITEST_SERVER_PORT = 8888;
12
12
  * harness's httpd has.
13
13
  */
14
14
  export declare function isMochitestServerHolder(holder: MarionettePortHolder): boolean;
15
+ /**
16
+ * How a recognized harness httpd relates to the checkout about to run.
17
+ *
18
+ * `this-checkout` is debris from one of OUR interrupted runs and may be
19
+ * offered for termination. `foreign-checkout` is the same harness serving a
20
+ * LIVE run in a sibling worktree: `server.js` plus objdir provenance is
21
+ * exactly what a peer's in-flight suite looks like, and the only thing
22
+ * separating the two is whose objdir the command line points into.
23
+ * `unrecognized` is not the harness at all.
24
+ */
25
+ export type MochitestServerHolderClass = 'this-checkout' | 'foreign-checkout' | 'unrecognized';
26
+ /**
27
+ * Classifies the holder against this project's engine directory. Every
28
+ * absolute path on the command line is tested with {@link isPathInsideRoot}
29
+ * (a token test, not a substring test, so `/a/hominis/engine` does not claim
30
+ * a holder living under `/a/hominis-2/engine`). On POSIX the command line is
31
+ * the full `ps -o args=` output, so the objdir path is always present for a
32
+ * harness httpd; a holder whose paths all lie elsewhere belongs to another
33
+ * checkout.
34
+ *
35
+ * @param holder - The listener found on the port
36
+ * @param engineDir - Absolute engine directory of THIS project
37
+ */
38
+ export declare function classifyMochitestServerHolder(holder: MarionettePortHolder, engineDir: string): MochitestServerHolderClass;
39
+ /**
40
+ * The objdir a foreign holder is serving from, for the refusal text: the
41
+ * first absolute command-line token with an `obj-…` segment, cut at that
42
+ * segment. Falls back to the first absolute token, then to a placeholder.
43
+ */
44
+ export declare function describeHolderObjdir(commandLine: string): string;
15
45
  /**
16
46
  * Refuses a mochitest dispatch when the harness's server port is held. It
17
47
  * terminates the holder first only for a recognized stale harness httpd,
@@ -26,10 +56,19 @@ export declare function isMochitestServerHolder(holder: MarionettePortHolder): b
26
56
  * Best effort: {@link probeMarionettePort} reports an
27
57
  * unprobeable port as free, so a host without `lsof` runs exactly as before.
28
58
  *
59
+ * A recognized httpd from ANOTHER checkout is a refusal and never a kill
60
+ * either, flag or no flag: it is a peer's live suite, not our debris. The
61
+ * browser-side probe draws the same line for a bare launch it cannot
62
+ * attribute; this one can attribute, because the objdir is on the command
63
+ * line, so it names the other checkout instead of offering `kill -9`.
64
+ *
29
65
  * @param port - Port to probe (default {@link DEFAULT_MOCHITEST_SERVER_PORT})
30
- * @param options - `killStaleServer` opts into terminating a recognized holder
66
+ * @param options - `engineDir` is this project's absolute engine directory,
67
+ * the ownership boundary; `killStaleServer` opts into terminating a holder
68
+ * recognized as THIS checkout's debris
31
69
  */
32
- export declare function ensureMochitestServerPortAvailable(port?: number, options?: {
70
+ export declare function ensureMochitestServerPortAvailable(port: number | undefined, options: {
71
+ engineDir: string;
33
72
  killStaleServer?: boolean;
34
73
  }): Promise<void>;
35
74
  /**
@@ -37,6 +76,6 @@ export declare function ensureMochitestServerPortAvailable(port?: number, option
37
76
  *
38
77
  * @param port - The probed port
39
78
  * @param holder - The listener found on it
40
- * @param recognized - Whether it is the harness's own `server.js`
79
+ * @param holderClass - Whose harness `server.js` it is, if it is one at all
41
80
  */
42
- export declare function describeMochitestServerRefusal(port: number, holder: MarionettePortHolder, recognized: boolean): string;
81
+ export declare function describeMochitestServerRefusal(port: number, holder: MarionettePortHolder, holderClass: MochitestServerHolderClass): string;