@hominis/fireforge 0.46.1 → 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.
- package/CHANGELOG.md +28 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/config.js +1 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +59 -0
- package/dist/src/commands/furnace/deploy-fragment-includers.js +120 -0
- package/dist/src/commands/furnace/deploy.js +36 -3
- package/dist/src/commands/furnace/validation-output.d.ts +5 -0
- package/dist/src/commands/furnace/validation-output.js +35 -9
- package/dist/src/commands/patch/delete-gap-notice.d.ts +24 -0
- package/dist/src/commands/patch/delete-gap-notice.js +46 -0
- package/dist/src/commands/patch/delete.js +11 -0
- package/dist/src/commands/re-export-lint.d.ts +26 -1
- package/dist/src/commands/re-export-lint.js +60 -20
- package/dist/src/commands/re-export-scan.d.ts +8 -0
- package/dist/src/commands/re-export-scan.js +12 -2
- package/dist/src/commands/re-export-single.js +24 -5
- package/dist/src/commands/run.js +42 -17
- package/dist/src/commands/test-harness-env.d.ts +24 -0
- package/dist/src/commands/test-harness-env.js +61 -0
- package/dist/src/commands/test-register.js +6 -2
- package/dist/src/commands/test-verdict.d.ts +3 -1
- package/dist/src/commands/test-verdict.js +17 -2
- package/dist/src/commands/test.js +9 -16
- package/dist/src/core/build-jar-escalation.d.ts +1 -4
- package/dist/src/core/build-jar-escalation.js +1 -4
- package/dist/src/core/build-prepare.js +72 -55
- package/dist/src/core/firefox-cache.js +1 -2
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +19 -4
- package/dist/src/core/furnace-css-fragments.d.ts +7 -0
- package/dist/src/core/furnace-css-fragments.js +12 -3
- package/dist/src/core/furnace-fragment-includers.d.ts +56 -0
- package/dist/src/core/furnace-fragment-includers.js +122 -0
- package/dist/src/core/git-diff-new-file.js +1 -2
- package/dist/src/core/git-file-ops.js +1 -2
- package/dist/src/core/git.js +1 -2
- package/dist/src/core/mach-build-artifacts.js +1 -2
- package/dist/src/core/mochitest-server-port.d.ts +43 -4
- package/dist/src/core/mochitest-server-port.js +64 -11
- package/dist/src/core/patch-lint-imports.js +4 -4
- package/dist/src/core/smoke-patterns.d.ts +24 -0
- package/dist/src/core/smoke-patterns.js +50 -5
- package/dist/src/core/token-dark-mode.d.ts +8 -0
- package/dist/src/core/token-dark-mode.js +30 -17
- package/dist/src/types/commands/options.d.ts +6 -0
- package/docs/configuration.md +8 -0
- package/docs/exit-codes.md +19 -19
- package/docs/machine-output.md +22 -10
- package/docs/testing.md +13 -1
- 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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
59
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
+
}
|
package/dist/src/core/git.js
CHANGED
|
@@ -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 - `
|
|
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
|
|
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
|
|
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,
|
|
81
|
+
export declare function describeMochitestServerRefusal(port: number, holder: MarionettePortHolder, holderClass: MochitestServerHolderClass): string;
|