@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +46 -1
- package/dist/src/commands/export.js +52 -113
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +74 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +191 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -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 -12
- package/dist/src/core/git-file-ops.js +84 -3
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +22 -1
- package/dist/src/core/mach.js +27 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- 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-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +122 -0
- package/dist/src/types/config.d.ts +11 -2
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Harness-crash classification for `fireforge test` (field reports C1/C2).
|
|
4
|
+
*
|
|
5
|
+
* The wrapped mach harness exhibits flaky non-test failures that an exit
|
|
6
|
+
* code (or a "did it print a summary" grep) cannot distinguish from real
|
|
7
|
+
* test results:
|
|
8
|
+
*
|
|
9
|
+
* - startup crashes from the mozlog resource monitor on macOS
|
|
10
|
+
* (`AttributeError: 'SystemResourceMonitor' object has no attribute
|
|
11
|
+
* 'poll_interval'`, `host_statistics64(HOST_VM_INFO64) syscall failed`,
|
|
12
|
+
* `(ipc/mig) array not large enough` — a psutil/macOS mismatch) which
|
|
13
|
+
* abort the run before any test executes;
|
|
14
|
+
* - hangs after browser startup that die at the no-output timeout yet
|
|
15
|
+
* still emit a `Passed: 0` summary;
|
|
16
|
+
* - post-green shutdown re-entry, where a fully green run stalls on
|
|
17
|
+
* "must wait for focus" and records "Application shut down (without
|
|
18
|
+
* crashing) in the middle of a test!" as the only unexpected failure.
|
|
19
|
+
*
|
|
20
|
+
* Classification therefore keys on `TEST-START` presence — summary lines
|
|
21
|
+
* never count as proof that tests ran — and recognizes the crash shapes
|
|
22
|
+
* above so the command layer can retry them with a bounded budget instead
|
|
23
|
+
* of reporting phantom test failures (or phantom passes).
|
|
24
|
+
*/
|
|
25
|
+
const TEST_START_PATTERN = /\bTEST-START\b/;
|
|
26
|
+
const UNEXPECTED_LINE_PATTERN = /^.*\bTEST-UNEXPECTED-[A-Z-]+\b.*$/gm;
|
|
27
|
+
const SHUTDOWN_REENTRY_PATTERN = /Application shut down \(without crashing\) in the middle of a test/i;
|
|
28
|
+
const FOCUS_STALL_PATTERN = /must wait for focus/i;
|
|
29
|
+
const TRACEBACK_PATTERN = /Traceback \(most recent call last\)/;
|
|
30
|
+
const NO_OUTPUT_TIMEOUT_PATTERN = /timed out after \d+ seconds with no output/i;
|
|
31
|
+
/**
|
|
32
|
+
* Startup-traceback fingerprints from the mozlog resource monitor / psutil
|
|
33
|
+
* on macOS. Each is matched per-line so the evidence line in the report is
|
|
34
|
+
* the concrete failure, not the whole traceback.
|
|
35
|
+
*/
|
|
36
|
+
const STARTUP_TRACEBACK_SIGNALS = [
|
|
37
|
+
/AttributeError:.*SystemResourceMonitor/,
|
|
38
|
+
/'SystemResourceMonitor' object has no attribute/,
|
|
39
|
+
/poll_interval/,
|
|
40
|
+
/host_statistics64/,
|
|
41
|
+
/HOST_VM_INFO64/,
|
|
42
|
+
/\(ipc\/mig\) array not large enough/,
|
|
43
|
+
/psutil\.[A-Za-z]*Error/,
|
|
44
|
+
];
|
|
45
|
+
function findLine(output, patterns) {
|
|
46
|
+
for (const line of output.split(/\r?\n/)) {
|
|
47
|
+
if (patterns.some((p) => p.test(line)))
|
|
48
|
+
return line.trim();
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Unexpected-failure lines that are NOT the shutdown re-entry artifact. */
|
|
53
|
+
function realUnexpectedFailureLines(output) {
|
|
54
|
+
const matches = output.match(UNEXPECTED_LINE_PATTERN) ?? [];
|
|
55
|
+
return matches.filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Detects the known harness-crash shapes in captured mach output.
|
|
59
|
+
* Returns undefined for anything that looks like a genuine test result.
|
|
60
|
+
*/
|
|
61
|
+
export function detectHarnessCrashSignature(output) {
|
|
62
|
+
const hasTestStart = TEST_START_PATTERN.test(output);
|
|
63
|
+
const realFailures = realUnexpectedFailureLines(output);
|
|
64
|
+
// Startup traceback cluster (resource monitor / psutil). Real test
|
|
65
|
+
// failures take precedence: a traceback printed during teardown of a
|
|
66
|
+
// genuinely failing run must not get the whole run retried.
|
|
67
|
+
if (TRACEBACK_PATTERN.test(output) && realFailures.length === 0) {
|
|
68
|
+
const signalLine = findLine(output, STARTUP_TRACEBACK_SIGNALS);
|
|
69
|
+
if (signalLine) {
|
|
70
|
+
return { reason: 'harness startup traceback (resource monitor/psutil)', line: signalLine };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Post-browser-startup hang: no test ever started, the harness died at
|
|
74
|
+
// the no-output timeout. A trailing "Passed: 0" summary is part of this
|
|
75
|
+
// shape and must not be read as a result.
|
|
76
|
+
if (!hasTestStart) {
|
|
77
|
+
const timeoutLine = findLine(output, [NO_OUTPUT_TIMEOUT_PATTERN]);
|
|
78
|
+
if (timeoutLine) {
|
|
79
|
+
return { reason: 'no-output timeout before any test started', line: timeoutLine };
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
// Post-green shutdown re-entry: every unexpected line is the
|
|
84
|
+
// shutdown-mid-test artifact, the run stalled on focus, and at least one
|
|
85
|
+
// such artifact exists — an otherwise green log.
|
|
86
|
+
const shutdownLine = findLine(output, [SHUTDOWN_REENTRY_PATTERN]);
|
|
87
|
+
if (shutdownLine && realFailures.length === 0 && FOCUS_STALL_PATTERN.test(output)) {
|
|
88
|
+
return { reason: 'post-green shutdown re-entry during harness teardown', line: shutdownLine };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Classifies a completed harness run. The decision tree, in order:
|
|
94
|
+
*
|
|
95
|
+
* 1. A recognized crash signature wins regardless of exit code (the
|
|
96
|
+
* shutdown re-entry shape exits non-zero on an otherwise green run;
|
|
97
|
+
* the hang shape can even exit zero with a `Passed: 0` summary).
|
|
98
|
+
* 2. No `TEST-START` with explicit paths requested means no test ran —
|
|
99
|
+
* `no-tests`, even when the exit code is zero. Summary lines are not
|
|
100
|
+
* trusted as evidence of execution.
|
|
101
|
+
* 3. Exit code zero with tests started is a pass; anything else is a
|
|
102
|
+
* test failure for the regular diagnosis chain.
|
|
103
|
+
*/
|
|
104
|
+
export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
105
|
+
const signature = detectHarnessCrashSignature(output);
|
|
106
|
+
if (signature) {
|
|
107
|
+
return { kind: 'harness-crash', signature };
|
|
108
|
+
}
|
|
109
|
+
if (!TEST_START_PATTERN.test(output) && requestedPaths.length > 0) {
|
|
110
|
+
return { kind: 'no-tests' };
|
|
111
|
+
}
|
|
112
|
+
return exitCode === 0 ? { kind: 'tests-ran-ok' } : { kind: 'test-failures' };
|
|
113
|
+
}
|
|
114
|
+
/** Builds the operator-facing failure message after retries are exhausted. */
|
|
115
|
+
export function buildHarnessCrashMessage(signature, attempts) {
|
|
116
|
+
return (`mach test crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
|
|
117
|
+
`Detected shape: ${signature.reason}\n` +
|
|
118
|
+
`Evidence line: ${signature.line}\n\n` +
|
|
119
|
+
'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
|
|
120
|
+
'shutdown re-entry, or a pre-test hang) rather than a test regression. Re-run the command, ' +
|
|
121
|
+
'raise the retry budget with --harness-retries <n>, or run the file in isolation. ' +
|
|
122
|
+
'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
|
|
123
|
+
'patching it manually does not stick).');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Builds the message for a run that produced no `TEST-START` despite
|
|
127
|
+
* requesting paths — including exit-code-zero runs whose `Passed: 0`
|
|
128
|
+
* summary would otherwise read as a silent false green.
|
|
129
|
+
*/
|
|
130
|
+
export function buildNoTestsRanMessage(exitCode, requestedPaths) {
|
|
131
|
+
const exitNote = exitCode === 0
|
|
132
|
+
? 'The harness exited 0 and may have printed a summary line, but a summary without a single TEST-START is not a test result.'
|
|
133
|
+
: `The harness exited ${exitCode} before any TEST-START line.`;
|
|
134
|
+
return ('mach test finished without starting any of the requested tests.\n\n' +
|
|
135
|
+
`${exitNote}\n\n` +
|
|
136
|
+
`Requested paths: ${requestedPaths.join(', ')}\n\n` +
|
|
137
|
+
'Check that the paths are registered in their test manifest (browser.toml / xpcshell.toml) ' +
|
|
138
|
+
'and that the manifest is reachable from moz.build, then retry.');
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=test-harness-crash.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildBaseline } from './build-baseline.js';
|
|
1
|
+
import type { BuildBaseline } from './build-baseline-types.js';
|
|
2
2
|
/** Result of the stale-build preflight probe. */
|
|
3
3
|
export interface StaleBuildResult {
|
|
4
4
|
/** True when at least one packageable engine file changed since the baseline. */
|
|
@@ -30,53 +30,9 @@ import { toError } from '../utils/errors.js';
|
|
|
30
30
|
import { verbose } from '../utils/logger.js';
|
|
31
31
|
import { isPackageablePath } from './build-audit.js';
|
|
32
32
|
import { readBuildBaseline } from './build-baseline.js';
|
|
33
|
-
import {
|
|
34
|
-
import { git } from './git-base.js';
|
|
35
|
-
import { getUntrackedFiles } from './git-status.js';
|
|
33
|
+
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
36
34
|
/** Cap on the number of changed paths rendered inline. */
|
|
37
35
|
const STALE_PATHS_LIMIT = 10;
|
|
38
|
-
/**
|
|
39
|
-
* Collects engine paths that changed since the baseline SHA plus any
|
|
40
|
-
* workdir modifications. Mirrors the helper inside `build-prepare.ts` but
|
|
41
|
-
* is kept separate so the test-side preflight does not need to pull in
|
|
42
|
-
* the full build-prepare dependency graph (mozconfig generation, furnace
|
|
43
|
-
* apply hooks, …).
|
|
44
|
-
*/
|
|
45
|
-
async function collectChangedEnginePaths(engineDir, baseline) {
|
|
46
|
-
const collected = new Set();
|
|
47
|
-
if (baseline.engineHeadSha) {
|
|
48
|
-
try {
|
|
49
|
-
const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
|
|
50
|
-
for (const line of diff.split('\n')) {
|
|
51
|
-
const trimmed = line.trim();
|
|
52
|
-
if (trimmed)
|
|
53
|
-
collected.add(trimmed);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (!isMissingHeadError(error)) {
|
|
58
|
-
verbose(`Stale-build preflight: could not diff engine against baseline — ${toError(error).message}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
try {
|
|
63
|
-
if (await hasChanges(engineDir)) {
|
|
64
|
-
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
65
|
-
for (const line of worktreeDiff.split('\n')) {
|
|
66
|
-
const trimmed = line.trim();
|
|
67
|
-
if (trimmed)
|
|
68
|
-
collected.add(trimmed);
|
|
69
|
-
}
|
|
70
|
-
for (const untracked of await getUntrackedFiles(engineDir)) {
|
|
71
|
-
collected.add(untracked);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
verbose(`Stale-build preflight: could not enumerate workdir changes — ${toError(error).message}`);
|
|
77
|
-
}
|
|
78
|
-
return [...collected];
|
|
79
|
-
}
|
|
80
36
|
/**
|
|
81
37
|
* Probes the engine tree for packageable changes since the last successful
|
|
82
38
|
* `fireforge build`. Returns a summary the `fireforge test` handler renders
|
|
@@ -92,7 +48,7 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
|
92
48
|
if (!baseline) {
|
|
93
49
|
return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
|
|
94
50
|
}
|
|
95
|
-
const changed = await collectChangedEnginePaths(engineDir, baseline);
|
|
51
|
+
const changed = await collectChangedEnginePaths(engineDir, baseline, 'Stale-build preflight');
|
|
96
52
|
let packageable = changed.filter((path) => isPackageablePath(path)).sort();
|
|
97
53
|
// Content-hash comparison: when the baseline carries a fingerprint set,
|
|
98
54
|
// fold each candidate path through a live re-hash and drop paths whose
|
|
@@ -3,5 +3,12 @@ export interface XpcshellRetryClassification {
|
|
|
3
3
|
xpcshell: readonly string[];
|
|
4
4
|
nonXpcshell: readonly string[];
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
6
|
+
/** Dispatches a (possibly suite-specific) mach test run, mirroring `testWithOutput`. */
|
|
7
|
+
export type TestDispatch = (engineDir: string, testPaths: string[], args: string[], env?: Record<string, string>) => Promise<MachCommandResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Removes a stale xpcshell install symlink and retries the focused mach test
|
|
10
|
+
* once. The retry uses the same `dispatch` (suite-specific or generic) the
|
|
11
|
+
* caller is already running on, so an xpcshell-suite run repairs and re-runs
|
|
12
|
+
* via `mach xpcshell-test` rather than falling back to the generic command.
|
|
13
|
+
*/
|
|
14
|
+
export declare function retryAfterXpcshellSymlinkRepair(engineDir: string, objDir: string | undefined, result: MachCommandResult, classification: XpcshellRetryClassification, normalizedPaths: string[], extraArgs: string[], env?: Record<string, string>, dispatch?: TestDispatch): Promise<MachCommandResult>;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { testWithOutput } from './mach.js';
|
|
3
3
|
import { tryRepairStaleXpcshellTestSymlink } from './test-stale-symlink.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Removes a stale xpcshell install symlink and retries the focused mach test
|
|
6
|
+
* once. The retry uses the same `dispatch` (suite-specific or generic) the
|
|
7
|
+
* caller is already running on, so an xpcshell-suite run repairs and re-runs
|
|
8
|
+
* via `mach xpcshell-test` rather than falling back to the generic command.
|
|
9
|
+
*/
|
|
10
|
+
export async function retryAfterXpcshellSymlinkRepair(engineDir, objDir, result, classification, normalizedPaths, extraArgs, env, dispatch = testWithOutput) {
|
|
6
11
|
if (result.exitCode !== 0 &&
|
|
7
12
|
classification.xpcshell.length > 0 &&
|
|
8
13
|
classification.nonXpcshell.length === 0) {
|
|
9
14
|
const repaired = await tryRepairStaleXpcshellTestSymlink(engineDir, objDir, `${result.stdout}\n${result.stderr}`);
|
|
10
15
|
if (repaired) {
|
|
11
|
-
return
|
|
16
|
+
return env
|
|
17
|
+
? dispatch(engineDir, normalizedPaths, extraArgs, env)
|
|
18
|
+
: dispatch(engineDir, normalizedPaths, extraArgs);
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
return result;
|
|
@@ -111,13 +111,22 @@ export function findDarkRootInsertionIndex(lines) {
|
|
|
111
111
|
}
|
|
112
112
|
if (rootOpenLine === -1)
|
|
113
113
|
return -1;
|
|
114
|
-
// Depth-count starting from the `:root` opener
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Depth-count starting from the `:root` opener; see findBlockCloseIndex.
|
|
115
|
+
return findBlockCloseIndex(stripped, rootOpenLine);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Depth-counts braces from `startLine` (whose lines must already have
|
|
119
|
+
* block comments stripped), returning the index of the line on which the
|
|
120
|
+
* block opened there returns to its entry depth — i.e. the line carrying
|
|
121
|
+
* the block's closing `}` — or -1 when the block never closes. The first
|
|
122
|
+
* `{` encountered sets the entry depth, so the scan may start on the
|
|
123
|
+
* selector/at-rule line itself rather than on the opener.
|
|
124
|
+
*/
|
|
125
|
+
function findBlockCloseIndex(stripped, startLine) {
|
|
117
126
|
let depth = 0;
|
|
118
127
|
let entryDepth = 0;
|
|
119
128
|
let enteredBlock = false;
|
|
120
|
-
for (let i =
|
|
129
|
+
for (let i = startLine; i < stripped.length; i++) {
|
|
121
130
|
const line = stripped[i] ?? '';
|
|
122
131
|
for (const ch of line) {
|
|
123
132
|
if (ch === '{') {
|
|
@@ -156,27 +165,6 @@ export function findDarkMediaCloseIndex(lines) {
|
|
|
156
165
|
}
|
|
157
166
|
if (darkMediaLine === -1)
|
|
158
167
|
return -1;
|
|
159
|
-
|
|
160
|
-
let entryDepth = 0;
|
|
161
|
-
let enteredBlock = false;
|
|
162
|
-
for (let i = darkMediaLine; i < stripped.length; i++) {
|
|
163
|
-
const line = stripped[i] ?? '';
|
|
164
|
-
for (const ch of line) {
|
|
165
|
-
if (ch === '{') {
|
|
166
|
-
depth++;
|
|
167
|
-
if (!enteredBlock) {
|
|
168
|
-
entryDepth = depth - 1;
|
|
169
|
-
enteredBlock = true;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (ch === '}') {
|
|
173
|
-
depth--;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (enteredBlock && depth === entryDepth) {
|
|
177
|
-
return i;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return -1;
|
|
168
|
+
return findBlockCloseIndex(stripped, darkMediaLine);
|
|
181
169
|
}
|
|
182
170
|
//# sourceMappingURL=token-dark-mode.js.map
|
|
@@ -20,6 +20,8 @@ export interface AddTokenOptions {
|
|
|
20
20
|
darkValue?: string | undefined;
|
|
21
21
|
/** Dry run mode */
|
|
22
22
|
dryRun?: boolean | undefined;
|
|
23
|
+
/** Declare the category banner in the tokens CSS when it does not exist yet. */
|
|
24
|
+
createCategory?: boolean | undefined;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Result of adding a token.
|
|
@@ -35,6 +37,8 @@ export interface AddTokenResult {
|
|
|
35
37
|
countUpdated: boolean;
|
|
36
38
|
/** Whether the operation was skipped (already exists) */
|
|
37
39
|
skipped: boolean;
|
|
40
|
+
/** Whether a new category banner was declared by this add. */
|
|
41
|
+
categoryCreated?: boolean;
|
|
38
42
|
}
|
|
39
43
|
/** Returns the token CSS path relative to engine root for a given binary name. */
|
|
40
44
|
export declare function getTokensCssPath(binaryName: string): string;
|
|
@@ -69,39 +69,54 @@ function validateDarkValue(options) {
|
|
|
69
69
|
throw new InvalidArgumentError('Override mode requires --dark-value to be specified.', 'darkValue');
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const lines = content.split('\n');
|
|
72
|
+
/**
|
|
73
|
+
* True when `lines` contain a category header (single-line or multi-line
|
|
74
|
+
* banner shape) naming `category`. Shared by the pre-add assertion and the
|
|
75
|
+
* in-memory banner creation path so both agree on what "exists" means.
|
|
76
|
+
*/
|
|
77
|
+
function categoryHeaderExists(lines, category) {
|
|
79
78
|
const escapedCategory = escapeRegex(category);
|
|
80
79
|
const singleLinePattern = new RegExp(`\\/\\*\\s*=.*${escapedCategory}.*=\\s*\\*\\/`);
|
|
81
80
|
for (let i = 0; i < lines.length; i++) {
|
|
82
81
|
const line = lines[i] ?? '';
|
|
83
82
|
if (singleLinePattern.test(line)) {
|
|
84
|
-
return;
|
|
83
|
+
return true;
|
|
85
84
|
}
|
|
86
85
|
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
87
86
|
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
88
87
|
const blockLine = lines[j] ?? '';
|
|
89
88
|
if (new RegExp(escapedCategory).test(blockLine)) {
|
|
90
|
-
return;
|
|
89
|
+
return true;
|
|
91
90
|
}
|
|
92
91
|
if (/\*\//.test(blockLine))
|
|
93
92
|
break;
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
async function assertTokenCategoryExists(engineDir, tokensCssPath, category, createCategory = false) {
|
|
99
|
+
const filePath = join(engineDir, tokensCssPath);
|
|
100
|
+
if (!(await pathExists(filePath))) {
|
|
101
|
+
throw new GeneralError(`Token CSS file not found: ${tokensCssPath}`);
|
|
102
|
+
}
|
|
103
|
+
const content = await readText(filePath);
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
if (categoryHeaderExists(lines, category))
|
|
106
|
+
return;
|
|
107
|
+
// The write path declares the banner in the same edit as the token
|
|
108
|
+
// insertion, so a missing category is fine when creation was requested.
|
|
109
|
+
if (createCategory)
|
|
110
|
+
return;
|
|
97
111
|
const discoveredCategories = discoverCategoryHeaders(lines);
|
|
98
112
|
const available = discoveredCategories.length > 0
|
|
99
113
|
? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
|
|
100
|
-
: 'The file currently has no category headers.
|
|
114
|
+
: 'The file currently has no category headers.';
|
|
101
115
|
throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
|
|
102
116
|
`${available}\n\n` +
|
|
103
117
|
'Categories are declared by comment headers. Single-line shape: /* = My Category = */. ' +
|
|
104
|
-
'Multi-line shape: /* =============\\n * My Category\\n * =============
|
|
118
|
+
'Multi-line shape: /* =============\\n * My Category\\n * ============= */.\n\n' +
|
|
119
|
+
'Re-run with --create-category to declare the banner and insert the token in one step.');
|
|
105
120
|
}
|
|
106
121
|
/**
|
|
107
122
|
* Scans a tokens CSS file for category header comments and returns the
|
|
@@ -156,7 +171,7 @@ export async function validateTokenAdd(root, options) {
|
|
|
156
171
|
validateTokenNameSyntax(options.tokenName);
|
|
157
172
|
await validateTokenPrefix(root, options);
|
|
158
173
|
validateDarkValue(options);
|
|
159
|
-
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category);
|
|
174
|
+
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
|
|
160
175
|
}
|
|
161
176
|
/**
|
|
162
177
|
* Adds a design token to the CSS file and documentation.
|
|
@@ -185,7 +200,7 @@ export async function addToken(root, options) {
|
|
|
185
200
|
};
|
|
186
201
|
}
|
|
187
202
|
// --- CSS file ---
|
|
188
|
-
const cssAdded = await addTokenToCSS(engineDir, options, tokensCssPath);
|
|
203
|
+
const { added: cssAdded, categoryCreated } = await addTokenToCSS(engineDir, options, tokensCssPath);
|
|
189
204
|
if (!cssAdded) {
|
|
190
205
|
return {
|
|
191
206
|
cssAdded: false,
|
|
@@ -203,8 +218,39 @@ export async function addToken(root, options) {
|
|
|
203
218
|
unmappedAdded: docsResult.unmappedAdded,
|
|
204
219
|
countUpdated: docsResult.countUpdated,
|
|
205
220
|
skipped: false,
|
|
221
|
+
categoryCreated,
|
|
206
222
|
};
|
|
207
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Splices a new single-line category banner ("= Name =" comment shape, the
|
|
226
|
+
* same format `discoverCategoryHeaders` recognises) just before the closing
|
|
227
|
+
* brace of the `:root` block, making the new category the last section.
|
|
228
|
+
* Mutates `lines` in place.
|
|
229
|
+
*/
|
|
230
|
+
function declareCategoryBanner(lines, category, tokensCssPath) {
|
|
231
|
+
let rootOpen = -1;
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
if (/:root\s*\{/.test(lines[i] ?? '')) {
|
|
234
|
+
rootOpen = i;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (rootOpen === -1) {
|
|
239
|
+
throw new GeneralError(`Cannot create category "${category}": no :root block found in ${tokensCssPath}. ` +
|
|
240
|
+
'Run "fireforge furnace init --force" to re-scaffold the tokens CSS file.');
|
|
241
|
+
}
|
|
242
|
+
let rootClose = -1;
|
|
243
|
+
for (let i = rootOpen + 1; i < lines.length; i++) {
|
|
244
|
+
if (/^\s*\}/.test(lines[i] ?? '')) {
|
|
245
|
+
rootClose = i;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (rootClose === -1) {
|
|
250
|
+
throw new GeneralError(`Cannot create category "${category}": the :root block in ${tokensCssPath} never closes.`);
|
|
251
|
+
}
|
|
252
|
+
lines.splice(rootClose, 0, '', ` /* = ${category} = */`);
|
|
253
|
+
}
|
|
208
254
|
function findCategorySection(lines, category, tokensCssPath) {
|
|
209
255
|
const escapedCategory = escapeRegex(category);
|
|
210
256
|
const singleLinePattern = new RegExp(`\\/\\*\\s*=.*${escapedCategory}.*=\\s*\\*\\/`);
|
|
@@ -296,15 +342,23 @@ function insertDarkModeOverride(lines, options) {
|
|
|
296
342
|
*/
|
|
297
343
|
async function addTokenToCSS(engineDir, options, tokensCssPath) {
|
|
298
344
|
const filePath = join(engineDir, tokensCssPath);
|
|
299
|
-
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category);
|
|
345
|
+
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
|
|
300
346
|
let content = await readText(filePath);
|
|
301
347
|
// Idempotency check — strip CSS block comments so we don't match inside them
|
|
302
348
|
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
303
349
|
if (stripped.includes(options.tokenName + ':')) {
|
|
304
|
-
return false;
|
|
350
|
+
return { added: false, categoryCreated: false };
|
|
305
351
|
}
|
|
306
352
|
const lines = content.split('\n');
|
|
307
353
|
const annotation = getModeAnnotation(options.mode, options.value);
|
|
354
|
+
// Declare a missing category banner in the same in-memory edit as the
|
|
355
|
+
// token insertion — the file is written exactly once, so a failure
|
|
356
|
+
// between "banner declared" and "token inserted" cannot occur.
|
|
357
|
+
let categoryCreated = false;
|
|
358
|
+
if (options.createCategory === true && !categoryHeaderExists(lines, options.category)) {
|
|
359
|
+
declareCategoryBanner(lines, options.category, tokensCssPath);
|
|
360
|
+
categoryCreated = true;
|
|
361
|
+
}
|
|
308
362
|
const { categoryLine, sectionEnd } = findCategorySection(lines, options.category, tokensCssPath);
|
|
309
363
|
// Build the insertion lines
|
|
310
364
|
const insertLines = [];
|
|
@@ -325,7 +379,7 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
|
|
|
325
379
|
insertDarkModeOverride(lines, options);
|
|
326
380
|
content = lines.join('\n');
|
|
327
381
|
await writeText(filePath, content);
|
|
328
|
-
return true;
|
|
382
|
+
return { added: true, categoryCreated };
|
|
329
383
|
}
|
|
330
384
|
/**
|
|
331
385
|
* Strips surrounding backticks from a cell, if present. Token cells are
|
|
@@ -11,27 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Filename used for the synthetic Firefox-globals shim source file. */
|
|
13
13
|
export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
|
|
14
|
-
/**
|
|
15
|
-
* Minimal `.d.ts` shim for Firefox privileged-scope globals.
|
|
16
|
-
*
|
|
17
|
-
* Firefox source is plain JS — no TypeScript allowed. The shim lets
|
|
18
|
-
* TS-driven type checking run without reporting "cannot find name"
|
|
19
|
-
* for the most common Mozilla APIs. Types are intentionally loose
|
|
20
|
-
* (`any`) because full Firefox type coverage is out of scope.
|
|
21
|
-
*
|
|
22
|
-
* Notable patterns that require shimming:
|
|
23
|
-
* - `const lazy = {};` + `ChromeUtils.defineESModuleGetters(lazy, { ... })`
|
|
24
|
-
* populates `lazy` at runtime; we declare it as `Record<string, any>`.
|
|
25
|
-
* - `Services.obs`, `Services.prefs`, etc. are XPCOM service accessors.
|
|
26
|
-
* - `Ci`, `Cc`, `Cr`, `Cu` are XPCOM component shortcuts.
|
|
27
|
-
* - Browser chrome globals like `gBrowser`, `gURLBar` are common in
|
|
28
|
-
* content scripts wired via `browser.js`.
|
|
29
|
-
* - Dynamic `import("resource:-…")` / `import("chrome:-…")` under patch
|
|
30
|
-
* checkJs: the compiler sees empty stubs (`noResolve`); without URL
|
|
31
|
-
* ambient modules namespaces degrade to unusable typings. Wildcards
|
|
32
|
-
* keep Firefox URL imports pragmatically loose, same posture as globals.
|
|
33
|
-
*/
|
|
34
|
-
export declare const FIREFOX_GLOBALS_SHIM = "\ndeclare var Services: any;\ndeclare var ChromeUtils: {\n defineESModuleGetters(target: any, modules: Record<string, string>): void;\n importESModule(specifier: string): any;\n import(specifier: string): any;\n defineModuleGetter(target: any, name: string, specifier: string): void;\n generateQI(interfaces: any[]): Function;\n isClassInfo(obj: any): boolean;\n};\ndeclare var Cu: any;\ndeclare var Ci: any;\ndeclare var Cc: any;\ndeclare var Cr: any;\ndeclare var Components: any;\ndeclare var XPCOMUtils: any;\ndeclare var lazy: Record<string, any>;\ndeclare var PathUtils: any;\ndeclare var IOUtils: any;\ndeclare var FileUtils: any;\ndeclare var gBrowser: any;\ndeclare var gURLBar: any;\ndeclare var gNavigatorBundle: any;\ndeclare var AppConstants: any;\n\n// Shorthand ambient modules \u2014 exports from matching URL imports are loosely typed,\n// avoiding noResolve empty-graph namespaces. (Named member access broke when we tried\n// export= Record under moduleResolution Bundler.)\ndeclare module 'resource:*';\ndeclare module 'chrome:*';\n\n";
|
|
35
14
|
/**
|
|
36
15
|
* TS diagnostic codes suppressed by both the patch-lint checkJs pass
|
|
37
16
|
* and the whole-project typecheck command. Each is a known false
|
|
@@ -61,7 +40,9 @@ export interface ComposedShim {
|
|
|
61
40
|
* direction is intentional (declarations later in concat order
|
|
62
41
|
* augment earlier ones), so a project that wants to refine `Services`
|
|
63
42
|
* with a more specific type can do so by declaring it in the extra
|
|
64
|
-
* shim.
|
|
43
|
+
* shim. Any triple-slash `/// <reference path="…">` directives inside the
|
|
44
|
+
* extra shim are inlined (resolved against the extra shim's own directory)
|
|
45
|
+
* so they are not silently dropped at the synthetic shim path.
|
|
65
46
|
*
|
|
66
47
|
* Missing extra-shim files raise a clear error rather than failing
|
|
67
48
|
* silently with a confusing "type not found" downstream — this is the
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* still fail `fireforge typecheck`, or vice versa, for reasons the
|
|
11
11
|
* operator could not infer from the rule names.
|
|
12
12
|
*/
|
|
13
|
-
import { resolve } from 'node:path';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
14
|
import { pathExists, readText } from '../utils/fs.js';
|
|
15
15
|
/** Filename used for the synthetic Firefox-globals shim source file. */
|
|
16
16
|
export const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
|
|
@@ -30,18 +30,21 @@ export const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
|
|
|
30
30
|
* - Browser chrome globals like `gBrowser`, `gURLBar` are common in
|
|
31
31
|
* content scripts wired via `browser.js`.
|
|
32
32
|
* - Dynamic `import("resource:-…")` / `import("chrome:-…")` under patch
|
|
33
|
-
* checkJs:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* checkJs: imports of *patch-owned* modules resolve to their real
|
|
34
|
+
* sources (see `patch-lint-checkjs.ts`); everything else fails host
|
|
35
|
+
* resolution and lands on these URL ambient wildcards, keeping
|
|
36
|
+
* upstream Firefox imports pragmatically loose, same posture as globals.
|
|
36
37
|
*/
|
|
37
|
-
|
|
38
|
+
const FIREFOX_GLOBALS_SHIM = `
|
|
38
39
|
declare var Services: any;
|
|
39
40
|
declare var ChromeUtils: {
|
|
40
41
|
defineESModuleGetters(target: any, modules: Record<string, string>): void;
|
|
41
42
|
importESModule(specifier: string): any;
|
|
42
43
|
import(specifier: string): any;
|
|
43
44
|
defineModuleGetter(target: any, name: string, specifier: string): void;
|
|
45
|
+
defineLazyGetter(target: any, name: string, getter: () => any): void;
|
|
44
46
|
generateQI(interfaces: any[]): Function;
|
|
47
|
+
getClassName(obj: any, unwrap?: boolean): string;
|
|
45
48
|
isClassInfo(obj: any): boolean;
|
|
46
49
|
};
|
|
47
50
|
declare var Cu: any;
|
|
@@ -58,6 +61,25 @@ declare var gBrowser: any;
|
|
|
58
61
|
declare var gURLBar: any;
|
|
59
62
|
declare var gNavigatorBundle: any;
|
|
60
63
|
declare var AppConstants: any;
|
|
64
|
+
// Fluent localization — a stable chrome global. Members stay loose (any),
|
|
65
|
+
// but the constructor shape is declared so "new Localization([...])" and
|
|
66
|
+
// "new Localization([...], true)" typecheck without a local cast.
|
|
67
|
+
declare var Localization: {
|
|
68
|
+
new (
|
|
69
|
+
resourceIds: Array<string | { path: string; optional?: boolean }>,
|
|
70
|
+
sync?: boolean
|
|
71
|
+
): {
|
|
72
|
+
formatValue(id: string, args?: Record<string, unknown>): any;
|
|
73
|
+
formatValues(keys: any[]): any;
|
|
74
|
+
formatMessages(keys: any[]): any;
|
|
75
|
+
formatValueSync(id: string, args?: Record<string, unknown>): any;
|
|
76
|
+
formatValuesSync(keys: any[]): any;
|
|
77
|
+
formatMessagesSync(keys: any[]): any;
|
|
78
|
+
addResourceIds(ids: Array<string | { path: string; optional?: boolean }>): void;
|
|
79
|
+
removeResourceIds(ids: string[]): number;
|
|
80
|
+
setAsync(): void;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
// Shorthand ambient modules — exports from matching URL imports are loosely typed,
|
|
63
85
|
// avoiding noResolve empty-graph namespaces. (Named member access broke when we tried
|
|
@@ -88,6 +110,43 @@ export const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
|
|
|
88
110
|
2580, // Cannot find name '{0}'. Do you need to install type definitions...
|
|
89
111
|
7016, // Could not find a declaration file for module '{0}'.
|
|
90
112
|
]);
|
|
113
|
+
/** Matches a lone triple-slash `/// <reference path="…" />` directive line. */
|
|
114
|
+
const TRIPLE_SLASH_REFERENCE = /^\s*\/\/\/\s*<reference\s+path\s*=\s*["']([^"']+)["']\s*\/?>\s*$/;
|
|
115
|
+
/**
|
|
116
|
+
* Inlines triple-slash `/// <reference path="…">` directives in shim source.
|
|
117
|
+
*
|
|
118
|
+
* Both shim consumers feed the text to the compiler at a *synthetic* path
|
|
119
|
+
* (an in-memory source file, not the extra shim's real location), so TS
|
|
120
|
+
* resolves a relative `/// <reference>` against that synthetic directory and
|
|
121
|
+
* silently drops it. Inlining the referenced file's contents (recursively,
|
|
122
|
+
* resolved against the *referencing* file's directory, deduped by absolute
|
|
123
|
+
* path) makes the directives self-contained so their declarations survive.
|
|
124
|
+
*
|
|
125
|
+
* @param source - Shim source possibly containing reference directives
|
|
126
|
+
* @param baseDir - Directory the directives' relative paths resolve against
|
|
127
|
+
* @param seen - Absolute paths already inlined (cycle / duplicate guard)
|
|
128
|
+
*/
|
|
129
|
+
async function inlineTripleSlashReferences(source, baseDir, seen) {
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const line of source.split('\n')) {
|
|
132
|
+
const match = TRIPLE_SLASH_REFERENCE.exec(line);
|
|
133
|
+
if (!match?.[1]) {
|
|
134
|
+
out.push(line);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const absolute = resolve(baseDir, match[1]);
|
|
138
|
+
if (seen.has(absolute))
|
|
139
|
+
continue;
|
|
140
|
+
seen.add(absolute);
|
|
141
|
+
if (!(await pathExists(absolute))) {
|
|
142
|
+
out.push(`// (fireforge: unresolved /// <reference path="${match[1]}">)`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const referenced = await readText(absolute);
|
|
146
|
+
out.push(await inlineTripleSlashReferences(referenced, dirname(absolute), seen));
|
|
147
|
+
}
|
|
148
|
+
return out.join('\n');
|
|
149
|
+
}
|
|
91
150
|
/**
|
|
92
151
|
* Composes the synthetic shim source by concatenating the built-in
|
|
93
152
|
* Firefox globals shim with the contents of an optional user-supplied
|
|
@@ -95,7 +154,9 @@ export const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
|
|
|
95
154
|
* direction is intentional (declarations later in concat order
|
|
96
155
|
* augment earlier ones), so a project that wants to refine `Services`
|
|
97
156
|
* with a more specific type can do so by declaring it in the extra
|
|
98
|
-
* shim.
|
|
157
|
+
* shim. Any triple-slash `/// <reference path="…">` directives inside the
|
|
158
|
+
* extra shim are inlined (resolved against the extra shim's own directory)
|
|
159
|
+
* so they are not silently dropped at the synthetic shim path.
|
|
99
160
|
*
|
|
100
161
|
* Missing extra-shim files raise a clear error rather than failing
|
|
101
162
|
* silently with a confusing "type not found" downstream — this is the
|
|
@@ -115,8 +176,9 @@ export async function composeShimSource(projectRoot, extraShimPath) {
|
|
|
115
176
|
'Check the path in fireforge.json or create the file.');
|
|
116
177
|
}
|
|
117
178
|
const extra = await readText(absoluteShim);
|
|
179
|
+
const inlinedExtra = await inlineTripleSlashReferences(extra, dirname(absoluteShim), new Set([absoluteShim]));
|
|
118
180
|
return {
|
|
119
|
-
source: `${FIREFOX_GLOBALS_SHIM}\n// ── extraShim: ${extraShimPath} ──\n${
|
|
181
|
+
source: `${FIREFOX_GLOBALS_SHIM}\n// ── extraShim: ${extraShimPath} ──\n${inlinedExtra}`,
|
|
120
182
|
extraShimAppended: true,
|
|
121
183
|
};
|
|
122
184
|
}
|