@hominis/fireforge 0.47.1 → 0.48.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 +54 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +15 -0
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +2 -0
- package/dist/src/utils/process.js +2 -1
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +81 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +4 -3
- package/docs/patch-workflow.md +35 -1
- package/package.json +7 -3
|
@@ -132,12 +132,14 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
132
132
|
const driftCtx = buildForeignDriftContext(options);
|
|
133
133
|
// Hoisted lint context, one per run: queue context + checkJs
|
|
134
134
|
// program + per-patch result cache, shared across every loop iteration.
|
|
135
|
-
|
|
135
|
+
// Taken before the context build, so the elapsed time the progress line
|
|
136
|
+
// reports includes it.
|
|
137
|
+
const startedAt = Date.now();
|
|
138
|
+
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true, new Set(selectedPatches.map((patch) => patch.filename)));
|
|
136
139
|
let reExported = 0;
|
|
137
140
|
const reExportedFilenames = [];
|
|
138
141
|
const failedFilenames = [];
|
|
139
142
|
const progress = spinner('Preparing re-export...');
|
|
140
|
-
const startedAt = Date.now();
|
|
141
143
|
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, async () => {
|
|
142
144
|
for (const [index, patch] of selectedPatches.entries()) {
|
|
143
145
|
const assignedScanFiles = scanFilesByPatch?.get(patch.filename);
|
|
@@ -22,10 +22,21 @@ export interface DirtyEngineConfirmationOptions {
|
|
|
22
22
|
warningMessage: string;
|
|
23
23
|
promptMessage: string;
|
|
24
24
|
cancelMessage: string;
|
|
25
|
+
/**
|
|
26
|
+
* Precomputed dirtiness. A caller that already measured what would be
|
|
27
|
+
* lost (and may count more than the working tree, such as commits made
|
|
28
|
+
* on top of the base) passes it here instead of the `hasChanges` probe.
|
|
29
|
+
*/
|
|
30
|
+
dirty?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Replaces the generic "Engine has uncommitted changes" opening of the
|
|
33
|
+
* non-interactive refusal, so the refusal names what would be lost.
|
|
34
|
+
*/
|
|
35
|
+
refusalDetail?: string;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Checks if the engine has uncommitted changes and prompts for confirmation.
|
|
28
39
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
29
40
|
* Throws in non-interactive mode without --yes.
|
|
30
41
|
*/
|
|
31
|
-
export declare function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, }: DirtyEngineConfirmationOptions): Promise<boolean>;
|
|
42
|
+
export declare function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, dirty, refusalDetail, }: DirtyEngineConfirmationOptions): Promise<boolean>;
|
|
@@ -12,13 +12,16 @@ import { cancel, isCancel, warn } from '../../utils/logger.js';
|
|
|
12
12
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
13
13
|
* Throws in non-interactive mode without --yes.
|
|
14
14
|
*/
|
|
15
|
-
export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, }) {
|
|
16
|
-
if (!(await hasChanges(engineDir))
|
|
15
|
+
export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, dirty, refusalDetail, }) {
|
|
16
|
+
if (yes || !(dirty ?? (await hasChanges(engineDir)))) {
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
19
|
const isInteractive = stdioIsInteractive();
|
|
20
20
|
if (!isInteractive) {
|
|
21
|
-
|
|
21
|
+
const opening = refusalDetail !== undefined
|
|
22
|
+
? `${refusalDetail} Interactive confirmation is not available.`
|
|
23
|
+
: 'Engine has uncommitted changes and interactive confirmation is not available.';
|
|
24
|
+
throw new InvalidArgumentError(`${opening} Run: ${nonInteractiveCommand}`, argumentName);
|
|
22
25
|
}
|
|
23
26
|
warn(warningMessage);
|
|
24
27
|
const confirmed = await confirm({
|
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
* Supports `--continue` (resume after manual fix) and `--abort` (cancel).
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from 'commander';
|
|
14
|
-
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
14
|
+
import { getProjectPaths, loadConfig, loadState } from '../../core/config.js';
|
|
15
15
|
import { assertEngineGitReady } from '../../core/engine-precondition.js';
|
|
16
16
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
17
17
|
import { getHead, resetChanges } from '../../core/git.js';
|
|
18
18
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
19
19
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
20
20
|
import { getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
21
|
+
import { replayQueueIndexOnly } from '../../core/rebase-dry-run.js';
|
|
21
22
|
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
22
23
|
import { GeneralError } from '../../errors/base.js';
|
|
23
|
-
import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
24
|
-
import { info, intro, outro, spinner } from '../../utils/logger.js';
|
|
24
|
+
import { CorruptRebaseSessionError, RebaseDryRunRejectError, RebaseSessionExistsError, } from '../../errors/rebase.js';
|
|
25
|
+
import { info, intro, outro, spinner, warn } from '../../utils/logger.js';
|
|
25
26
|
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
26
|
-
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
27
|
+
import { compareFirefoxVersions, parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
27
28
|
import { handleAbort } from './abort.js';
|
|
28
29
|
import { confirmDirtyEngineReset } from './confirm.js';
|
|
29
30
|
import { handleContinue } from './continue.js';
|
|
@@ -57,23 +58,51 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
57
58
|
if (!manifest || manifest.patches.length === 0) {
|
|
58
59
|
throw new GeneralError('No patches found in manifest. Nothing to rebase.');
|
|
59
60
|
}
|
|
60
|
-
// Determine the "from" version from the
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Determine the "from" version from the patch stamps. A queue can carry
|
|
62
|
+
// several: patches re-exported since the last hop are stamped newer than
|
|
63
|
+
// untouched ones. The oldest stamp is "from", compared semantically (a
|
|
64
|
+
// lexical sort put 153.10.0esr before 153.2.0esr).
|
|
65
|
+
const versionCounts = new Map();
|
|
66
|
+
for (const patch of manifest.patches) {
|
|
67
|
+
const version = getPatchSourceVersion(patch);
|
|
68
|
+
versionCounts.set(version, (versionCounts.get(version) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
const sortedVersions = [...versionCounts.keys()].sort(compareFirefoxVersions);
|
|
64
71
|
const fromVersion = sortedVersions[0] ?? currentVersion;
|
|
65
|
-
const fromProduct =
|
|
66
|
-
|
|
72
|
+
const fromProduct = manifest.patches.find((p) => getPatchSourceVersion(p) === fromVersion)?.sourceProduct ??
|
|
73
|
+
config.firefox.product;
|
|
74
|
+
if (versionCounts.size === 1 && fromVersion === currentVersion) {
|
|
67
75
|
info('All patches already match the current Firefox version. Nothing to rebase.');
|
|
68
76
|
outro('Rebase not needed');
|
|
69
77
|
return;
|
|
70
78
|
}
|
|
71
79
|
info(`Rebasing patches: ${fromVersion} → ${currentVersion}`);
|
|
80
|
+
if (versionCounts.size > 1) {
|
|
81
|
+
info(`Patch stamps: ${sortedVersions.map((v) => `${versionCounts.get(v)} at ${v}`).join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
// The rebase replays onto whatever engine/ holds. The pin is the target,
|
|
84
|
+
// so an engine still on another version means the download step has not
|
|
85
|
+
// run yet and every verdict below would be against the wrong tree.
|
|
86
|
+
const engineVersion = (await loadState(projectRoot)).downloadedVersion;
|
|
87
|
+
if (engineVersion !== undefined && engineVersion !== currentVersion) {
|
|
88
|
+
warn(`engine/ holds ${engineVersion}, not the pinned target ${currentVersion}. ` +
|
|
89
|
+
'Run "fireforge download --force" first, or the rebase applies onto the old source.');
|
|
90
|
+
}
|
|
72
91
|
info(`Found ${manifest.patches.length} patch(es)`);
|
|
73
92
|
info(`Max context-reduction steps (fuzz-like): ${maxFuzz}`);
|
|
74
93
|
if (isDryRun) {
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
const patches = await discoverPatches(paths.patches);
|
|
95
|
+
const replaySpinner = spinner(`Replaying ${patches.length} patch(es) on a private index...`);
|
|
96
|
+
let replay;
|
|
97
|
+
try {
|
|
98
|
+
replay = await replayQueueIndexOnly(paths.engine, patches, maxFuzz);
|
|
99
|
+
replaySpinner.stop('Replay finished; engine/ untouched');
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
replaySpinner.error('Replay failed');
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
reportDryRunReplay(replay);
|
|
77
106
|
return;
|
|
78
107
|
}
|
|
79
108
|
if (!(await confirmDirtyEngineReset({
|
|
@@ -114,6 +143,41 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
114
143
|
// Run the patch loop
|
|
115
144
|
await runPatchLoop(projectRoot, session, paths, maxFuzz, resolveWaitLockSeconds(options.waitLock));
|
|
116
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Prints one line per patch and a tally, then fails the run when any patch
|
|
148
|
+
* rejects: a dry run that exits 0 must mean the real rebase would apply the
|
|
149
|
+
* whole queue.
|
|
150
|
+
*/
|
|
151
|
+
function reportDryRunReplay(replay) {
|
|
152
|
+
let reduced = 0;
|
|
153
|
+
const rejected = [];
|
|
154
|
+
for (const [index, verdict] of replay.verdicts.entries()) {
|
|
155
|
+
const cascade = replay.firstRejectIndex !== undefined && index > replay.firstRejectIndex
|
|
156
|
+
? ' (after an earlier reject; may cascade from it)'
|
|
157
|
+
: '';
|
|
158
|
+
switch (verdict.outcome) {
|
|
159
|
+
case 'clean':
|
|
160
|
+
info(` clean ${verdict.filename}`);
|
|
161
|
+
break;
|
|
162
|
+
case 'reduced-context':
|
|
163
|
+
reduced += 1;
|
|
164
|
+
info(` reduced context ${verdict.filename} (step ${verdict.step}, ${verdict.contextArg})`);
|
|
165
|
+
break;
|
|
166
|
+
case 'reject': {
|
|
167
|
+
rejected.push(verdict.filename);
|
|
168
|
+
const files = verdict.files.length > 0 ? verdict.files.join(', ') : 'see git output';
|
|
169
|
+
warn(` reject ${verdict.filename}: ${files}${cascade}`);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const clean = replay.verdicts.length - reduced - rejected.length;
|
|
175
|
+
info(`[dry-run] ${replay.verdicts.length} patch(es): ${clean} clean, ${reduced} with reduced context, ${rejected.length} rejected.`);
|
|
176
|
+
if (rejected.length > 0) {
|
|
177
|
+
throw new RebaseDryRunRejectError(rejected);
|
|
178
|
+
}
|
|
179
|
+
outro('Dry run complete: every patch applies');
|
|
180
|
+
}
|
|
117
181
|
// ── Public API ──
|
|
118
182
|
/**
|
|
119
183
|
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
@@ -136,7 +200,7 @@ export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
|
|
|
136
200
|
.description('Semi-automated Firefox source version upgrade — apply patches with drift tolerance and re-export')
|
|
137
201
|
.option('--continue', 'Resume after manually resolving a failed patch')
|
|
138
202
|
.option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
|
|
139
|
-
.option('--dry-run', '
|
|
203
|
+
.option('--dry-run', 'Replay the queue onto the engine base commit in a private index and report each patch as clean, reduced-context or reject (exit 6 on any reject); engine/ is not touched')
|
|
140
204
|
.option('--max-fuzz <n>', 'Maximum context-reduction steps for git apply -C<n> (fuzz-like drift tolerance; default: 3)', commanderArgParser((v) => parsePositiveIntegerFlag('--max-fuzz', v)))
|
|
141
205
|
.option('-y, --yes', 'Skip dirty-tree confirmation prompt');
|
|
142
206
|
addWaitLockOption(command).action(withErrorHandling(async (options) => {
|
package/dist/src/commands/run.js
CHANGED
|
@@ -8,6 +8,7 @@ import { assertEngineExists } from '../core/engine-precondition.js';
|
|
|
8
8
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
9
9
|
import { hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke } from '../core/mach.js';
|
|
10
10
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
11
|
+
import { resolveRunProfile, sweepAbandonedRunProfiles, } from '../core/run-profile.js';
|
|
11
12
|
import { compileAllowlistFromFile, compileAllowlistFromStrings, findOverCapAllowlistEntries, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
12
13
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
14
|
import { BuildError } from '../errors/build.js';
|
|
@@ -97,12 +98,38 @@ export async function runCommand(projectRoot, options = {}) {
|
|
|
97
98
|
await warnIfFurnaceStale(projectRoot);
|
|
98
99
|
// Clean stale profile state to prevent silent startup failures
|
|
99
100
|
await cleanDevProfile(paths.engine);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Reclaim temporary profiles a SIGKILLed FireForge could not remove.
|
|
102
|
+
const swept = await sweepAbandonedRunProfiles();
|
|
103
|
+
if (swept > 0)
|
|
104
|
+
verbose(`Removed ${swept} abandoned temporary run profile(s).`);
|
|
105
|
+
const profile = await resolveRunProfile({
|
|
106
|
+
profile: options.profile,
|
|
107
|
+
tempProfile: options.tempProfile,
|
|
108
|
+
smoke: options.smokeExit !== undefined,
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
if (options.smokeExit !== undefined) {
|
|
112
|
+
await runSmokeExit(paths.engine, options, profile);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
info('Launching browser...\n');
|
|
116
|
+
await runInteractive(paths.engine, options, profile);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await profile.dispose();
|
|
103
120
|
}
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Launches the browser under `mach run` for an interactive session.
|
|
124
|
+
* @param engineDir - Path to the engine directory
|
|
125
|
+
* @param options - Run options
|
|
126
|
+
* @param profile - The profile this run launches with
|
|
127
|
+
*/
|
|
128
|
+
async function runInteractive(engineDir, options, profile) {
|
|
129
|
+
const exitCode = await run(engineDir, [
|
|
130
|
+
...(options.headless ? ['--headless'] : []),
|
|
131
|
+
...profile.args,
|
|
132
|
+
]);
|
|
106
133
|
// Exit-code whitelist:
|
|
107
134
|
// 0: clean shutdown
|
|
108
135
|
// 130: SIGINT (Ctrl+C), user-initiated termination
|
|
@@ -121,7 +148,7 @@ export async function runCommand(projectRoot, options = {}) {
|
|
|
121
148
|
* exit contract. The deadline-fires-SIGTERM path is treated as a clean
|
|
122
149
|
* window iff no unallowed errors were observed.
|
|
123
150
|
*/
|
|
124
|
-
async function runSmokeExit(engineDir, options) {
|
|
151
|
+
async function runSmokeExit(engineDir, options, profile) {
|
|
125
152
|
// Windows lacks the POSIX process-group primitives --smoke-exit leans on to
|
|
126
153
|
// SIGTERM the whole mach → python → firefox tree. Running through anyway
|
|
127
154
|
// would only kill the top-level wrapper and orphan Firefox content
|
|
@@ -194,10 +221,17 @@ async function runSmokeExit(engineDir, options) {
|
|
|
194
221
|
};
|
|
195
222
|
warnIfHeadedOnDesktop(options.headless === true);
|
|
196
223
|
info(`Launching browser (smoke-exit after ${smokeExit}s)...\n`);
|
|
224
|
+
if (profile.temporary) {
|
|
225
|
+
info(`Profile: fresh temporary profile (removed after the run; --profile <path> to use a named one)`);
|
|
226
|
+
}
|
|
227
|
+
else if (profile.dir !== undefined) {
|
|
228
|
+
info(`Profile: ${profile.dir}`);
|
|
229
|
+
}
|
|
197
230
|
const startedAt = Date.now();
|
|
198
231
|
let result;
|
|
199
232
|
try {
|
|
200
|
-
|
|
233
|
+
const machArgs = ['run', ...(options.headless ? ['--headless'] : []), ...profile.args];
|
|
234
|
+
result = await runMachSmoke(machArgs, engineDir, {
|
|
201
235
|
smokeTimeoutMs,
|
|
202
236
|
onStdoutLine: (line) => {
|
|
203
237
|
handleLine('stdout', line);
|
|
@@ -358,6 +392,8 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
|
|
|
358
392
|
.option('--console-allow-file <path>', 'Newline-delimited allowlist regex file. Blank lines and # comments are ignored. A "# max-hits: N" comment directly above an entry caps how many lines it may match in one run; more than that fails the run with exit 12.')
|
|
359
393
|
.option('--capture-console <file>', 'Mirror captured console output to <file> for post-exit inspection.')
|
|
360
394
|
.option('--headless', 'Launch the browser with --headless. Recommended for --smoke-exit on a shared desktop: input into a headed smoke window contaminates the console capture.')
|
|
395
|
+
.option('--profile <path>', 'Launch with this profile directory (passed to Firefox as -profile). Left in place after the run. With --smoke-exit it replaces the default fresh profile.')
|
|
396
|
+
.option('--temp-profile', 'Launch with a fresh temporary profile that is removed when the run ends. The default for --smoke-exit.')
|
|
361
397
|
.action(withErrorHandling(
|
|
362
398
|
// `args` is the variadic positional, not a Commander flag, so it
|
|
363
399
|
// never appears in this object.
|
|
@@ -6,7 +6,7 @@ import { assertEngineExists } from '../core/engine-precondition.js';
|
|
|
6
6
|
import { formatEngineSessionLockStatus, readEngineSessionLockStatus, } from '../core/engine-session-lock.js';
|
|
7
7
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
8
8
|
import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
9
|
-
import {
|
|
9
|
+
import { listUntrackedFilesInDirs, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
|
|
10
10
|
import { renderOwnershipTable } from '../core/ownership-table.js';
|
|
11
11
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
12
12
|
import { readProcessCpuSeconds } from '../core/process-cpu-time.js';
|
|
@@ -49,9 +49,11 @@ function renderTruncationBanner(truncations) {
|
|
|
49
49
|
async function expandDirectoryEntries(files, engineDir) {
|
|
50
50
|
const expanded = [];
|
|
51
51
|
const truncations = [];
|
|
52
|
+
const isCollapsedUntracked = (entry) => entry.file.endsWith('/') && entry.status.includes('?');
|
|
53
|
+
const untrackedByDir = await listUntrackedFilesInDirs(engineDir, files.filter(isCollapsedUntracked).map((entry) => entry.file));
|
|
52
54
|
for (const entry of files) {
|
|
53
|
-
if (
|
|
54
|
-
const individualFiles =
|
|
55
|
+
if (isCollapsedUntracked(entry)) {
|
|
56
|
+
const individualFiles = untrackedByDir.get(entry.file) ?? [];
|
|
55
57
|
const cap = getMaxUntrackedFilesPerDir();
|
|
56
58
|
if (individualFiles.length > cap) {
|
|
57
59
|
// Recorded once here, reported once by renderTruncationBanner. The
|
|
@@ -26,6 +26,11 @@ export interface TypecheckCommandOptions {
|
|
|
26
26
|
* issues without waiting for the full set.
|
|
27
27
|
*/
|
|
28
28
|
project?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Build every project from scratch and write no incremental build info
|
|
31
|
+
* (`--no-cache`).
|
|
32
|
+
*/
|
|
33
|
+
noCache?: boolean;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* Resolves the project list to type-check. `--project` wins over
|
|
@@ -88,7 +88,9 @@ async function runTypecheckCommandBody(projectRoot, options) {
|
|
|
88
88
|
// use so typecheck checks against the current workspace.
|
|
89
89
|
await regenerateStaleGeneratedJsconfig(projectRoot);
|
|
90
90
|
info(`Running typecheck across ${cfg.projects.length} project(s): ${cfg.projects.join(', ')}`);
|
|
91
|
-
const results = await runTypecheck(projectRoot, cfg
|
|
91
|
+
const results = await runTypecheck(projectRoot, cfg, {
|
|
92
|
+
...(options.noCache === true ? { noCache: true } : {}),
|
|
93
|
+
});
|
|
92
94
|
// Fold the EXPORT-TIME authority into this pass. Per-patch lint runs
|
|
93
95
|
// checkJs over the queue's patch-owned modules in relative isolation and
|
|
94
96
|
// resolves imported typedefs differently from the whole-project pass, so
|
|
@@ -193,11 +195,15 @@ export function registerTypecheck(program, { getProjectRoot, withErrorHandling }
|
|
|
193
195
|
.command('typecheck')
|
|
194
196
|
.description('Run TypeScript type checking against project-owned jsconfig.json files (CI-grade, whole-project)')
|
|
195
197
|
.option('--project <path>', 'Override typecheck.projects with a single jsconfig.json path (one-off run)')
|
|
198
|
+
.option('--no-cache', 'Check every project from scratch; neither read nor write the incremental build info in .fireforge/typecheck/')
|
|
196
199
|
.action(withErrorHandling(async (options) => {
|
|
197
200
|
const opts = {};
|
|
198
201
|
if (options.project !== undefined) {
|
|
199
202
|
opts.project = options.project;
|
|
200
203
|
}
|
|
204
|
+
if (options.cache === false) {
|
|
205
|
+
opts.noCache = true;
|
|
206
|
+
}
|
|
201
207
|
await typecheckCommand(getProjectRoot(), opts);
|
|
202
208
|
}));
|
|
203
209
|
}
|
|
@@ -58,6 +58,8 @@ export declare function isBackendInvalidatingFile(path: string): boolean;
|
|
|
58
58
|
* 3. Sets up branding directory if not already done
|
|
59
59
|
* 4. Applies Furnace components if furnace.json exists
|
|
60
60
|
* 5. Generates mozconfig
|
|
61
|
+
* 6. Runs `mach configure` when a backend-invalidating input, including
|
|
62
|
+
* the mozconfig just generated, changed since the previous baseline
|
|
61
63
|
*
|
|
62
64
|
* @param projectRoot - Root directory of the project
|
|
63
65
|
* @param paths - Resolved project paths
|
|
@@ -14,6 +14,7 @@ import { isBrandingSetup, setupBranding } from './branding.js';
|
|
|
14
14
|
import { isBuildInputPath, isJarManifestPath } from './build-audit.js';
|
|
15
15
|
import { evaluateJarManifestEscalation, formatJarEscalationNotice, } from './build-jar-escalation.js';
|
|
16
16
|
import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
|
|
17
|
+
import { hashEngineFile } from './coverage-extend.js';
|
|
17
18
|
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
18
19
|
import { applyAllComponents } from './furnace-apply.js';
|
|
19
20
|
import { logApplyWarnings } from './furnace-apply-output.js';
|
|
@@ -231,6 +232,40 @@ async function applyFurnaceBeforeBuild(projectRoot) {
|
|
|
231
232
|
}
|
|
232
233
|
return furnaceApplied;
|
|
233
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Runs the auto-configure step (`mach configure`) against the freshly
|
|
237
|
+
* generated mozconfig, surfacing the underlying mozbuild error on failure.
|
|
238
|
+
* @param engineDir - Path to the engine directory
|
|
239
|
+
* @param triggers - Why the backend is being regenerated, for the notice
|
|
240
|
+
*/
|
|
241
|
+
async function runAutoConfigure(engineDir, triggers) {
|
|
242
|
+
// Warning severity: this is the "why is this run slow" explanation,
|
|
243
|
+
// and an info-level line does not survive an agent output filter that
|
|
244
|
+
// keeps only warnings and errors.
|
|
245
|
+
notice(`Backend config changed; running backend regeneration first (${triggers}). Backend command: mach configure.`);
|
|
246
|
+
const configureSpinner = spinner('Running mach configure...');
|
|
247
|
+
try {
|
|
248
|
+
const captured = await runMachCapture(['configure'], engineDir);
|
|
249
|
+
const exitCode = captured.exitCode;
|
|
250
|
+
if (exitCode !== 0) {
|
|
251
|
+
configureSpinner.error(`mach configure failed with exit code ${exitCode}`);
|
|
252
|
+
// Surface the underlying mozbuild error (e.g. UnsortedError)
|
|
253
|
+
// instead of a bare exit code, since the generic message hid the
|
|
254
|
+
// actual cause, plus any matched mach-error hints (see the helper).
|
|
255
|
+
throw buildConfigureFailureError(captured);
|
|
256
|
+
}
|
|
257
|
+
configureSpinner.stop('Backend regenerated successfully (mach configure exit code 0)');
|
|
258
|
+
info('Backend regeneration succeeded; continuing with build.');
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (error instanceof BuildError) {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
configureSpinner.error('mach configure failed');
|
|
265
|
+
verbose(`Auto-configure error: ${toError(error).message}`);
|
|
266
|
+
throw new BuildError(`Backend regeneration failed while running mach configure: ${toError(error).message}. Build stopped because continuing would hide the real configure failure.`, 'mach configure', error instanceof Error ? error : undefined);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
234
269
|
/**
|
|
235
270
|
* Runs the shared pre-flight steps for build and package commands:
|
|
236
271
|
* 1. Warns (or refuses) about unexported engine drift the writes below
|
|
@@ -239,6 +274,8 @@ async function applyFurnaceBeforeBuild(projectRoot) {
|
|
|
239
274
|
* 3. Sets up branding directory if not already done
|
|
240
275
|
* 4. Applies Furnace components if furnace.json exists
|
|
241
276
|
* 5. Generates mozconfig
|
|
277
|
+
* 6. Runs `mach configure` when a backend-invalidating input, including
|
|
278
|
+
* the mozconfig just generated, changed since the previous baseline
|
|
242
279
|
*
|
|
243
280
|
* @param projectRoot - Root directory of the project
|
|
244
281
|
* @param paths - Resolved project paths
|
|
@@ -259,11 +296,16 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
259
296
|
}
|
|
260
297
|
}
|
|
261
298
|
await reportUnexportedDriftBeforeOverwrite(projectRoot, config, options);
|
|
262
|
-
// Auto-configure: if any backend-invalidating file (moz.build,
|
|
263
|
-
// Makefile.in) changed since the last successful build,
|
|
264
|
-
// before the build step. Prevents incremental builds
|
|
265
|
-
// work against a stale recursive-make backend.
|
|
266
|
-
|
|
299
|
+
// Auto-configure census: if any backend-invalidating file (moz.build,
|
|
300
|
+
// moz.configure, Makefile.in) changed since the last successful build,
|
|
301
|
+
// `mach configure` runs before the build step. Prevents incremental builds
|
|
302
|
+
// from silently skipping work against a stale recursive-make backend. The
|
|
303
|
+
// configure itself waits until the mozconfig below has been regenerated:
|
|
304
|
+
// run earlier, it configured against the previous engine/mozconfig, and a
|
|
305
|
+
// configs/ fix for a failing configure (a pinned --with-macos-sdk) could
|
|
306
|
+
// never take effect because the failure stopped the build before the
|
|
307
|
+
// regeneration.
|
|
308
|
+
const configureTriggers = [];
|
|
267
309
|
let fullBuildRequired = false;
|
|
268
310
|
let fullBuildReason;
|
|
269
311
|
if (options.previousBaseline) {
|
|
@@ -273,35 +315,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
273
315
|
fullBuildRequired = jarEscalation !== undefined;
|
|
274
316
|
fullBuildReason = jarEscalation;
|
|
275
317
|
if (invalidating.length > 0) {
|
|
276
|
-
|
|
277
|
-
// and an info-level line does not survive an agent output filter that
|
|
278
|
-
// keeps only warnings and errors.
|
|
279
|
-
notice(`Backend config changed; running backend regeneration first (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched). Backend command: mach configure.`);
|
|
280
|
-
const configureSpinner = spinner('Running mach configure...');
|
|
281
|
-
try {
|
|
282
|
-
const captured = await runMachCapture(['configure'], paths.engine);
|
|
283
|
-
const exitCode = captured.exitCode;
|
|
284
|
-
if (exitCode !== 0) {
|
|
285
|
-
configureSpinner.error(`mach configure failed with exit code ${exitCode}`);
|
|
286
|
-
// Surface the underlying mozbuild error (e.g. UnsortedError)
|
|
287
|
-
// instead of a bare exit code, since the generic message hid the
|
|
288
|
-
// actual cause, plus any matched mach-error hints (see the helper).
|
|
289
|
-
throw buildConfigureFailureError(captured);
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
configureSpinner.stop('Backend regenerated successfully (mach configure exit code 0)');
|
|
293
|
-
info('Backend regeneration succeeded; continuing with build.');
|
|
294
|
-
reconfigured = true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
if (error instanceof BuildError) {
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
configureSpinner.error('mach configure failed');
|
|
302
|
-
verbose(`Auto-configure error: ${toError(error).message}`);
|
|
303
|
-
throw new BuildError(`Backend regeneration failed while running mach configure: ${toError(error).message}. Build stopped because continuing would hide the real configure failure.`, 'mach configure', error instanceof Error ? error : undefined);
|
|
304
|
-
}
|
|
318
|
+
configureTriggers.push(`${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched`);
|
|
305
319
|
}
|
|
306
320
|
}
|
|
307
321
|
// Clean stories before build to ensure they don't leak into production binary
|
|
@@ -333,14 +347,32 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
333
347
|
const furnaceApplied = await applyFurnaceBeforeBuild(projectRoot);
|
|
334
348
|
// Generate mozconfig
|
|
335
349
|
const mozconfigSpinner = spinner('Generating mozconfig...');
|
|
350
|
+
let mozconfigWritten;
|
|
336
351
|
try {
|
|
337
|
-
await generateMozconfig(paths.configs, paths.engine, config);
|
|
352
|
+
mozconfigWritten = await generateMozconfig(paths.configs, paths.engine, config);
|
|
338
353
|
mozconfigSpinner.stop('mozconfig generated');
|
|
339
354
|
}
|
|
340
355
|
catch (error) {
|
|
341
356
|
mozconfigSpinner.error('Failed to generate mozconfig');
|
|
342
357
|
throw error;
|
|
343
358
|
}
|
|
359
|
+
// A changed mozconfig is itself backend-invalidating. Compare against the
|
|
360
|
+
// hash the last successful build recorded; a baseline from before that
|
|
361
|
+
// field existed falls back to whether this run rewrote the file.
|
|
362
|
+
if (options.previousBaseline) {
|
|
363
|
+
const recorded = options.previousBaseline.mozconfigHash;
|
|
364
|
+
const mozconfigChanged = recorded !== undefined
|
|
365
|
+
? (await hashEngineFile(paths.engine, 'mozconfig')) !== recorded
|
|
366
|
+
: mozconfigWritten;
|
|
367
|
+
if (mozconfigChanged) {
|
|
368
|
+
configureTriggers.push('engine/mozconfig changed since the last build');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
let reconfigured = false;
|
|
372
|
+
if (configureTriggers.length > 0) {
|
|
373
|
+
await runAutoConfigure(paths.engine, configureTriggers.join('; '));
|
|
374
|
+
reconfigured = true;
|
|
375
|
+
}
|
|
344
376
|
return {
|
|
345
377
|
furnaceApplied,
|
|
346
378
|
reconfigured,
|
|
@@ -7,7 +7,7 @@ export declare const FIREFORGE_DIR = ".fireforge";
|
|
|
7
7
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
8
8
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "buildAudit", "test", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
|
|
9
9
|
/** Supported config paths that can be read or set without --force. */
|
|
10
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
10
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchLint.forwardRegistration", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
11
11
|
/**
|
|
12
12
|
* Gets all project paths based on a root directory.
|
|
13
13
|
* @param root - Root directory of the project
|
|
@@ -376,6 +376,13 @@ function parsePatchLintBlock(rec) {
|
|
|
376
376
|
if (fileSizeThresholds !== undefined) {
|
|
377
377
|
out.fileSizeThresholds = parsePatchLintFileSizeThresholds(fileSizeThresholds);
|
|
378
378
|
}
|
|
379
|
+
const forwardRegistration = rec.raw('forwardRegistration');
|
|
380
|
+
if (forwardRegistration !== undefined) {
|
|
381
|
+
if (forwardRegistration !== 'support-files' && forwardRegistration !== 'extended') {
|
|
382
|
+
throw new ConfigError('Config field "patchLint.forwardRegistration" must be one of: support-files, extended');
|
|
383
|
+
}
|
|
384
|
+
out.forwardRegistration = forwardRegistration;
|
|
385
|
+
}
|
|
379
386
|
for (const key of PATCH_LINT_SEVERITY_GATE_KEYS) {
|
|
380
387
|
const gate = parseSeverityGate(rec.raw(key), `patchLint.${key}`);
|
|
381
388
|
if (gate !== undefined)
|
|
@@ -53,11 +53,12 @@ export async function planDiscardBaselines(patchesDir, engineDir, entries) {
|
|
|
53
53
|
if (claimedEntries.length === 0) {
|
|
54
54
|
return planUpstreamDiscards(entries);
|
|
55
55
|
}
|
|
56
|
-
const { computePatched } = await createPatchedContentContext(patchesDir, engineDir);
|
|
56
|
+
const { computePatched, prefetchBase } = await createPatchedContentContext(patchesDir, engineDir);
|
|
57
57
|
const claimedFiles = [
|
|
58
58
|
...new Set(claimedEntries.flatMap((entry) => entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file])),
|
|
59
59
|
];
|
|
60
60
|
const trackedInHead = await listTrackedInHead(engineDir, claimedFiles);
|
|
61
|
+
await prefetchBase(claimedFiles);
|
|
61
62
|
const computeExpected = async (path) => {
|
|
62
63
|
try {
|
|
63
64
|
return await computePatched(path);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, queue-level entry point to the `forward-registration` census.
|
|
3
|
+
*
|
|
4
|
+
* A consumer that keeps its own audit (a ratchet baseline over forward
|
|
5
|
+
* edges, say) runs the rule's own predicate here instead of mirroring it.
|
|
6
|
+
* Lives apart from the rule module because it builds the queue context,
|
|
7
|
+
* which imports the rule back.
|
|
8
|
+
*/
|
|
9
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
10
|
+
import type { ForwardRegistration, ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
11
|
+
/** Options for {@link findForwardRegistrations}. */
|
|
12
|
+
export interface FindForwardRegistrationsOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Which carriers to check. Defaults to `'extended'` (every carrier the
|
|
15
|
+
* rule can resolve), since a census wants the whole picture whatever the
|
|
16
|
+
* lint gate is configured to enforce.
|
|
17
|
+
*/
|
|
18
|
+
scope?: ForwardRegistrationScope;
|
|
19
|
+
/** Project config, for the queue's patchPolicy ordering context. Optional. */
|
|
20
|
+
config?: FireForgeConfig;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lists every undeclared forward registration in a patch queue: a line a
|
|
24
|
+
* patch adds (to a test manifest, jar.mn, moz.build or customElements.js)
|
|
25
|
+
* that names a file only a later-ordered patch creates. Each record carries
|
|
26
|
+
* the paste-and-run `patch staged-dependency` command that declares it.
|
|
27
|
+
*
|
|
28
|
+
* @param patchesDir - The project's `patches/` directory
|
|
29
|
+
* @param options - Scope and optional config
|
|
30
|
+
* @returns One record per (patch, carrier file, created file)
|
|
31
|
+
*/
|
|
32
|
+
export declare function findForwardRegistrations(patchesDir: string, options?: FindForwardRegistrationsOptions): Promise<ForwardRegistration[]>;
|
|
33
|
+
export type { ForwardRegistration, ForwardRegistrationKind, ForwardRegistrationScope, } from './patch-lint-forward-registration-extended.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildPatchQueueContext } from './patch-lint-cross.js';
|
|
2
|
+
import { collectForwardRegistrations } from './patch-lint-forward-registration.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lists every undeclared forward registration in a patch queue: a line a
|
|
5
|
+
* patch adds (to a test manifest, jar.mn, moz.build or customElements.js)
|
|
6
|
+
* that names a file only a later-ordered patch creates. Each record carries
|
|
7
|
+
* the paste-and-run `patch staged-dependency` command that declares it.
|
|
8
|
+
*
|
|
9
|
+
* @param patchesDir - The project's `patches/` directory
|
|
10
|
+
* @param options - Scope and optional config
|
|
11
|
+
* @returns One record per (patch, carrier file, created file)
|
|
12
|
+
*/
|
|
13
|
+
export async function findForwardRegistrations(patchesDir, options = {}) {
|
|
14
|
+
const ctx = await buildPatchQueueContext(patchesDir, options.config);
|
|
15
|
+
return collectForwardRegistrations(ctx, options.scope ?? 'extended');
|
|
16
|
+
}
|