@hominis/fireforge 0.50.0 → 0.52.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 +67 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +220 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +11 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -1
- package/dist/src/core/typecheck-shim.js +139 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +29 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
5
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
5
6
|
import { countUnlabelledIconButtons } from './furnace-validate-icon-button.js';
|
|
6
7
|
import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Reads the composed component: `<tagName>.mjs` followed by its companion
|
|
10
|
+
* modules (`<tagName>-*.mjs`, tests and stories left out), so a template's
|
|
11
|
+
* findings do not depend on which of the component's modules it lives in.
|
|
12
|
+
* Returns undefined when the host module is missing.
|
|
13
|
+
*/
|
|
14
|
+
async function readComposedComponentModules(componentDir, tagName) {
|
|
15
|
+
const hostName = `${tagName}.mjs`;
|
|
16
|
+
const hostPath = join(componentDir, hostName);
|
|
17
|
+
if (!(await pathExists(hostPath)))
|
|
18
|
+
return undefined;
|
|
19
|
+
const entries = await readdir(componentDir).catch(() => []);
|
|
20
|
+
const companions = entries
|
|
21
|
+
.filter((name) => name.startsWith(`${tagName}-`) &&
|
|
22
|
+
name.endsWith('.mjs') &&
|
|
23
|
+
!/\.(test|spec|stories)\./.test(name))
|
|
24
|
+
.sort();
|
|
25
|
+
const modules = [{ name: hostName, content: await readText(hostPath) }];
|
|
26
|
+
for (const name of companions) {
|
|
27
|
+
modules.push({ name, content: await readText(join(componentDir, name)) });
|
|
28
|
+
}
|
|
29
|
+
return modules;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates accessibility patterns in a component's composed source: its
|
|
33
|
+
* host module and every companion module (see
|
|
34
|
+
* {@link readComposedComponentSource}). Checks for ARIA roles, keyboard
|
|
35
|
+
* handlers, l10n, and focus delegation.
|
|
10
36
|
*
|
|
11
37
|
* @param componentDir - Directory holding the component's sources
|
|
12
38
|
* @param tagName - Custom element tag name, used to locate `<tagName>.mjs`
|
|
@@ -20,10 +46,10 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
|
|
|
20
46
|
* adds the icon-only `<moz-button>` naming check.
|
|
21
47
|
*/
|
|
22
48
|
export async function validateAccessibility(componentDir, tagName, customConfig, optInChecks = []) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
49
|
+
const modules = await readComposedComponentModules(componentDir, tagName);
|
|
50
|
+
if (modules === undefined)
|
|
25
51
|
return [];
|
|
26
|
-
const content =
|
|
52
|
+
const content = modules.map((module) => module.content).join('\n');
|
|
27
53
|
const issues = [];
|
|
28
54
|
if (!hasAriaRole(content) && hasGenericInteractiveElement(content)) {
|
|
29
55
|
issues.push(createIssue(tagName, 'warning', 'no-aria-role', 'Generic interactive markup has no native semantics. Prefer native elements, or add role= when native markup cannot provide the semantics.'));
|
|
@@ -41,13 +67,24 @@ export async function validateAccessibility(componentDir, tagName, customConfig,
|
|
|
41
67
|
// activation flows through the inner element and a duplicate handler on
|
|
42
68
|
// the wrapper would either no-op or fire twice alongside the child's
|
|
43
69
|
// built-in keyboard path.
|
|
44
|
-
const
|
|
70
|
+
const syntheticTags = syntheticClickTags(content);
|
|
45
71
|
const keyboardCovered = isKeyboardCoveredByComposition(customConfig);
|
|
46
|
-
if (
|
|
47
|
-
issues.push(
|
|
72
|
+
if (syntheticTags.length > 0 && !hasKeyboardHandler && !keyboardCovered) {
|
|
73
|
+
issues.push({
|
|
74
|
+
...createIssue(tagName, 'warning', 'no-keyboard-handler', `Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup), on ${syntheticTags.map((tag) => `<${tag}>`).join(', ')}.`),
|
|
75
|
+
tags: syntheticTags,
|
|
76
|
+
});
|
|
48
77
|
}
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
// Judged per module, so the finding names its files and an exception can
|
|
79
|
+
// accept one module (a vendored bundle) without muting the others.
|
|
80
|
+
const textFiles = modules
|
|
81
|
+
.filter((module) => containsHardcodedTemplateText(module.content))
|
|
82
|
+
.map((module) => module.name);
|
|
83
|
+
if (textFiles.length > 0) {
|
|
84
|
+
issues.push({
|
|
85
|
+
...createIssue(tagName, 'warning', 'hardcoded-text', `Possible hardcoded string found in ${textFiles.join(', ')}. Use data-l10n-id for localization.`),
|
|
86
|
+
files: textFiles,
|
|
87
|
+
});
|
|
51
88
|
}
|
|
52
89
|
const isInteractive = hasClick || hasKeyboardHandler;
|
|
53
90
|
if (isInteractive && !hasDelegatesFocusEnabled(content)) {
|
|
@@ -164,6 +201,9 @@ const NATIVE_CLICK_INTERACTIVE_TAGS = new Set([
|
|
|
164
201
|
'moz-radio',
|
|
165
202
|
'moz-radio-group',
|
|
166
203
|
'moz-menulist',
|
|
204
|
+
// Toolkit's panel-list row renders a button that takes Enter and Space
|
|
205
|
+
// and dispatches click.
|
|
206
|
+
'panel-item',
|
|
167
207
|
]);
|
|
168
208
|
/**
|
|
169
209
|
* Returns true when `customConfig` declares that the component's keyboard
|
|
@@ -185,28 +225,33 @@ function isKeyboardCoveredByComposition(customConfig) {
|
|
|
185
225
|
return composes.some((tag) => NATIVE_CLICK_INTERACTIVE_TAGS.has(tag));
|
|
186
226
|
}
|
|
187
227
|
/**
|
|
188
|
-
* Returns
|
|
189
|
-
*
|
|
190
|
-
* keyboard activation and therefore needs an explicit key
|
|
191
|
-
* Enter/Space.
|
|
192
|
-
* interactive element. Those already fire `click` on keyboard
|
|
228
|
+
* Returns the tags (in first-seen order) of every *synthetic* interactive
|
|
229
|
+
* element carrying an `@click=${...}` handler (e.g. `<div @click>`), which
|
|
230
|
+
* lacks native keyboard activation and therefore needs an explicit key
|
|
231
|
+
* handler for Enter/Space. Empty when every `@click` handler sits on a
|
|
232
|
+
* native interactive element. Those already fire `click` on keyboard
|
|
233
|
+
* activation.
|
|
193
234
|
*/
|
|
194
|
-
function
|
|
235
|
+
function syntheticClickTags(content) {
|
|
195
236
|
const pattern = /@click\s*=\s*\$\{/g;
|
|
237
|
+
const tags = [];
|
|
196
238
|
let match;
|
|
197
239
|
while ((match = pattern.exec(content)) !== null) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
const tag = syntheticClickTag(content, match.index);
|
|
241
|
+
if (tag !== undefined && !tags.includes(tag))
|
|
242
|
+
tags.push(tag);
|
|
201
243
|
}
|
|
202
|
-
return
|
|
244
|
+
return tags;
|
|
203
245
|
}
|
|
246
|
+
/** The tag a synthetic-element `@click` sits on, `?` when it cannot be told. */
|
|
247
|
+
const UNKNOWN_TAG = '?';
|
|
204
248
|
/**
|
|
205
249
|
* Given the offset of an `@click=${...}` occurrence, walks backwards to find
|
|
206
|
-
* the opening `<tag` that owns it and
|
|
207
|
-
* interactive element (
|
|
250
|
+
* the opening `<tag` that owns it and returns that tag when it is a
|
|
251
|
+
* synthetic interactive element (warning needed), or undefined when it is a
|
|
252
|
+
* native one (no warning needed).
|
|
208
253
|
*/
|
|
209
|
-
function
|
|
254
|
+
function syntheticClickTag(content, clickIndex) {
|
|
210
255
|
// Find the nearest preceding `<` that starts a tag (skip `</` closers).
|
|
211
256
|
let i = clickIndex - 1;
|
|
212
257
|
let tagOpenIndex = -1;
|
|
@@ -219,15 +264,15 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
|
219
264
|
// which shouldn't happen for a well-formed template but we defensively
|
|
220
265
|
// treat it as synthetic to preserve the prior-behaviour warning.
|
|
221
266
|
if (content[i] === '>') {
|
|
222
|
-
return
|
|
267
|
+
return UNKNOWN_TAG;
|
|
223
268
|
}
|
|
224
269
|
i--;
|
|
225
270
|
}
|
|
226
271
|
if (tagOpenIndex < 0)
|
|
227
|
-
return
|
|
272
|
+
return UNKNOWN_TAG;
|
|
228
273
|
const tagMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(content.slice(tagOpenIndex));
|
|
229
274
|
if (!tagMatch?.[1])
|
|
230
|
-
return
|
|
275
|
+
return UNKNOWN_TAG;
|
|
231
276
|
const tagName = tagMatch[1].toLowerCase();
|
|
232
277
|
if (tagName === 'a') {
|
|
233
278
|
// Bare <a> (no href) is non-interactive, so require a keyboard handler.
|
|
@@ -235,11 +280,11 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
|
235
280
|
// attribute text in between for an href attribute.
|
|
236
281
|
const tagEnd = content.indexOf('>', tagOpenIndex);
|
|
237
282
|
if (tagEnd < 0)
|
|
238
|
-
return
|
|
283
|
+
return 'a';
|
|
239
284
|
const attrs = content.slice(tagOpenIndex, tagEnd);
|
|
240
|
-
return
|
|
285
|
+
return /\shref\s*=/.test(attrs) ? undefined : 'a';
|
|
241
286
|
}
|
|
242
|
-
return
|
|
287
|
+
return NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName) ? undefined : tagName;
|
|
243
288
|
}
|
|
244
289
|
/** ASCII punctuation that reads as decoration rather than localizable text. */
|
|
245
290
|
const SYMBOL_ONLY_ASCII = '+-*=<>|/\\^~@#&!?%';
|
|
@@ -303,7 +348,7 @@ function isWithinLocalizedElement(content, matchIndex) {
|
|
|
303
348
|
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
304
349
|
*
|
|
305
350
|
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
306
|
-
* findings (matches the pre-existing escape hatch).
|
|
351
|
+
* findings in that file (matches the pre-existing escape hatch).
|
|
307
352
|
*/
|
|
308
353
|
export function containsHardcodedTemplateText(content) {
|
|
309
354
|
if (/furnace-ignore:\s*hardcoded-text/.test(content)) {
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Applies `furnace.json#validationExceptions[]` to a `furnace validate`
|
|
3
3
|
* result. An exception suppresses every finding of its check on its
|
|
4
|
-
* component
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* component, or with `tag`, a finding whose element tags it (and its
|
|
5
|
+
* sibling tag entries) cover, or with `file`, a finding whose module files
|
|
6
|
+
* it (and its sibling file entries) cover. One that suppresses nothing is stale: the
|
|
7
|
+
* finding was fixed (or renamed) and the review record outlived it, so it
|
|
8
|
+
* is reported as a warning naming the entry to delete, which keeps the
|
|
9
|
+
* list honest under `--max-warnings 0`.
|
|
10
|
+
*
|
|
11
|
+
* An exception never suppresses an error: an error blocks apply, and a
|
|
12
|
+
* review record cannot make a broken component deployable. The error stays
|
|
13
|
+
* counted and the matching entry is reported as
|
|
14
|
+
* `validation-exception-on-error`.
|
|
8
15
|
*/
|
|
9
16
|
import type { FurnaceValidationException, ValidationIssue } from '../types/furnace.js';
|
|
10
17
|
/** The check name of the warning a stale exception produces. */
|
|
11
18
|
export declare const STALE_VALIDATION_EXCEPTION_CHECK = "stale-validation-exception";
|
|
19
|
+
/** The check name of the error an exception matching an error produces. */
|
|
20
|
+
export declare const EXCEPTION_ON_ERROR_CHECK = "validation-exception-on-error";
|
|
12
21
|
/** A finding an exception suppressed, with the exception's reason. */
|
|
13
22
|
export interface ExceptedValidationIssue extends ValidationIssue {
|
|
14
23
|
reason: string;
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
2
2
|
/** The check name of the warning a stale exception produces. */
|
|
3
3
|
export const STALE_VALIDATION_EXCEPTION_CHECK = 'stale-validation-exception';
|
|
4
|
+
/** The check name of the error an exception matching an error produces. */
|
|
5
|
+
export const EXCEPTION_ON_ERROR_CHECK = 'validation-exception-on-error';
|
|
4
6
|
function matches(exception, issue) {
|
|
5
7
|
return exception.component === issue.component && exception.check === issue.check;
|
|
6
8
|
}
|
|
9
|
+
/** An exception's narrowing: the finding field it reads and its value. */
|
|
10
|
+
function scopeOf(entry) {
|
|
11
|
+
if (entry.tag !== undefined)
|
|
12
|
+
return { key: 'tags', value: entry.tag };
|
|
13
|
+
if (entry.file !== undefined)
|
|
14
|
+
return { key: 'files', value: entry.file };
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The exceptions that suppress a warning: an unscoped one alone, or scoped
|
|
19
|
+
* ones that together cover every tag (or every file) the finding names.
|
|
20
|
+
*/
|
|
21
|
+
function coveringExceptions(issue, candidates) {
|
|
22
|
+
const unscoped = candidates.find((entry) => scopeOf(entry) === undefined);
|
|
23
|
+
if (unscoped !== undefined)
|
|
24
|
+
return [unscoped];
|
|
25
|
+
for (const key of ['tags', 'files']) {
|
|
26
|
+
const named = issue[key] ?? [];
|
|
27
|
+
if (named.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
const covering = named.map((value) => candidates.find((entry) => {
|
|
30
|
+
const scope = scopeOf(entry);
|
|
31
|
+
return scope?.key === key && scope.value === value;
|
|
32
|
+
}));
|
|
33
|
+
if (covering.every((entry) => entry !== undefined))
|
|
34
|
+
return [...new Set(covering)];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
7
38
|
/**
|
|
8
39
|
* Splits findings into counted and excepted, and reports stale exceptions.
|
|
9
40
|
*
|
|
@@ -18,14 +49,38 @@ export function applyValidationExceptions(issues, exceptions, judged) {
|
|
|
18
49
|
const outcome = { issues: [], excepted: [], stale: [] };
|
|
19
50
|
const list = exceptions ?? [];
|
|
20
51
|
const used = new Set();
|
|
52
|
+
const onError = new Set();
|
|
21
53
|
for (const issue of issues) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
54
|
+
const candidates = list.filter((entry) => matches(entry, issue));
|
|
55
|
+
if (issue.severity === 'error') {
|
|
56
|
+
for (const entry of candidates) {
|
|
57
|
+
used.add(entry);
|
|
58
|
+
onError.add(entry);
|
|
59
|
+
}
|
|
24
60
|
outcome.issues.push(issue);
|
|
25
61
|
continue;
|
|
26
62
|
}
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
const covering = coveringExceptions(issue, candidates);
|
|
64
|
+
if (covering.length === 0) {
|
|
65
|
+
// A scoped exception that covers some of the tags (or files) is not
|
|
66
|
+
// stale: its element still carries the finding, beside one it does
|
|
67
|
+
// not cover.
|
|
68
|
+
for (const entry of candidates) {
|
|
69
|
+
const scope = scopeOf(entry);
|
|
70
|
+
if (scope !== undefined && issue[scope.key]?.includes(scope.value) === true) {
|
|
71
|
+
used.add(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
outcome.issues.push(issue);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of covering)
|
|
78
|
+
used.add(entry);
|
|
79
|
+
outcome.excepted.push({ ...issue, reason: covering.map((entry) => entry.reason).join('; ') });
|
|
80
|
+
}
|
|
81
|
+
for (const exception of onError) {
|
|
82
|
+
outcome.issues.push(createIssue(exception.component, 'error', EXCEPTION_ON_ERROR_CHECK, `furnace.json validationExceptions entry for [${exception.check}] matches an error, ` +
|
|
83
|
+
'which an exception cannot suppress. Fix the error, then delete the entry.'));
|
|
29
84
|
}
|
|
30
85
|
for (const exception of list) {
|
|
31
86
|
if (used.has(exception) || !judged(exception.component))
|
|
@@ -18,10 +18,39 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
|
|
|
18
18
|
const NAMING_ATTRIBUTES = ['data-l10n-id', 'aria-label', '.arialabel'];
|
|
19
19
|
/** Attributes that give the button a visible label, so it is not icon-only. */
|
|
20
20
|
const LABEL_ATTRIBUTES = ['label', '.label'];
|
|
21
|
+
/**
|
|
22
|
+
* Whether the button's content has a non-blank character outside tags.
|
|
23
|
+
* Scans instead of stripping tags, so nested or unbalanced markup cannot
|
|
24
|
+
* leave a fragment behind that reads as text, and a `>` inside a quoted
|
|
25
|
+
* attribute value does not end its tag.
|
|
26
|
+
*/
|
|
27
|
+
function hasVisibleText(markup) {
|
|
28
|
+
let inTag = false;
|
|
29
|
+
let quote;
|
|
30
|
+
for (const char of markup) {
|
|
31
|
+
if (quote !== undefined) {
|
|
32
|
+
if (char === quote)
|
|
33
|
+
quote = undefined;
|
|
34
|
+
}
|
|
35
|
+
else if (inTag) {
|
|
36
|
+
if (char === '"' || char === "'")
|
|
37
|
+
quote = char;
|
|
38
|
+
else if (char === '>')
|
|
39
|
+
inTag = false;
|
|
40
|
+
}
|
|
41
|
+
else if (char === '<') {
|
|
42
|
+
inTag = true;
|
|
43
|
+
}
|
|
44
|
+
else if (char.trim() !== '') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
21
50
|
function isIconOnly(attrs, innerText) {
|
|
22
51
|
if (LABEL_ATTRIBUTES.some((name) => attrs.has(name)))
|
|
23
52
|
return false;
|
|
24
|
-
if (innerText
|
|
53
|
+
if (hasVisibleText(innerText))
|
|
25
54
|
return false;
|
|
26
55
|
const type = attrs.get('type') ?? '';
|
|
27
56
|
return attrs.has('iconsrc') || attrs.has('.iconsrc') || /(^|\s)icon(\s|$)/.test(type);
|
|
@@ -17,7 +17,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
17
17
|
import { appendFile, link, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
20
|
-
import { ensureDir } from '../utils/fs.js';
|
|
20
|
+
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
21
21
|
import { info, verbose } from '../utils/logger.js';
|
|
22
22
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
23
23
|
import { isValidHistoryPath } from './config-validate-history.js';
|
|
@@ -131,6 +131,13 @@ export async function migrateLegacyHistoryLog(patchesDir, target) {
|
|
|
131
131
|
await link(claimed, target);
|
|
132
132
|
}
|
|
133
133
|
catch (error) {
|
|
134
|
+
// Windows can report two racing renames of one file as both
|
|
135
|
+
// succeeding, the file ending under the other claimant's name. That
|
|
136
|
+
// claimant holds the records and moves them, so this one stands down.
|
|
137
|
+
if (getNodeErrorCode(error) === 'ENOENT' && !(await pathExists(claimed))) {
|
|
138
|
+
verbose('History log migration lost the claim to a concurrent command');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
134
141
|
// EEXIST: both logs exist. Anything else (a filesystem without hard
|
|
135
142
|
// links, a target on another device): the append below creates the
|
|
136
143
|
// target when it is missing, so it covers that case too.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefixes every line of a chunked stream, for output from several
|
|
3
|
+
* concurrent harness instances (`fireforge test --jobs`) that share one
|
|
4
|
+
* terminal and one run log. Without a prefix, interleaved lines from two
|
|
5
|
+
* browsers cannot be attributed after the fact.
|
|
6
|
+
*/
|
|
7
|
+
/** A stateful line prefixer for one stream. */
|
|
8
|
+
export interface LinePrefixer {
|
|
9
|
+
/** Returns `chunk` with the prefix inserted at the start of each line. */
|
|
10
|
+
transform(chunk: string): string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a prefixer. Line state carries across chunks, so a line split
|
|
14
|
+
* between two chunks gets the prefix exactly once.
|
|
15
|
+
*
|
|
16
|
+
* @param prefix - Text to insert, e.g. `[job 2] `
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLinePrefixer(prefix: string): LinePrefixer;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Prefixes every line of a chunked stream, for output from several
|
|
4
|
+
* concurrent harness instances (`fireforge test --jobs`) that share one
|
|
5
|
+
* terminal and one run log. Without a prefix, interleaved lines from two
|
|
6
|
+
* browsers cannot be attributed after the fact.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Creates a prefixer. Line state carries across chunks, so a line split
|
|
10
|
+
* between two chunks gets the prefix exactly once.
|
|
11
|
+
*
|
|
12
|
+
* @param prefix - Text to insert, e.g. `[job 2] `
|
|
13
|
+
*/
|
|
14
|
+
export function createLinePrefixer(prefix) {
|
|
15
|
+
let atLineStart = true;
|
|
16
|
+
return {
|
|
17
|
+
transform(chunk) {
|
|
18
|
+
if (chunk.length === 0)
|
|
19
|
+
return chunk;
|
|
20
|
+
let out = '';
|
|
21
|
+
let start = 0;
|
|
22
|
+
for (let index = chunk.indexOf('\n'); index !== -1; index = chunk.indexOf('\n', start)) {
|
|
23
|
+
out += `${atLineStart ? prefix : ''}${chunk.slice(start, index + 1)}`;
|
|
24
|
+
atLineStart = true;
|
|
25
|
+
start = index + 1;
|
|
26
|
+
}
|
|
27
|
+
if (start < chunk.length) {
|
|
28
|
+
out += `${atLineStart ? prefix : ''}${chunk.slice(start)}`;
|
|
29
|
+
atLineStart = false;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface MachOptions {
|
|
|
47
47
|
onProcessGroup?: (pgid: number) => void;
|
|
48
48
|
/** Exec-layer hook, see {@link ProcessGroupOptions.postCloseSweep}. Capture-only. */
|
|
49
49
|
postCloseSweep?: () => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Prefix for every echoed and logged line (concurrent `test --jobs`
|
|
52
|
+
* instances). The capture and `onOutputChunk` stay raw. Capture-only.
|
|
53
|
+
*/
|
|
54
|
+
outputPrefix?: string | undefined;
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* Result of running a mach command while capturing streamed output.
|
|
@@ -263,6 +268,8 @@ export interface MachTestSuiteOptions {
|
|
|
263
268
|
* the harness launched. See {@link MachOptions}.
|
|
264
269
|
*/
|
|
265
270
|
teardown?: Pick<MachOptions, 'onOutputChunk' | 'onProcessGroup' | 'postCloseSweep'> | undefined;
|
|
271
|
+
/** Line prefix for echo and run log, see {@link MachOptions.outputPrefix}. */
|
|
272
|
+
outputPrefix?: string | undefined;
|
|
266
273
|
}
|
|
267
274
|
/**
|
|
268
275
|
* Runs one mach test command (`mach test` / `mach xpcshell-test` / `mach
|
package/dist/src/core/mach.js
CHANGED
|
@@ -7,6 +7,7 @@ import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from
|
|
|
7
7
|
import { withBuildRunRecord } from './build-run-record.js';
|
|
8
8
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
9
9
|
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
10
|
+
import { createLinePrefixer } from './line-prefixer.js';
|
|
10
11
|
import { explainMachError } from './mach-error-hints.js';
|
|
11
12
|
import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, hasKnownTeardownNoise, KNOWN_TEARDOWN_NOISE_BUILD_NOTE, } from './mach-known-noise-filter.js';
|
|
12
13
|
import { getPython } from './mach-python.js';
|
|
@@ -89,6 +90,15 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
89
90
|
const noiseContext = options.annotateKnownTeardownNoise === true ? createTeardownNoiseContext() : undefined;
|
|
90
91
|
const stdoutFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
91
92
|
const stderrFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
93
|
+
const prefix = options.outputPrefix;
|
|
94
|
+
const prefixers = prefix === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: {
|
|
97
|
+
stdoutLog: createLinePrefixer(prefix),
|
|
98
|
+
stdoutEcho: createLinePrefixer(prefix),
|
|
99
|
+
stderrLog: createLinePrefixer(prefix),
|
|
100
|
+
stderrEcho: createLinePrefixer(prefix),
|
|
101
|
+
};
|
|
92
102
|
const exitCode = await execStream(python, [machPath, ...args], {
|
|
93
103
|
cwd: engineDir,
|
|
94
104
|
...(options.env ? { env: options.env } : {}),
|
|
@@ -110,8 +120,9 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
110
120
|
// echo filter only exists to shorten what a human scrolls past.
|
|
111
121
|
// Collapsing a traceback in the artifact would reintroduce the loss
|
|
112
122
|
// this log exists to prevent.
|
|
113
|
-
writeToActiveRunLog(data);
|
|
114
|
-
|
|
123
|
+
writeToActiveRunLog(prefixers ? prefixers.stdoutLog.transform(data) : data);
|
|
124
|
+
const echo = stdoutFilter ? stdoutFilter.transform(data) : data;
|
|
125
|
+
process.stdout.write(prefixers ? prefixers.stdoutEcho.transform(echo) : echo);
|
|
115
126
|
},
|
|
116
127
|
onStderr: (data) => {
|
|
117
128
|
options.onOutputChunk?.('stderr', data);
|
|
@@ -119,19 +130,22 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
119
130
|
if (stderr.length > CAPTURE_TAIL_LIMIT) {
|
|
120
131
|
stderr = stderr.slice(-CAPTURE_TAIL_LIMIT);
|
|
121
132
|
}
|
|
122
|
-
writeToActiveRunLog(data);
|
|
123
|
-
|
|
133
|
+
writeToActiveRunLog(prefixers ? prefixers.stderrLog.transform(data) : data);
|
|
134
|
+
const echo = stderrFilter ? stderrFilter.transform(data) : data;
|
|
135
|
+
process.stderr.write(prefixers ? prefixers.stderrEcho.transform(echo) : echo);
|
|
124
136
|
},
|
|
125
137
|
});
|
|
126
138
|
if (stdoutFilter) {
|
|
127
139
|
const residue = stdoutFilter.flush();
|
|
128
|
-
if (residue.length > 0)
|
|
129
|
-
process.stdout.write(residue);
|
|
140
|
+
if (residue.length > 0) {
|
|
141
|
+
process.stdout.write(prefixers ? prefixers.stdoutEcho.transform(residue) : residue);
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
if (stderrFilter) {
|
|
132
145
|
const residue = stderrFilter.flush();
|
|
133
|
-
if (residue.length > 0)
|
|
134
|
-
process.stderr.write(residue);
|
|
146
|
+
if (residue.length > 0) {
|
|
147
|
+
process.stderr.write(prefixers ? prefixers.stderrEcho.transform(residue) : residue);
|
|
148
|
+
}
|
|
135
149
|
}
|
|
136
150
|
return { stdout, stderr, exitCode };
|
|
137
151
|
}
|
|
@@ -418,13 +432,14 @@ function testVerbosityEnvUnset(fullOutput) {
|
|
|
418
432
|
* @param options - Engine checkout, test paths, extra args, env, verbosity
|
|
419
433
|
*/
|
|
420
434
|
export async function runMachTestSuite(kind, options) {
|
|
421
|
-
const { engineDir, testPaths = [], args = [], env, fullOutput, teardown } = options;
|
|
435
|
+
const { engineDir, testPaths = [], args = [], env, fullOutput, teardown, outputPrefix } = options;
|
|
422
436
|
const guard = await installMachResourceGuard(engineDir);
|
|
423
437
|
const envUnset = testVerbosityEnvUnset(fullOutput);
|
|
424
438
|
return runMachCapture([kind, ...testPaths, ...args], engineDir, {
|
|
425
439
|
env: { ...guard.env, ...env },
|
|
426
440
|
...(envUnset ? { envUnset } : {}),
|
|
427
441
|
...teardown,
|
|
442
|
+
...(outputPrefix === undefined ? {} : { outputPrefix }),
|
|
428
443
|
annotateKnownTeardownNoise: true,
|
|
429
444
|
});
|
|
430
445
|
}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* no owned file is an upstream Firefox module, which FireForge does not
|
|
19
19
|
* police and cannot cheaply enumerate. Policing only what the fork owns is
|
|
20
20
|
* both the actionable set and the one that produces real incidents.
|
|
21
|
+
*
|
|
22
|
+
* An unscoped `verify` widens that set with the engine worktree: changed
|
|
23
|
+
* files import, and untracked files are targets. A chrome script's lazy
|
|
24
|
+
* getter for a new, not yet exported `.sys.mjs` with no moz.build line
|
|
25
|
+
* SIGSEGVed every start of a shared objdir, and neither file was in the
|
|
26
|
+
* queue yet, so the queue-only scan could not see it.
|
|
21
27
|
*/
|
|
22
28
|
/** Why a queue-owned module import does not resolve to a packaged module. */
|
|
23
29
|
export type UnresolvedModuleReason = 'missing-file' | 'unregistered';
|
|
@@ -31,16 +37,26 @@ export interface UnresolvedModuleImport {
|
|
|
31
37
|
importers: string[];
|
|
32
38
|
reason: UnresolvedModuleReason;
|
|
33
39
|
}
|
|
40
|
+
/** Engine worktree files the preflight also judges, beside the queue's. */
|
|
41
|
+
export interface WorktreeModuleFiles {
|
|
42
|
+
/** Changed or untracked files: scanned for imports. */
|
|
43
|
+
changed: readonly string[];
|
|
44
|
+
/** Untracked files: new modules the fork has not exported yet. */
|
|
45
|
+
untracked: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** Lists the engine's changed and untracked files for the worktree scan. */
|
|
48
|
+
export declare function collectWorktreeModuleFiles(engineDir: string): Promise<WorktreeModuleFiles>;
|
|
34
49
|
/**
|
|
35
50
|
* Resolves every `resource://` system-module import made by the
|
|
36
|
-
* queue-owned files and
|
|
37
|
-
* load.
|
|
51
|
+
* queue-owned files (and, given `worktree`, by changed engine files) and
|
|
52
|
+
* reports the ones a packaged browser could not load.
|
|
38
53
|
*
|
|
39
54
|
* @param engineDir - Path to the engine checkout
|
|
40
55
|
* @param ownedFiles - Engine-relative paths the patch queue owns
|
|
56
|
+
* @param worktree - Changed and untracked engine files to judge as well
|
|
41
57
|
* @returns Findings sorted by module path. Empty when everything resolves
|
|
42
58
|
*/
|
|
43
|
-
export declare function findUnresolvedSystemModuleImports(engineDir: string, ownedFiles: readonly string[]): Promise<UnresolvedModuleImport[]>;
|
|
59
|
+
export declare function findUnresolvedSystemModuleImports(engineDir: string, ownedFiles: readonly string[], worktree?: WorktreeModuleFiles): Promise<UnresolvedModuleImport[]>;
|
|
44
60
|
/**
|
|
45
61
|
* Renders findings as operator-facing lines, naming the unregistered or
|
|
46
62
|
* unresolvable module, so the failure is a sentence instead of a bare
|
|
@@ -19,12 +19,19 @@
|
|
|
19
19
|
* no owned file is an upstream Firefox module, which FireForge does not
|
|
20
20
|
* police and cannot cheaply enumerate. Policing only what the fork owns is
|
|
21
21
|
* both the actionable set and the one that produces real incidents.
|
|
22
|
+
*
|
|
23
|
+
* An unscoped `verify` widens that set with the engine worktree: changed
|
|
24
|
+
* files import, and untracked files are targets. A chrome script's lazy
|
|
25
|
+
* getter for a new, not yet exported `.sys.mjs` with no moz.build line
|
|
26
|
+
* SIGSEGVed every start of a shared objdir, and neither file was in the
|
|
27
|
+
* queue yet, so the queue-only scan could not see it.
|
|
22
28
|
*/
|
|
23
29
|
import { dirname, join } from 'node:path';
|
|
24
30
|
import { toError } from '../utils/errors.js';
|
|
25
31
|
import { pathExists, readText } from '../utils/fs.js';
|
|
26
32
|
import { verbose } from '../utils/logger.js';
|
|
27
33
|
import { escapeRegex } from '../utils/regex.js';
|
|
34
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from './git-status.js';
|
|
28
35
|
import { extractResourceModuleSpecifiers, moduleSourceSuffix, } from './patch-lint-module-registration.js';
|
|
29
36
|
/** Extensions whose contents are scanned for resource-module imports. */
|
|
30
37
|
const IMPORTABLE_EXTENSIONS = ['.mjs', '.sys.mjs', '.js', '.jsm'];
|
|
@@ -79,20 +86,31 @@ async function isRegisteredInAncestorMozBuild(engineDir, moduleRelPath, mozBuild
|
|
|
79
86
|
dir = parent;
|
|
80
87
|
}
|
|
81
88
|
}
|
|
89
|
+
/** Lists the engine's changed and untracked files for the worktree scan. */
|
|
90
|
+
export async function collectWorktreeModuleFiles(engineDir) {
|
|
91
|
+
const entries = await expandUntrackedDirectoryEntries(engineDir, await getWorkingTreeStatus(engineDir));
|
|
92
|
+
const present = entries.filter((entry) => entry.worktreeStatus !== 'D');
|
|
93
|
+
return {
|
|
94
|
+
changed: present.map((entry) => entry.file),
|
|
95
|
+
untracked: present.filter((entry) => entry.status === '??').map((entry) => entry.file),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
82
98
|
/**
|
|
83
99
|
* Resolves every `resource://` system-module import made by the
|
|
84
|
-
* queue-owned files and
|
|
85
|
-
* load.
|
|
100
|
+
* queue-owned files (and, given `worktree`, by changed engine files) and
|
|
101
|
+
* reports the ones a packaged browser could not load.
|
|
86
102
|
*
|
|
87
103
|
* @param engineDir - Path to the engine checkout
|
|
88
104
|
* @param ownedFiles - Engine-relative paths the patch queue owns
|
|
105
|
+
* @param worktree - Changed and untracked engine files to judge as well
|
|
89
106
|
* @returns Findings sorted by module path. Empty when everything resolves
|
|
90
107
|
*/
|
|
91
|
-
export async function findUnresolvedSystemModuleImports(engineDir, ownedFiles) {
|
|
92
|
-
const owned = [...new Set(ownedFiles)];
|
|
93
|
-
|
|
108
|
+
export async function findUnresolvedSystemModuleImports(engineDir, ownedFiles, worktree) {
|
|
109
|
+
const owned = [...new Set([...ownedFiles, ...(worktree?.untracked ?? [])])];
|
|
110
|
+
const importing = [...new Set([...owned, ...(worktree?.changed ?? [])])];
|
|
111
|
+
/** specifier → importing files. */
|
|
94
112
|
const importersBySpecifier = new Map();
|
|
95
|
-
for (const file of
|
|
113
|
+
for (const file of importing) {
|
|
96
114
|
if (!IMPORTABLE_EXTENSIONS.some((extension) => file.endsWith(extension)))
|
|
97
115
|
continue;
|
|
98
116
|
const content = await readIfPresent(join(engineDir, file));
|