@hominis/fireforge 0.31.0 → 0.33.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 +22 -0
- package/dist/src/commands/export-all.js +4 -1
- package/dist/src/commands/export-shared.js +10 -1
- package/dist/src/commands/export.js +5 -1
- package/dist/src/commands/lint-per-patch.d.ts +2 -0
- package/dist/src/commands/lint-per-patch.js +206 -44
- package/dist/src/commands/lint.js +100 -7
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/test-run.d.ts +10 -0
- package/dist/src/commands/test-run.js +13 -4
- package/dist/src/commands/test.js +46 -7
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +52 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-jsconfig.js +22 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -0
- package/dist/src/core/git-file-ops.js +82 -1
- package/dist/src/core/mach-resource-shim.d.ts +21 -0
- package/dist/src/core/mach-resource-shim.js +92 -0
- package/dist/src/core/mach.d.ts +17 -0
- package/dist/src/core/mach.js +30 -2
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +213 -67
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +19 -163
- package/dist/src/core/test-harness-crash.d.ts +6 -3
- package/dist/src/core/test-harness-crash.js +32 -4
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +9 -4
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck-shim.d.ts +3 -1
- package/dist/src/core/typecheck-shim.js +43 -3
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/types/commands/options.d.ts +22 -0
- package/dist/src/types/config.d.ts +24 -2
- package/package.json +3 -3
|
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { getDiffForFilesAgainstHead } from '../../core/git-diff.js';
|
|
11
11
|
import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
|
|
12
12
|
import { extractAffectedFiles } from '../../core/patch-apply.js';
|
|
13
|
-
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
13
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
14
14
|
import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
|
|
15
15
|
import { applyRenameMapToManifest, buildProjectedManifest } from '../../core/patch-policy.js';
|
|
16
16
|
import { buildPatchSourceMetadata } from '../../core/patch-source-metadata.js';
|
|
@@ -82,16 +82,12 @@ function buildEntryProjection(diff) {
|
|
|
82
82
|
}
|
|
83
83
|
return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
|
|
84
84
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
88
|
-
* regressions the split itself would introduce.
|
|
89
|
-
*/
|
|
90
|
-
export async function runProjectedSplitLint(patchesDir, plan) {
|
|
85
|
+
/** Builds the projected post-split queue entries (renumber + shrunken source + new patch). */
|
|
86
|
+
async function buildProjectedSplitEntries(patchesDir, plan) {
|
|
91
87
|
const movedSet = new Set(plan.movedFiles);
|
|
92
88
|
const ownerLookup = (old) => plan.placement.renameMap.get(old)?.newFilename;
|
|
93
89
|
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
94
|
-
const
|
|
90
|
+
const entries = baseCtx.entries.map((entry) => {
|
|
95
91
|
let metadata = entry.metadata;
|
|
96
92
|
if (metadata) {
|
|
97
93
|
metadata = rewriteStagedDependencyOwners(metadata, ownerLookup);
|
|
@@ -105,13 +101,90 @@ export async function runProjectedSplitLint(patchesDir, plan) {
|
|
|
105
101
|
return base;
|
|
106
102
|
return { ...base, ...buildEntryProjection(plan.remainingDiff) };
|
|
107
103
|
});
|
|
108
|
-
|
|
104
|
+
entries.push({
|
|
109
105
|
filename: plan.placement.newFilename,
|
|
110
106
|
order: plan.placement.insertionOrder,
|
|
111
107
|
metadata: null,
|
|
112
108
|
...buildEntryProjection(plan.movedDiff),
|
|
113
109
|
});
|
|
114
|
-
|
|
110
|
+
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
111
|
+
return { baseCtx, entries };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Computes the staged forward-import declarations the split introduces:
|
|
115
|
+
* forward edges from existing patches into the freshly-created patch (its
|
|
116
|
+
* `creates` files are the moved files; the owner is the new patch, so it is
|
|
117
|
+
* known). Keyed by the importing patch's projected (post-rename) filename.
|
|
118
|
+
*
|
|
119
|
+
* These edges did not exist before the split (importer and imported file
|
|
120
|
+
* lived in the same patch), so they have no declaration yet — without this
|
|
121
|
+
* the projected lint flags them while the real per-patch gate would resolve
|
|
122
|
+
* them once declared. Auto-declaring keeps the two in lock-step and lets a
|
|
123
|
+
* sound split read as sound.
|
|
124
|
+
*/
|
|
125
|
+
function computeSplitStagedDependencyAdditions(projectedEntries, newFilename) {
|
|
126
|
+
const additions = new Map();
|
|
127
|
+
for (const edge of collectForwardImportEdges({ entries: projectedEntries })) {
|
|
128
|
+
if (edge.owner !== newFilename)
|
|
129
|
+
continue;
|
|
130
|
+
const decl = {
|
|
131
|
+
file: edge.sitePath,
|
|
132
|
+
specifier: edge.specifier,
|
|
133
|
+
creates: edge.creates,
|
|
134
|
+
owner: newFilename,
|
|
135
|
+
};
|
|
136
|
+
const list = additions.get(edge.entry) ?? [];
|
|
137
|
+
const dup = list.some((d) => d.file === decl.file &&
|
|
138
|
+
d.specifier === decl.specifier &&
|
|
139
|
+
d.creates === decl.creates &&
|
|
140
|
+
d.owner === decl.owner);
|
|
141
|
+
if (!dup)
|
|
142
|
+
list.push(decl);
|
|
143
|
+
additions.set(edge.entry, list);
|
|
144
|
+
}
|
|
145
|
+
return additions;
|
|
146
|
+
}
|
|
147
|
+
/** Merges `decls` into a patch's `stagedDependencies.forwardImports` (no duplicates). */
|
|
148
|
+
export function mergeStagedForwardImports(patch, decls) {
|
|
149
|
+
if (decls.length === 0)
|
|
150
|
+
return patch;
|
|
151
|
+
const existing = patch.stagedDependencies?.forwardImports ?? [];
|
|
152
|
+
const merged = [...existing];
|
|
153
|
+
for (const decl of decls) {
|
|
154
|
+
const dup = merged.some((d) => d.file === decl.file &&
|
|
155
|
+
d.specifier === decl.specifier &&
|
|
156
|
+
d.creates === decl.creates &&
|
|
157
|
+
(d.owner ?? '') === (decl.owner ?? ''));
|
|
158
|
+
if (!dup)
|
|
159
|
+
merged.push(decl);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...patch,
|
|
163
|
+
stagedDependencies: { ...patch.stagedDependencies, forwardImports: merged },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Injects the computed staged-dependency additions into projected entries' metadata. */
|
|
167
|
+
function injectStagedDependencyAdditions(entries, additions) {
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const decls = additions.get(entry.filename);
|
|
170
|
+
if (!decls?.length || !entry.metadata)
|
|
171
|
+
continue;
|
|
172
|
+
entry.metadata = mergeStagedForwardImports(entry.metadata, decls);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Projects the full split (renumber + shrunken source + synthetic new
|
|
177
|
+
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
178
|
+
* regressions the split itself would introduce. Forward edges into the new
|
|
179
|
+
* patch are auto-declared (and the declarations returned) so the projection
|
|
180
|
+
* matches the real per-patch gate the split leaves behind.
|
|
181
|
+
*/
|
|
182
|
+
export async function runProjectedSplitLint(patchesDir, plan) {
|
|
183
|
+
const { baseCtx, entries: projectedEntries } = await buildProjectedSplitEntries(patchesDir, plan);
|
|
184
|
+
// Discover and auto-declare the forward edges this split introduces into
|
|
185
|
+
// the new patch, then inject them before linting so they resolve.
|
|
186
|
+
const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename);
|
|
187
|
+
injectStagedDependencyAdditions(projectedEntries, stagedDependencyAdditions);
|
|
115
188
|
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
116
189
|
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
117
190
|
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
@@ -119,12 +192,13 @@ export async function runProjectedSplitLint(patchesDir, plan) {
|
|
|
119
192
|
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
|
|
120
193
|
'error(s) unrelated to this split. Run "fireforge verify" to list them.');
|
|
121
194
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
195
|
+
const conflicts = regressions.length === 0
|
|
196
|
+
? null
|
|
197
|
+
: {
|
|
198
|
+
reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
199
|
+
details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
|
|
200
|
+
};
|
|
201
|
+
return { conflicts, stagedDependencyAdditions };
|
|
128
202
|
}
|
|
129
203
|
/** Builds the projected manifest for policy enforcement. */
|
|
130
204
|
export function projectSplitManifest(manifest, plan, newMetadata) {
|
|
@@ -32,7 +32,7 @@ import { info, intro, outro, success, warn } from '../../utils/logger.js';
|
|
|
32
32
|
import { pickDefined } from '../../utils/options.js';
|
|
33
33
|
import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
|
|
34
34
|
import { runPatchLint } from '../export-shared.js';
|
|
35
|
-
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
|
|
35
|
+
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
|
|
36
36
|
/**
|
|
37
37
|
* Commits a confirmed split under the patch directory lock: renumber →
|
|
38
38
|
* write new patch body → write shrunken source body → single manifest
|
|
@@ -74,7 +74,13 @@ async function commitPatchSplit(patchesDir, plan, newMetadata, options) {
|
|
|
74
74
|
if (!fresh)
|
|
75
75
|
throw new GeneralError('Manifest disappeared during split commit.');
|
|
76
76
|
const updatedPatches = fresh.patches.map((patch) => {
|
|
77
|
-
|
|
77
|
+
let withOwners = rewriteSplitOwners(patch, effectiveSourceFilename, movedSet, plan.placement.newFilename);
|
|
78
|
+
// Persist the auto-declared forward edges into the new patch so the
|
|
79
|
+
// real per-patch gate stays clean (keyed by post-rename filename).
|
|
80
|
+
const decls = plan.stagedDependencyAdditions.get(withOwners.filename);
|
|
81
|
+
if (decls?.length) {
|
|
82
|
+
withOwners = mergeStagedForwardImports(withOwners, decls);
|
|
83
|
+
}
|
|
78
84
|
if (patch.filename !== effectiveSourceFilename)
|
|
79
85
|
return withOwners;
|
|
80
86
|
return { ...withOwners, filesAffected: plan.remainingFiles };
|
|
@@ -199,13 +205,16 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
199
205
|
name: options.name,
|
|
200
206
|
description: options.description ?? '',
|
|
201
207
|
ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
|
|
208
|
+
// Populated by runProjectedSplitLint below (forward edges into the new patch).
|
|
209
|
+
stagedDependencyAdditions: new Map(),
|
|
202
210
|
};
|
|
203
211
|
// Per-patch lint both projected bodies, threading the source patch's
|
|
204
212
|
// tier/lintIgnore so an intentional-advisory patch can still split.
|
|
205
213
|
const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
|
|
206
214
|
await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
|
|
207
215
|
await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
|
|
208
|
-
const conflicts = await runProjectedSplitLint(paths.patches, plan);
|
|
216
|
+
const { conflicts, stagedDependencyAdditions } = await runProjectedSplitLint(paths.patches, plan);
|
|
217
|
+
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
209
218
|
const newMetadata = buildNewPatchMetadata(plan, config);
|
|
210
219
|
enforcePatchPolicy({
|
|
211
220
|
config,
|
|
@@ -198,7 +198,10 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
198
198
|
// standard re-export path).
|
|
199
199
|
const { effectiveTier, effectiveLintIgnore, flagIgnoreSet } = resolveEffectiveTierAndLintIgnore(target, options);
|
|
200
200
|
const ignoreChecks = effectiveLintIgnore ? new Set(effectiveLintIgnore) : undefined;
|
|
201
|
-
await
|
|
201
|
+
const patchQueueCtx = (await pathExists(paths.patches))
|
|
202
|
+
? await buildPatchQueueContext(paths.patches)
|
|
203
|
+
: undefined;
|
|
204
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, effectiveTier);
|
|
202
205
|
const conflicts = await runProjectedCrossPatchLint(paths.patches, target.filename, projectedDiff);
|
|
203
206
|
const filesUpdates = buildFilesModeMetadataUpdates(actualProjectedFiles, options, effectiveLintIgnore, flagIgnoreSet);
|
|
204
207
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
@@ -7,6 +7,7 @@ import { isGitRepository } from '../core/git.js';
|
|
|
7
7
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
8
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
9
9
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
10
|
+
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
10
11
|
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
11
12
|
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
12
13
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
@@ -185,7 +186,13 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
185
186
|
command: 're-export',
|
|
186
187
|
forceUnsafe: options.forceUnsafe === true,
|
|
187
188
|
});
|
|
188
|
-
|
|
189
|
+
// Pass the whole-queue context so checkJs resolves cross-patch
|
|
190
|
+
// `resource:///` imports against the real owning sources (report scope
|
|
191
|
+
// stays this patch — see runPatchLint).
|
|
192
|
+
const patchQueueCtx = (await pathExists(paths.patches))
|
|
193
|
+
? await buildPatchQueueContext(paths.patches)
|
|
194
|
+
: undefined;
|
|
195
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, patchQueueCtx, ignoreChecks, effectiveTier);
|
|
189
196
|
if (isDryRun) {
|
|
190
197
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
191
198
|
if (effectiveTier !== undefined && effectiveTier !== patch.tier) {
|
|
@@ -14,6 +14,14 @@ import { type MachCommandResult } from '../core/mach.js';
|
|
|
14
14
|
import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
15
15
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
16
16
|
export declare const DEFAULT_HARNESS_RETRIES = 2;
|
|
17
|
+
/**
|
|
18
|
+
* Which mach command a run dispatches to. Single-suite runs use the
|
|
19
|
+
* suite-specific command (`mach xpcshell-test` / `mach mochitest`), which
|
|
20
|
+
* skips the mozlog resource monitor that crashes generic `mach test` on a
|
|
21
|
+
* broken host (field report E1). `generic` is the historical `mach test`
|
|
22
|
+
* path (mixed/all-tests runs, or the `--generic-mach-test` opt-out).
|
|
23
|
+
*/
|
|
24
|
+
export type TestSuite = 'xpcshell' | 'mochitest' | 'generic';
|
|
17
25
|
/** Inputs shared by every harness invocation in one `fireforge test` run. */
|
|
18
26
|
export interface TestRunContext {
|
|
19
27
|
engineDir: string;
|
|
@@ -22,6 +30,8 @@ export interface TestRunContext {
|
|
|
22
30
|
xpcshell: string[];
|
|
23
31
|
nonXpcshell: string[];
|
|
24
32
|
};
|
|
33
|
+
/** Suite-specific dispatch target for this run (E1). */
|
|
34
|
+
suite: TestSuite;
|
|
25
35
|
/** Extra mach args before per-shard appdir injection. */
|
|
26
36
|
baseExtraArgs: readonly string[];
|
|
27
37
|
/** Bounded harness-crash retry budget (0 disables retries). */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* harness runs cost startup time but make results reproducible; the
|
|
12
12
|
* combined invocation stays available via `--no-shard`.
|
|
13
13
|
*/
|
|
14
|
-
import { testWithOutput } from '../core/mach.js';
|
|
14
|
+
import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
|
|
15
15
|
import { buildHarnessCrashMessage, classifyHarnessRun, } from '../core/test-harness-crash.js';
|
|
16
16
|
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
17
17
|
import { BuildError } from '../errors/build.js';
|
|
@@ -19,6 +19,14 @@ import { info, note, warn } from '../utils/logger.js';
|
|
|
19
19
|
import { maybeInjectAppdirArg } from './test-appdir.js';
|
|
20
20
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
21
21
|
export const DEFAULT_HARNESS_RETRIES = 2;
|
|
22
|
+
/** Resolves the capturing mach dispatcher for a suite. */
|
|
23
|
+
function dispatchForSuite(suite) {
|
|
24
|
+
if (suite === 'xpcshell')
|
|
25
|
+
return xpcshellTestWithOutput;
|
|
26
|
+
if (suite === 'mochitest')
|
|
27
|
+
return mochitestWithOutput;
|
|
28
|
+
return testWithOutput;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
24
32
|
* crashes up to the configured budget. Every attempt goes through the
|
|
@@ -27,6 +35,7 @@ export const DEFAULT_HARNESS_RETRIES = 2;
|
|
|
27
35
|
export async function runTestsWithRetries(ctx, paths) {
|
|
28
36
|
const extraArgs = [...ctx.baseExtraArgs];
|
|
29
37
|
const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
|
|
38
|
+
const dispatch = dispatchForSuite(ctx.suite);
|
|
30
39
|
const maxAttempts = Math.max(1, ctx.harnessRetries + 1);
|
|
31
40
|
let attempts = 0;
|
|
32
41
|
let result;
|
|
@@ -34,9 +43,9 @@ export async function runTestsWithRetries(ctx, paths) {
|
|
|
34
43
|
for (;;) {
|
|
35
44
|
attempts += 1;
|
|
36
45
|
result = ctx.env
|
|
37
|
-
? await
|
|
38
|
-
: await
|
|
39
|
-
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, ctx.env);
|
|
46
|
+
? await dispatch(ctx.engineDir, paths, extraArgs, ctx.env)
|
|
47
|
+
: await dispatch(ctx.engineDir, paths, extraArgs);
|
|
48
|
+
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, ctx.env, dispatch);
|
|
40
49
|
const combined = `${result.stdout}\n${result.stderr}`;
|
|
41
50
|
verdict = classifyHarnessRun(result.exitCode, combined, paths);
|
|
42
51
|
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
@@ -5,6 +5,7 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, withBuildLock, } from '../core/mach.js';
|
|
6
6
|
import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
7
7
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
|
+
import { buildHarnessCrashMessage, detectHarnessCrashSignature, } from '../core/test-harness-crash.js';
|
|
8
9
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
9
10
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
10
11
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
@@ -42,6 +43,25 @@ async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
|
42
43
|
}
|
|
43
44
|
return result;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
48
|
+
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
49
|
+
* `mach mochitest`), which degrades a broken host resource monitor to a
|
|
50
|
+
* warning instead of crashing generic `mach test` at startup (E1). Mixed runs
|
|
51
|
+
* are rejected before this point; a path-less "run all" or an explicit
|
|
52
|
+
* `--generic-mach-test` opt-out stays on the generic command.
|
|
53
|
+
*/
|
|
54
|
+
function resolveTestSuite(classification, forceGeneric) {
|
|
55
|
+
if (forceGeneric)
|
|
56
|
+
return 'generic';
|
|
57
|
+
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length === 0) {
|
|
58
|
+
return 'xpcshell';
|
|
59
|
+
}
|
|
60
|
+
if (classification.nonXpcshell.length > 0 && classification.xpcshell.length === 0) {
|
|
61
|
+
return 'mochitest';
|
|
62
|
+
}
|
|
63
|
+
return 'generic';
|
|
64
|
+
}
|
|
45
65
|
function buildMixedHarnessMessage(classification) {
|
|
46
66
|
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
47
67
|
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
@@ -80,16 +100,31 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
80
100
|
}
|
|
81
101
|
return bundleCheck.expectedPath;
|
|
82
102
|
}
|
|
83
|
-
async function runPreTestBuild(projectRoot, paths, projectConfig) {
|
|
103
|
+
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
|
|
84
104
|
await withBuildLock(projectRoot, async () => {
|
|
85
105
|
await prepareBuildEnvironment(projectRoot, paths, projectConfig);
|
|
86
106
|
const s = spinner('Running incremental build...');
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
// The pre-test build runs through mach too, so the same resource-monitor
|
|
108
|
+
// startup crash that aborts `mach test` can abort `mach build faster`.
|
|
109
|
+
// Run the harness-crash classifier over the build output and retry within
|
|
110
|
+
// the same `--harness-retries` budget rather than hard-failing with a
|
|
111
|
+
// bare "Pre-test build failed" (field report E2).
|
|
112
|
+
const maxAttempts = Math.max(1, harnessRetries + 1);
|
|
113
|
+
for (let attempt = 1;; attempt += 1) {
|
|
114
|
+
const buildResult = await buildUI(paths.engine);
|
|
115
|
+
if (buildResult.exitCode === 0) {
|
|
116
|
+
s.stop('Build complete');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const signature = detectHarnessCrashSignature(`${buildResult.stdout}\n${buildResult.stderr}`);
|
|
120
|
+
if (signature && attempt < maxAttempts) {
|
|
121
|
+
s.message(`Pre-test build hit a harness crash (${signature.reason}); ` +
|
|
122
|
+
`retrying (attempt ${attempt + 1} of ${maxAttempts})...`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
89
125
|
s.error('Pre-test build failed');
|
|
90
|
-
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
126
|
+
throw new BuildError(signature ? buildHarnessCrashMessage(signature, attempt) : 'Pre-test build failed', 'mach build faster');
|
|
91
127
|
}
|
|
92
|
-
s.stop('Build complete');
|
|
93
128
|
});
|
|
94
129
|
}
|
|
95
130
|
function logTestSelection(normalizedPaths) {
|
|
@@ -239,9 +274,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
239
274
|
// missing-bundle path instead of a cryptic `Browser process exited
|
|
240
275
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
241
276
|
const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
277
|
+
const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
|
|
242
278
|
// Run incremental build if requested
|
|
243
279
|
if (options.build) {
|
|
244
|
-
await runPreTestBuild(projectRoot, paths, projectConfig);
|
|
280
|
+
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries);
|
|
245
281
|
info('');
|
|
246
282
|
}
|
|
247
283
|
else {
|
|
@@ -299,6 +335,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
299
335
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
300
336
|
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
301
337
|
}
|
|
338
|
+
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
302
339
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
303
340
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
304
341
|
: [];
|
|
@@ -325,8 +362,9 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
325
362
|
engineDir: paths.engine,
|
|
326
363
|
objDir: buildCheck.objDir,
|
|
327
364
|
classification,
|
|
365
|
+
suite,
|
|
328
366
|
baseExtraArgs: extraArgs,
|
|
329
|
-
harnessRetries
|
|
367
|
+
harnessRetries,
|
|
330
368
|
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
331
369
|
};
|
|
332
370
|
const postRebuildContext = options.build
|
|
@@ -381,6 +419,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
381
419
|
}
|
|
382
420
|
return n;
|
|
383
421
|
})
|
|
422
|
+
.option('--generic-mach-test', 'Force dispatch through generic `mach test` instead of the suite-specific `mach xpcshell-test` / `mach mochitest` a single-suite run auto-selects (the suite-specific commands skip the mozlog resource monitor that crashes `mach test` on some hosts).')
|
|
384
423
|
.option('--no-shard', 'Run multiple test paths in one combined mach invocation instead of sequential per-file shards')
|
|
385
424
|
.option('--perf-samples <path>', 'Publish a perf-sample artifact path to the harness via <BINARYNAME>_PERF_SAMPLE_JSON (resolved against the project root)')
|
|
386
425
|
.option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and (unless --mach-arg includes --flavor=xpcshell) auto-forwarded mach args: --setpref=marionette.port=<n> (browser listener) and --marionette=127.0.0.1:<n> (mochitest client). Omits the client flag when --mach-arg already sets --marionette. Use when 2828 is busy or CI assigns another port.', (raw) => {
|
|
@@ -74,12 +74,18 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
74
74
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
75
75
|
...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
|
|
76
76
|
...(options.createCategory === true ? { createCategory: true } : {}),
|
|
77
|
+
...(options.variant !== undefined ? { variant: options.variant } : {}),
|
|
77
78
|
dryRun: true,
|
|
78
79
|
});
|
|
79
80
|
info('[dry-run] Would add token:');
|
|
80
81
|
info(` Name: ${tokenName}`);
|
|
81
82
|
info(` Value: ${value}`);
|
|
82
|
-
|
|
83
|
+
if (options.variant !== undefined) {
|
|
84
|
+
info(` Variant: :root${options.variant}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
info(` Category: ${options.category}${options.createCategory === true ? ' (created if missing)' : ''}`);
|
|
88
|
+
}
|
|
83
89
|
info(` Mode: ${options.mode}`);
|
|
84
90
|
if (options.description)
|
|
85
91
|
info(` Description: ${options.description}`);
|
|
@@ -96,6 +102,7 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
96
102
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
97
103
|
...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
|
|
98
104
|
...(options.createCategory === true ? { createCategory: true } : {}),
|
|
105
|
+
...(options.variant !== undefined ? { variant: options.variant } : {}),
|
|
99
106
|
});
|
|
100
107
|
if (result.skipped) {
|
|
101
108
|
info(`Token ${tokenName} already exists (skipped)`);
|
|
@@ -151,6 +158,9 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
151
158
|
.makeOptionMandatory(true))
|
|
152
159
|
.option('--description <desc>', 'Comment description for the CSS file')
|
|
153
160
|
.option('--dark-value <val>', 'Dark mode value (required if mode is "override")')
|
|
161
|
+
.option('--variant <selector>', 'Attribute selector fragment routing the declaration into a `:root<selector>` block ' +
|
|
162
|
+
"(e.g. '[data-skin=precision]' or '[data-private]'); creates or updates the block. " +
|
|
163
|
+
'CSS-only — cannot be combined with --mode override.')
|
|
154
164
|
.option('--create-category', 'Declare the category banner in the tokens CSS if it does not exist yet')
|
|
155
165
|
.option('--dry-run', 'Show what would be changed without writing')
|
|
156
166
|
.action(withErrorHandling(async (tokenName, value, options) => {
|
|
@@ -162,6 +172,7 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
162
172
|
darkValue: options.darkValue,
|
|
163
173
|
dryRun: options.dryRun,
|
|
164
174
|
createCategory: options.createCategory,
|
|
175
|
+
variant: options.variant,
|
|
165
176
|
}),
|
|
166
177
|
});
|
|
167
178
|
}));
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* but do not fail. Designed for CI use.
|
|
17
17
|
*/
|
|
18
18
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
19
|
+
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
20
|
+
import { findJsconfigPathsDrift, syncFurnaceJsconfigPaths } from '../core/furnace-jsconfig.js';
|
|
19
21
|
import { relativeForDisplay, runTypecheck } from '../core/typecheck.js';
|
|
20
22
|
import { GeneralError } from '../errors/base.js';
|
|
21
23
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
@@ -32,6 +34,11 @@ export function resolveTypecheckProjects(configTypecheck, override) {
|
|
|
32
34
|
return {
|
|
33
35
|
projects: [override],
|
|
34
36
|
...(configTypecheck?.extraShim !== undefined ? { extraShim: configTypecheck.extraShim } : {}),
|
|
37
|
+
// Preserve any per-project override for the targeted path so a one-off
|
|
38
|
+
// `--project` run honours its opt-out / shim override just like a full run.
|
|
39
|
+
...(configTypecheck?.projectOverrides !== undefined
|
|
40
|
+
? { projectOverrides: configTypecheck.projectOverrides }
|
|
41
|
+
: {}),
|
|
35
42
|
};
|
|
36
43
|
}
|
|
37
44
|
if (!configTypecheck) {
|
|
@@ -64,10 +71,38 @@ export async function typecheckCommand(projectRoot, options) {
|
|
|
64
71
|
getProjectPaths(projectRoot);
|
|
65
72
|
const config = await loadConfig(projectRoot);
|
|
66
73
|
const cfg = resolveTypecheckProjects(config.typecheck, options.project);
|
|
74
|
+
// Regenerate a stale Furnace-managed jsconfig before running: the generated
|
|
75
|
+
// `compilerOptions.paths` shim drifts when components are added/renamed
|
|
76
|
+
// without a re-deploy, and a stale shim reports type errors in files the
|
|
77
|
+
// session never touched (e.g. 47 phantom errors from an out-of-date
|
|
78
|
+
// chrome-module mapping). Run the same reconciler `furnace deploy`/`sync`
|
|
79
|
+
// use so typecheck checks against the current workspace.
|
|
80
|
+
await regenerateStaleGeneratedJsconfig(projectRoot);
|
|
67
81
|
info(`Running typecheck across ${String(cfg.projects.length)} project(s): ${cfg.projects.join(', ')}`);
|
|
68
82
|
const results = await runTypecheck(projectRoot, cfg);
|
|
69
83
|
reportResults(projectRoot, results);
|
|
70
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Staleness-checks and regenerates the Furnace-managed jsconfig
|
|
87
|
+
* (`furnace.json` → `typecheckJsconfig`) before typecheck runs. No-op when
|
|
88
|
+
* the project has no furnace.json or no `typecheckJsconfig` is configured.
|
|
89
|
+
* A missing `typecheckJsconfig` file surfaces the reconciler's own clear
|
|
90
|
+
* error rather than producing phantom type diagnostics.
|
|
91
|
+
*/
|
|
92
|
+
async function regenerateStaleGeneratedJsconfig(projectRoot) {
|
|
93
|
+
if (!(await furnaceConfigExists(projectRoot)))
|
|
94
|
+
return;
|
|
95
|
+
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
96
|
+
if (!furnaceConfig.typecheckJsconfig)
|
|
97
|
+
return;
|
|
98
|
+
const drift = await findJsconfigPathsDrift(projectRoot, furnaceConfig);
|
|
99
|
+
if (!drift.changed)
|
|
100
|
+
return;
|
|
101
|
+
info(`Regenerating stale generated jsconfig ${furnaceConfig.typecheckJsconfig} before typecheck ` +
|
|
102
|
+
`(+${String(drift.added.length)} added, ~${String(drift.updated.length)} updated, ` +
|
|
103
|
+
`-${String(drift.pruned.length)} pruned).`);
|
|
104
|
+
await syncFurnaceJsconfigPaths(projectRoot, furnaceConfig);
|
|
105
|
+
}
|
|
71
106
|
/**
|
|
72
107
|
* Prints all issues, computes the per-project + total counts, and
|
|
73
108
|
* throws on errors. Extracted so it can be exercised directly by
|
|
@@ -14,9 +14,23 @@ import { applyAllComponents } from './furnace-apply.js';
|
|
|
14
14
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
15
15
|
import { runFurnaceMutation } from './furnace-operation.js';
|
|
16
16
|
import { cleanStories } from './furnace-stories.js';
|
|
17
|
-
import { generateMozconfig,
|
|
17
|
+
import { generateMozconfig, runMachCapture } from './mach.js';
|
|
18
18
|
/** Path fragments of files whose edits invalidate the recursive-make backend. */
|
|
19
19
|
const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the tail of captured `mach configure` output so the underlying
|
|
22
|
+
* mozbuild failure (e.g. `mozbuild.util.UnsortedError: ... is not in sorted
|
|
23
|
+
* order`) is carried into the thrown `BuildError` instead of being reduced to
|
|
24
|
+
* a bare exit code. mozbuild writes the error and its traceback to stderr;
|
|
25
|
+
* stdout is included as a fallback for shells that interleave the streams.
|
|
26
|
+
* Returns an empty string when nothing useful was captured.
|
|
27
|
+
*/
|
|
28
|
+
function extractMachConfigureError(result) {
|
|
29
|
+
const combined = `${result.stderr}\n${result.stdout}`.trim();
|
|
30
|
+
if (!combined)
|
|
31
|
+
return '';
|
|
32
|
+
return combined.split('\n').slice(-40).join('\n').trim();
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* Returns true when the file path matches a pattern that forces
|
|
22
36
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -65,10 +79,16 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
65
79
|
info(`Backend command: mach configure`);
|
|
66
80
|
const configureSpinner = spinner('Running mach configure...');
|
|
67
81
|
try {
|
|
68
|
-
const
|
|
82
|
+
const captured = await runMachCapture(['configure'], paths.engine);
|
|
83
|
+
const exitCode = captured.exitCode;
|
|
69
84
|
if (exitCode !== 0) {
|
|
70
85
|
configureSpinner.error(`mach configure failed with exit code ${exitCode}`);
|
|
71
|
-
|
|
86
|
+
// Surface the underlying mozbuild error (e.g. UnsortedError) instead
|
|
87
|
+
// of a bare exit code — the generic message hid the actual cause.
|
|
88
|
+
const detail = extractMachConfigureError(captured);
|
|
89
|
+
throw new BuildError(`Backend regeneration failed: mach configure exited with code ${exitCode}.` +
|
|
90
|
+
(detail ? `\n\nmach configure output (tail):\n${detail}` : '') +
|
|
91
|
+
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
72
92
|
}
|
|
73
93
|
else {
|
|
74
94
|
configureSpinner.stop('Backend regenerated successfully (mach configure exit code 0)');
|
|
@@ -243,6 +243,28 @@ const PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS = [
|
|
|
243
243
|
'noUnusedLocals',
|
|
244
244
|
'noUnusedParameters',
|
|
245
245
|
];
|
|
246
|
+
/**
|
|
247
|
+
* Validates the reviewed `paths` mapping: an object of pattern → string[]
|
|
248
|
+
* targets, each pattern carrying at most one `*`. Lets patch-owned modules
|
|
249
|
+
* be typed from their real sources without an ambient stub shim.
|
|
250
|
+
*/
|
|
251
|
+
function parseCheckJsPathsMapping(raw) {
|
|
252
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
253
|
+
throw new ConfigError('Config field "patchLint.checkJsCompilerOptions.paths" must be a plain object');
|
|
254
|
+
}
|
|
255
|
+
const rec = raw;
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [pattern, targets] of Object.entries(rec)) {
|
|
258
|
+
if ((pattern.match(/\*/g) ?? []).length > 1) {
|
|
259
|
+
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths" key "${pattern}" may contain at most one "*"`);
|
|
260
|
+
}
|
|
261
|
+
if (!Array.isArray(targets) || targets.some((t) => typeof t !== 'string')) {
|
|
262
|
+
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths.${pattern}" must be an array of strings`);
|
|
263
|
+
}
|
|
264
|
+
out[pattern] = targets;
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
246
268
|
function parsePatchLintCheckJsCompilerOptions(raw) {
|
|
247
269
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
248
270
|
throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" must be a plain object');
|
|
@@ -251,6 +273,10 @@ function parsePatchLintCheckJsCompilerOptions(raw) {
|
|
|
251
273
|
const allowed = new Set(PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS);
|
|
252
274
|
const out = {};
|
|
253
275
|
for (const key of Object.keys(rec)) {
|
|
276
|
+
if (key === 'paths') {
|
|
277
|
+
out.paths = parseCheckJsPathsMapping(rec[key]);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
254
280
|
if (!allowed.has(key)) {
|
|
255
281
|
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions" has unknown key "${key}"`);
|
|
256
282
|
}
|
|
@@ -375,6 +401,32 @@ function parseTypecheckBlock(rec) {
|
|
|
375
401
|
if (extraShim !== undefined) {
|
|
376
402
|
out.extraShim = parseShimPath(extraShim, 'typecheck.extraShim');
|
|
377
403
|
}
|
|
404
|
+
const overrides = parseTypecheckProjectOverrides(rec.raw('projectOverrides'), projects);
|
|
405
|
+
if (overrides) {
|
|
406
|
+
out.projectOverrides = overrides;
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Validates the optional `typecheck.projectOverrides` map: keys must name a
|
|
412
|
+
* declared project, values must be either `null` (opt out of the shared extra
|
|
413
|
+
* shim) or a contained relative `.d.ts` path (per-project override). Returns
|
|
414
|
+
* `undefined` when the field is absent.
|
|
415
|
+
*/
|
|
416
|
+
function parseTypecheckProjectOverrides(raw, projects) {
|
|
417
|
+
if (raw === undefined)
|
|
418
|
+
return undefined;
|
|
419
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
420
|
+
throw new ConfigError('Config field "typecheck.projectOverrides" must be an object');
|
|
421
|
+
}
|
|
422
|
+
const known = new Set(projects);
|
|
423
|
+
const out = {};
|
|
424
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
425
|
+
if (!known.has(key)) {
|
|
426
|
+
throw new ConfigError(`Config field "typecheck.projectOverrides" key "${key}" does not match any entry in "typecheck.projects"`);
|
|
427
|
+
}
|
|
428
|
+
out[key] = value === null ? null : parseShimPath(value, `typecheck.projectOverrides["${key}"]`);
|
|
429
|
+
}
|
|
378
430
|
return out;
|
|
379
431
|
}
|
|
380
432
|
//# sourceMappingURL=config-validate.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-run action planning for custom-component apply. Extracted from
|
|
3
|
+
* `furnace-apply-helpers.ts` so the apply path and its dry-run mirror each
|
|
4
|
+
* stay within the per-file line budget. Consumed only by that module.
|
|
5
|
+
*/
|
|
6
|
+
import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
|
|
7
|
+
interface DirectoryEntry {
|
|
8
|
+
isFile(): boolean;
|
|
9
|
+
isSymbolicLink?(): boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
|
|
13
|
+
export declare function buildCustomDryRunActions(name: string, componentDir: string, engineDir: string, config: CustomComponentConfig, targetDir: string, entries: DirectoryEntry[], ftlDir: string): Promise<{
|
|
14
|
+
actions: DryRunAction[];
|
|
15
|
+
stepErrors: StepError[];
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|