@hominis/fireforge 0.37.0 → 0.38.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 +13 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +5 -5
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +5 -1
- package/dist/src/commands/export-flow.js +10 -7
- package/dist/src/commands/export-placement-gate.js +3 -5
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +49 -4
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +17 -6
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +15 -50
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +34 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +47 -7
- 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 +6 -1
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +76 -1
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +25 -7
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-stale-check.d.ts +41 -1
- package/dist/src/core/test-stale-check.js +77 -3
- package/dist/src/types/commands/options.d.ts +57 -3
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -13,16 +13,18 @@ export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
|
|
|
13
13
|
* Gets the download URL for a Firefox source tarball.
|
|
14
14
|
* @param version - Firefox version (e.g., "140.9.0esr")
|
|
15
15
|
* @param product - Firefox product type
|
|
16
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
16
17
|
* @returns Full URL to the source tarball
|
|
17
18
|
*/
|
|
18
|
-
export declare function getDownloadUrl(version: string, product?: FirefoxProduct): string;
|
|
19
|
+
export declare function getDownloadUrl(version: string, product?: FirefoxProduct, candidate?: string): string;
|
|
19
20
|
/**
|
|
20
21
|
* Gets the filename for a Firefox source tarball.
|
|
21
22
|
* @param version - Firefox version
|
|
22
23
|
* @param product - Firefox product type
|
|
24
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
23
25
|
* @returns Tarball filename
|
|
24
26
|
*/
|
|
25
|
-
export declare function getTarballFilename(version: string, product?: FirefoxProduct): string;
|
|
27
|
+
export declare function getTarballFilename(version: string, product?: FirefoxProduct, candidate?: string): string;
|
|
26
28
|
/**
|
|
27
29
|
* Lifecycle phase reported by {@link downloadFirefoxSource}. The download
|
|
28
30
|
* CLI command uses this to swap spinners between the bytes-on-the-wire
|
|
@@ -57,5 +59,8 @@ export declare function sweepOrphanedEngineWorkDirs(destDir: string): Promise<st
|
|
|
57
59
|
* @param onPhase - Optional callback fired when the function transitions
|
|
58
60
|
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
59
61
|
* phase even if the cached archive path skips the wire entirely.
|
|
62
|
+
* @param candidate - Optional release-candidate build directory (e.g.
|
|
63
|
+
* "build2") resolving the archive under `candidates/` instead of
|
|
64
|
+
* `releases/`.
|
|
60
65
|
*/
|
|
61
|
-
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback, expectedSha256?: string, onPhaseProgress?: FirefoxSourceProgressCallback): Promise<void>;
|
|
66
|
+
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback, expectedSha256?: string, onPhaseProgress?: FirefoxSourceProgressCallback, candidate?: string): Promise<void>;
|
package/dist/src/core/firefox.js
CHANGED
|
@@ -19,19 +19,21 @@ export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
|
|
|
19
19
|
* Gets the download URL for a Firefox source tarball.
|
|
20
20
|
* @param version - Firefox version (e.g., "140.9.0esr")
|
|
21
21
|
* @param product - Firefox product type
|
|
22
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
22
23
|
* @returns Full URL to the source tarball
|
|
23
24
|
*/
|
|
24
|
-
export function getDownloadUrl(version, product = 'firefox') {
|
|
25
|
-
return resolveArchive(version, product).url;
|
|
25
|
+
export function getDownloadUrl(version, product = 'firefox', candidate) {
|
|
26
|
+
return resolveArchive(version, product, candidate).url;
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Gets the filename for a Firefox source tarball.
|
|
29
30
|
* @param version - Firefox version
|
|
30
31
|
* @param product - Firefox product type
|
|
32
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
31
33
|
* @returns Tarball filename
|
|
32
34
|
*/
|
|
33
|
-
export function getTarballFilename(version, product = 'firefox') {
|
|
34
|
-
return resolveArchive(version, product).filename;
|
|
35
|
+
export function getTarballFilename(version, product = 'firefox', candidate) {
|
|
36
|
+
return resolveArchive(version, product, candidate).filename;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Classifies an extraction failure as archive corruption (invalidate the
|
|
@@ -100,9 +102,12 @@ export async function sweepOrphanedEngineWorkDirs(destDir) {
|
|
|
100
102
|
* @param onPhase - Optional callback fired when the function transitions
|
|
101
103
|
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
102
104
|
* phase even if the cached archive path skips the wire entirely.
|
|
105
|
+
* @param candidate - Optional release-candidate build directory (e.g.
|
|
106
|
+
* "build2") resolving the archive under `candidates/` instead of
|
|
107
|
+
* `releases/`.
|
|
103
108
|
*/
|
|
104
|
-
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase, expectedSha256, onPhaseProgress) {
|
|
105
|
-
const archive = resolveArchive(version, product);
|
|
109
|
+
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase, expectedSha256, onPhaseProgress, candidate) {
|
|
110
|
+
const archive = resolveArchive(version, product, candidate);
|
|
106
111
|
const tarballPath = join(cacheDir, archive.filename);
|
|
107
112
|
// Ensure cache directory exists
|
|
108
113
|
await ensureDir(cacheDir);
|
|
@@ -27,10 +27,33 @@ export function hasPositiveTabindex(content) {
|
|
|
27
27
|
return value !== undefined && parseInt(value, 10) > 0;
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Collects `[start, end)` spans of `<label>…</label>` elements whose content
|
|
32
|
+
* includes actual label text. Labels cannot nest per HTML, so the non-greedy
|
|
33
|
+
* inner match is sound. A span qualifies only when the inner content minus
|
|
34
|
+
* tags still contains non-whitespace — an empty or tag-only wrapper provides
|
|
35
|
+
* no accessible name. A `${…}` Lit binding counts as label text: dynamic
|
|
36
|
+
* (usually localized) text is a legitimate accessible name.
|
|
37
|
+
*/
|
|
38
|
+
function collectLabelledSpans(content) {
|
|
39
|
+
const spans = [];
|
|
40
|
+
const labelPattern = /<label\b[^>]*>([\s\S]*?)<\/label>/gi;
|
|
41
|
+
let labelMatch;
|
|
42
|
+
while ((labelMatch = labelPattern.exec(content)) !== null) {
|
|
43
|
+
const inner = labelMatch[1] ?? '';
|
|
44
|
+
if (/\S/.test(inner.replace(/<[^>]*>/g, ' '))) {
|
|
45
|
+
spans.push({ start: labelMatch.index, end: labelMatch.index + labelMatch[0].length });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return spans;
|
|
49
|
+
}
|
|
30
50
|
/** Detects form inputs without associated labels. */
|
|
31
51
|
export function hasUnlabelledFormInput(content) {
|
|
32
52
|
// Look for <input> or <select> or <textarea> without aria-label, aria-labelledby, or id
|
|
33
|
-
// (id implies an external <label for="..."> could exist)
|
|
53
|
+
// (id implies an external <label for="..."> could exist). A control wrapped
|
|
54
|
+
// in a <label> that carries actual text is implicitly associated and is
|
|
55
|
+
// exempt as well.
|
|
56
|
+
const labelledSpans = collectLabelledSpans(content);
|
|
34
57
|
const inputPattern = /<(input|select|textarea)\b([^>]*)>/gi;
|
|
35
58
|
let inputMatch;
|
|
36
59
|
while ((inputMatch = inputPattern.exec(content)) !== null) {
|
|
@@ -41,6 +64,10 @@ export function hasUnlabelledFormInput(content) {
|
|
|
41
64
|
/type\s*=\s*["']hidden["']/i.test(attrs)) {
|
|
42
65
|
continue;
|
|
43
66
|
}
|
|
67
|
+
const index = inputMatch.index;
|
|
68
|
+
if (labelledSpans.some((span) => index >= span.start && index < span.end)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
44
71
|
return true;
|
|
45
72
|
}
|
|
46
73
|
return false;
|
|
@@ -34,6 +34,11 @@ export declare function getLicenseHeader(license: ProjectLicense, style: Comment
|
|
|
34
34
|
* on lines 1–2 with the MPL header on lines 3+, which the raw
|
|
35
35
|
* `startsWith` check would otherwise miss.
|
|
36
36
|
*
|
|
37
|
+
* The MPL-2.0 header is additionally matched on normalized whitespace
|
|
38
|
+
* (see {@link normalizeCommentHead}) so upstream files using the older
|
|
39
|
+
* Mozilla wrap — breaking after "file," instead of "with this" — are
|
|
40
|
+
* accepted too; only the wrap position differs, never the wording.
|
|
41
|
+
*
|
|
37
42
|
* @param content - File content to check
|
|
38
43
|
* @param style - Comment syntax of the file
|
|
39
44
|
*/
|
|
@@ -51,6 +56,13 @@ export declare function hasAnyLicenseHeader(content: string, style: CommentStyle
|
|
|
51
56
|
* patch-lint, making it dead code for every other project license and
|
|
52
57
|
* forcing repo-side audit workarounds.
|
|
53
58
|
*
|
|
59
|
+
* Matching is wrap-agnostic: after the exact `startsWith` fast path, the
|
|
60
|
+
* leading block comment is compared on normalized whitespace (see
|
|
61
|
+
* {@link normalizeCommentHead}) so the older upstream wrap — breaking
|
|
62
|
+
* after "file," instead of "with this", as `ext-browser.js` ships —
|
|
63
|
+
* passes too. Only the line-break position may differ; altered wording
|
|
64
|
+
* still rejects.
|
|
65
|
+
*
|
|
54
66
|
* @param content - File content to check
|
|
55
67
|
*/
|
|
56
68
|
export declare function hasUpstreamMplBlockHeader(content: string): boolean;
|
|
@@ -86,6 +86,59 @@ function stripLeadingEditorDirectives(content) {
|
|
|
86
86
|
} while (result !== prev);
|
|
87
87
|
return result;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Collapses all whitespace runs to single spaces and trims, so header
|
|
91
|
+
* comparisons ignore the exact line-break/wrap positions.
|
|
92
|
+
*/
|
|
93
|
+
function normalizeText(text) {
|
|
94
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The canonical upstream MPL-2.0 notice as a single normalized sentence
|
|
98
|
+
* (including the `http://mozilla.org/MPL/2.0/.` URL). Wrap-agnostic
|
|
99
|
+
* header checks match against this instead of an exact rendering.
|
|
100
|
+
*/
|
|
101
|
+
const MPL_HEADER_NORMALIZED = normalizeText(HEADER_LINES['MPL-2.0'].join(' '));
|
|
102
|
+
/**
|
|
103
|
+
* Extracts ONLY the leading comment span of `content` for the given
|
|
104
|
+
* comment style and returns it whitespace-normalized:
|
|
105
|
+
*
|
|
106
|
+
* - `js`: a leading run of `//` lines (markers stripped), or — when the
|
|
107
|
+
* file opens with a block comment instead — the first
|
|
108
|
+
* `/* … *\/` block;
|
|
109
|
+
* - `css`: the first `/* … *\/` block, with the ` * ` continuation-line
|
|
110
|
+
* prefixes stripped (same collapsing
|
|
111
|
+
* {@link normalizeLicenseHeadForScan} does);
|
|
112
|
+
* - `hash`: a leading run of `#` lines (markers stripped).
|
|
113
|
+
*
|
|
114
|
+
* A non-comment first line yields `''`. This lets callers accept a known
|
|
115
|
+
* header text regardless of where upstream wrapped its lines (e.g. the
|
|
116
|
+
* older Mozilla wrap that breaks after "file,"), without ever matching
|
|
117
|
+
* text beyond the leading comment.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeCommentHead(content, style) {
|
|
120
|
+
const src = content.replace(/\r\n?/g, '\n');
|
|
121
|
+
const collectLineRun = (marker) => {
|
|
122
|
+
const parts = [];
|
|
123
|
+
for (const line of src.split('\n')) {
|
|
124
|
+
const m = marker.exec(line);
|
|
125
|
+
if (!m)
|
|
126
|
+
break;
|
|
127
|
+
parts.push(m[1] ?? '');
|
|
128
|
+
}
|
|
129
|
+
return normalizeText(parts.join(' '));
|
|
130
|
+
};
|
|
131
|
+
if (style === 'hash') {
|
|
132
|
+
return collectLineRun(/^[ \t]*#[ \t]?(.*)$/);
|
|
133
|
+
}
|
|
134
|
+
if (style === 'js' && /^[ \t]*\/\//.test(src)) {
|
|
135
|
+
return collectLineRun(/^[ \t]*\/\/[ \t]?(.*)$/);
|
|
136
|
+
}
|
|
137
|
+
const block = /^[ \t]*\/\*([\s\S]*?)\*\//.exec(src);
|
|
138
|
+
if (!block)
|
|
139
|
+
return '';
|
|
140
|
+
return normalizeText((block[1] ?? '').replace(/\n[ \t]*\*[ \t]*/g, ' '));
|
|
141
|
+
}
|
|
89
142
|
/**
|
|
90
143
|
* Returns true if `content` starts with any known license header for the
|
|
91
144
|
* given comment style.
|
|
@@ -103,6 +156,11 @@ function stripLeadingEditorDirectives(content) {
|
|
|
103
156
|
* on lines 1–2 with the MPL header on lines 3+, which the raw
|
|
104
157
|
* `startsWith` check would otherwise miss.
|
|
105
158
|
*
|
|
159
|
+
* The MPL-2.0 header is additionally matched on normalized whitespace
|
|
160
|
+
* (see {@link normalizeCommentHead}) so upstream files using the older
|
|
161
|
+
* Mozilla wrap — breaking after "file," instead of "with this" — are
|
|
162
|
+
* accepted too; only the wrap position differs, never the wording.
|
|
163
|
+
*
|
|
106
164
|
* @param content - File content to check
|
|
107
165
|
* @param style - Comment syntax of the file
|
|
108
166
|
*/
|
|
@@ -116,6 +174,9 @@ export function hasAnyLicenseHeader(content, style) {
|
|
|
116
174
|
if (style === 'js' && candidate.startsWith(getLicenseHeader('MPL-2.0', 'css'))) {
|
|
117
175
|
return true;
|
|
118
176
|
}
|
|
177
|
+
if (normalizeCommentHead(candidate, style).startsWith(MPL_HEADER_NORMALIZED)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
119
180
|
}
|
|
120
181
|
return false;
|
|
121
182
|
}
|
|
@@ -132,11 +193,25 @@ export function hasAnyLicenseHeader(content, style) {
|
|
|
132
193
|
* patch-lint, making it dead code for every other project license and
|
|
133
194
|
* forcing repo-side audit workarounds.
|
|
134
195
|
*
|
|
196
|
+
* Matching is wrap-agnostic: after the exact `startsWith` fast path, the
|
|
197
|
+
* leading block comment is compared on normalized whitespace (see
|
|
198
|
+
* {@link normalizeCommentHead}) so the older upstream wrap — breaking
|
|
199
|
+
* after "file," instead of "with this", as `ext-browser.js` ships —
|
|
200
|
+
* passes too. Only the line-break position may differ; altered wording
|
|
201
|
+
* still rejects.
|
|
202
|
+
*
|
|
135
203
|
* @param content - File content to check
|
|
136
204
|
*/
|
|
137
205
|
export function hasUpstreamMplBlockHeader(content) {
|
|
138
206
|
const blockHeader = getLicenseHeader('MPL-2.0', 'css');
|
|
139
|
-
|
|
207
|
+
const candidates = [content, stripLeadingEditorDirectives(content)];
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
if (candidate.startsWith(blockHeader))
|
|
210
|
+
return true;
|
|
211
|
+
if (normalizeCommentHead(candidate, 'css').startsWith(MPL_HEADER_NORMALIZED))
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
140
215
|
}
|
|
141
216
|
/**
|
|
142
217
|
* Returns true if `content` starts with any known license header in any
|
|
@@ -8,6 +8,9 @@ export { lintPatchedCss };
|
|
|
8
8
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
9
9
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
10
10
|
*
|
|
11
|
+
* Line counting agrees with `wc -l`: a trailing newline does not add a
|
|
12
|
+
* phantom empty line to either `total` or the binary-hunk accounting.
|
|
13
|
+
*
|
|
11
14
|
* @param diffContent - Raw unified diff string
|
|
12
15
|
* @returns Object with `total` line count and `textLines` (total minus binary hunk lines)
|
|
13
16
|
*/
|
|
@@ -35,15 +35,33 @@ const FILE_SIZE_THRESHOLDS = {
|
|
|
35
35
|
general: { notice: 500, warning: 750, error: 900 },
|
|
36
36
|
test: { notice: 1200, warning: 1400, error: 1600 },
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Counts the lines of `content` the way `wc -l` reports them: a trailing
|
|
40
|
+
* newline terminates the last line rather than starting an empty extra
|
|
41
|
+
* one. A naive `split('\n').length` over-counts by one for any content
|
|
42
|
+
* ending in `\n` (the common case), which made the size rules fire one
|
|
43
|
+
* line early against the operator's own `wc -l` measurement.
|
|
44
|
+
*/
|
|
45
|
+
function countContentLines(content) {
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
if (lines[lines.length - 1] === '')
|
|
48
|
+
lines.pop();
|
|
49
|
+
return lines.length;
|
|
50
|
+
}
|
|
38
51
|
/**
|
|
39
52
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
40
53
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
41
54
|
*
|
|
55
|
+
* Line counting agrees with `wc -l`: a trailing newline does not add a
|
|
56
|
+
* phantom empty line to either `total` or the binary-hunk accounting.
|
|
57
|
+
*
|
|
42
58
|
* @param diffContent - Raw unified diff string
|
|
43
59
|
* @returns Object with `total` line count and `textLines` (total minus binary hunk lines)
|
|
44
60
|
*/
|
|
45
61
|
export function countNonBinaryDiffLines(diffContent) {
|
|
46
62
|
const lines = diffContent.split('\n');
|
|
63
|
+
if (lines[lines.length - 1] === '')
|
|
64
|
+
lines.pop();
|
|
47
65
|
const total = lines.length;
|
|
48
66
|
let binaryLines = 0;
|
|
49
67
|
let inBinaryHunk = false;
|
|
@@ -329,12 +347,12 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
329
347
|
}
|
|
330
348
|
// 2. File size check (new files only)
|
|
331
349
|
if (isNew) {
|
|
332
|
-
const lineCount = content
|
|
350
|
+
const lineCount = countContentLines(content);
|
|
333
351
|
const isTest = isTestFile(file);
|
|
334
352
|
const thresholds = isTest ? FILE_SIZE_THRESHOLDS.test : FILE_SIZE_THRESHOLDS.general;
|
|
335
353
|
const label = isTest ? 'Test file' : 'New file';
|
|
336
354
|
const verb = isTest ? 'splitting' : 'decomposing';
|
|
337
|
-
if (lineCount
|
|
355
|
+
if (lineCount > thresholds.error) {
|
|
338
356
|
issues.push({
|
|
339
357
|
file,
|
|
340
358
|
check: 'file-too-large',
|
|
@@ -342,7 +360,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
342
360
|
severity: 'error',
|
|
343
361
|
});
|
|
344
362
|
}
|
|
345
|
-
else if (lineCount
|
|
363
|
+
else if (lineCount > thresholds.warning) {
|
|
346
364
|
issues.push({
|
|
347
365
|
file,
|
|
348
366
|
check: 'file-too-large',
|
|
@@ -350,7 +368,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
350
368
|
severity: 'warning',
|
|
351
369
|
});
|
|
352
370
|
}
|
|
353
|
-
else if (lineCount
|
|
371
|
+
else if (lineCount > thresholds.notice) {
|
|
354
372
|
issues.push({
|
|
355
373
|
file,
|
|
356
374
|
check: 'file-too-large',
|
|
@@ -507,7 +525,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
507
525
|
severity: 'warning',
|
|
508
526
|
});
|
|
509
527
|
}
|
|
510
|
-
if (lineCount
|
|
528
|
+
if (lineCount > lineThresholds.error) {
|
|
511
529
|
issues.push({
|
|
512
530
|
file: AGGREGATE_PATCH_FILE,
|
|
513
531
|
check: 'large-patch-lines',
|
|
@@ -515,7 +533,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
515
533
|
severity: 'error',
|
|
516
534
|
});
|
|
517
535
|
}
|
|
518
|
-
else if (lineCount
|
|
536
|
+
else if (lineCount > lineThresholds.warning) {
|
|
519
537
|
issues.push({
|
|
520
538
|
file: AGGREGATE_PATCH_FILE,
|
|
521
539
|
check: 'large-patch-lines',
|
|
@@ -523,7 +541,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
523
541
|
severity: 'warning',
|
|
524
542
|
});
|
|
525
543
|
}
|
|
526
|
-
else if (lineCount
|
|
544
|
+
else if (lineCount > lineThresholds.notice) {
|
|
527
545
|
issues.push({
|
|
528
546
|
file: AGGREGATE_PATCH_FILE,
|
|
529
547
|
check: 'large-patch-lines',
|
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
* normal after `fireforge import`.
|
|
12
12
|
* - `patch-owned-drift`: the file is claimed by exactly one patch, but
|
|
13
13
|
* the live engine content no longer matches that patch's expected
|
|
14
|
-
* post-apply content.
|
|
14
|
+
* post-apply content. This includes furnace-prefixed paths: a path
|
|
15
|
+
* can be both Furnace-managed and patch-claimed (export a deployed
|
|
16
|
+
* component, then edit the workspace source and `furnace deploy`
|
|
17
|
+
* again — the deployed copy now has content the patch body lacks).
|
|
18
|
+
* Before 0.38.0 the furnace prefix check ran first and silently
|
|
19
|
+
* bucketed such files as `furnace`, so `status` reported the stale
|
|
20
|
+
* patch as owned.
|
|
15
21
|
* - `unmanaged`: edits not explained by any patch or tool — local
|
|
16
22
|
* drift to export or discard.
|
|
17
23
|
* - `branding`: files under tool-managed branding paths, written by
|
|
18
24
|
* FireForge's branding pipeline.
|
|
19
|
-
* - `furnace`: files under Furnace-managed component prefixes.
|
|
25
|
+
* - `furnace`: files under Furnace-managed component prefixes. When
|
|
26
|
+
* exactly one patch also claims the path, this bucket asserts the
|
|
27
|
+
* live content matches that patch's expected post-apply content —
|
|
28
|
+
* a mismatch is reported as `patch-owned-drift` instead (see above).
|
|
20
29
|
* - `conflict`: the file is claimed by two or more patches in
|
|
21
30
|
* `patches.json`. The human `--ownership` mode already surfaces
|
|
22
31
|
* this bucket as `CONFLICT`; the classification is carried through
|
|
@@ -55,6 +55,40 @@ function isGeneratedBrandingPath(file, binaryName) {
|
|
|
55
55
|
normalized === `${brandingRoot}/locales/en-US/brand.properties` ||
|
|
56
56
|
normalized === `${brandingRoot}/locales/en-US/brand.ftl`);
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Compares a single-owner file's live engine content against its owning
|
|
60
|
+
* patch's expected post-apply content. `matchClassification` is what a
|
|
61
|
+
* clean match reports — `patch-backed` for ordinary patch-claimed paths,
|
|
62
|
+
* `furnace` for furnace-prefixed paths so healthy deployed components
|
|
63
|
+
* keep landing in the pinned furnace bucket. Any mismatch (including a
|
|
64
|
+
* deletion the patch does not expect, or a failed comparison) reports
|
|
65
|
+
* `patch-owned-drift`.
|
|
66
|
+
*/
|
|
67
|
+
async function classifySingleOwnerFile(entry, engineDir, patchesDir, matchClassification) {
|
|
68
|
+
if (getPrimaryStatusCode(entry.status) === 'D') {
|
|
69
|
+
// Deleted file: content matches only if the patch expects deletion
|
|
70
|
+
const expected = await computePatchedContent(patchesDir, engineDir, entry.file);
|
|
71
|
+
return {
|
|
72
|
+
...entry,
|
|
73
|
+
classification: expected === null ? matchClassification : 'patch-owned-drift',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// File exists on disk — compare actual vs expected
|
|
77
|
+
try {
|
|
78
|
+
const [expected, actual] = await Promise.all([
|
|
79
|
+
computePatchedContent(patchesDir, engineDir, entry.file),
|
|
80
|
+
readText(join(engineDir, entry.file)),
|
|
81
|
+
]);
|
|
82
|
+
return {
|
|
83
|
+
...entry,
|
|
84
|
+
classification: actual === expected ? matchClassification : 'patch-owned-drift',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
verbose(`Treating ${entry.file} as patch-owned drift because patch-backed classification failed: ${toError(error).message}`);
|
|
89
|
+
return { ...entry, classification: 'patch-owned-drift' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
58
92
|
/**
|
|
59
93
|
* Classifies files into patch-backed, unmanaged, branding, furnace, or
|
|
60
94
|
* conflict buckets.
|
|
@@ -104,7 +138,20 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
104
138
|
}
|
|
105
139
|
}
|
|
106
140
|
if (isFurnace) {
|
|
107
|
-
|
|
141
|
+
// A furnace path claimed by exactly one patch gets the same
|
|
142
|
+
// expected-vs-actual comparison as any other single-owner path:
|
|
143
|
+
// after a `furnace deploy` of an edited component the deployed
|
|
144
|
+
// copy has content the owning patch's body lacks, and the old
|
|
145
|
+
// unconditional short-circuit silently bucketed that drift as
|
|
146
|
+
// `furnace`. Multi-owner and unowned furnace paths keep the
|
|
147
|
+
// short-circuit — the ownership table independently flags
|
|
148
|
+
// filesAffected conflicts.
|
|
149
|
+
if (owners && owners.length === 1) {
|
|
150
|
+
results.push(await classifySingleOwnerFile(entry, engineDir, patchesDir, 'furnace'));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
results.push({ ...entry, classification: 'furnace' });
|
|
154
|
+
}
|
|
108
155
|
continue;
|
|
109
156
|
}
|
|
110
157
|
}
|
|
@@ -127,30 +174,7 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
127
174
|
continue;
|
|
128
175
|
}
|
|
129
176
|
// File is claimed by exactly one patch — compare content.
|
|
130
|
-
|
|
131
|
-
// Deleted file: patch-backed only if patch expects deletion
|
|
132
|
-
const expected = await computePatchedContent(patchesDir, engineDir, entry.file);
|
|
133
|
-
results.push({
|
|
134
|
-
...entry,
|
|
135
|
-
classification: expected === null ? 'patch-backed' : 'patch-owned-drift',
|
|
136
|
-
});
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
// File exists on disk — compare actual vs expected
|
|
140
|
-
try {
|
|
141
|
-
const [expected, actual] = await Promise.all([
|
|
142
|
-
computePatchedContent(patchesDir, engineDir, entry.file),
|
|
143
|
-
readText(join(engineDir, entry.file)),
|
|
144
|
-
]);
|
|
145
|
-
results.push({
|
|
146
|
-
...entry,
|
|
147
|
-
classification: actual === expected ? 'patch-backed' : 'patch-owned-drift',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
verbose(`Treating ${entry.file} as patch-owned drift because patch-backed classification failed: ${toError(error).message}`);
|
|
152
|
-
results.push({ ...entry, classification: 'patch-owned-drift' });
|
|
153
|
-
}
|
|
177
|
+
results.push(await classifySingleOwnerFile(entry, engineDir, patchesDir, 'patch-backed'));
|
|
154
178
|
}
|
|
155
179
|
return results;
|
|
156
180
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isXpcomManifestPath } from './build-audit.js';
|
|
1
2
|
import type { BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
|
|
3
|
+
export { isXpcomManifestPath };
|
|
2
4
|
/** Result of the stale-build preflight probe. */
|
|
3
5
|
export interface StaleBuildResult {
|
|
4
6
|
/** True when at least one packageable engine file changed since the baseline. */
|
|
@@ -53,7 +55,10 @@ export declare const FULL_SUITE_REQUEST = "(entire suite)";
|
|
|
53
55
|
*
|
|
54
56
|
* Coverage semantics: `undefined` (pre-0.37.0 baseline) and `'full'` cover
|
|
55
57
|
* everything. A scoped list covers a request path when the request equals a
|
|
56
|
-
* covered entry
|
|
58
|
+
* covered entry, sits beneath a covered directory entry, or shares a
|
|
59
|
+
* manifest granule with a covered entry ({@link toManifestGranule} — a
|
|
60
|
+
* scoped `test --build` packages the whole manifest directory, so a
|
|
61
|
+
* same-manifest sibling of a covered file is packaged too). Both sides are
|
|
57
62
|
* normalized to forward slashes so Windows-style CLI input cannot defeat
|
|
58
63
|
* the prefix rule (baseline paths are POSIX by convention). A request with
|
|
59
64
|
* no paths is a full-suite run and is never covered by a scoped list — the
|
|
@@ -68,3 +73,38 @@ export declare function findUncoveredRequestPaths(coverage: TestPackagingCoverag
|
|
|
68
73
|
* independently (same split as {@link formatStaleBuildWarning}).
|
|
69
74
|
*/
|
|
70
75
|
export declare function formatTestCoverageRefusal(uncovered: string[], coverage: string[]): string;
|
|
76
|
+
/** Result of the compiled-StaticComponents staleness probe. */
|
|
77
|
+
export interface StaticComponentsStaleResult {
|
|
78
|
+
/** True when at least one `components.conf` genuinely diverged from the anchor. */
|
|
79
|
+
stale: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Engine-relative `components.conf` paths changed since the last FULL
|
|
82
|
+
* build. Sorted; NOT capped — {@link formatStaticComponentsRefusal}
|
|
83
|
+
* applies the render cap.
|
|
84
|
+
*/
|
|
85
|
+
changedManifests: string[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Probes whether any `components.conf` changed since the last FULL
|
|
89
|
+
* `fireforge build` — i.e. since the compiled StaticComponents table was
|
|
90
|
+
* last regenerated. `components.conf` entries bake into compiled code; a
|
|
91
|
+
* scoped `test --build` packages the file but the child process resolves
|
|
92
|
+
* the OLD table and fails with `NS_ERROR_MALFORMED_URI` that reads as a
|
|
93
|
+
* test bug.
|
|
94
|
+
*
|
|
95
|
+
* The diff anchors to the baseline's `staticComponentsBaseline` (the last
|
|
96
|
+
* full build's engine HEAD SHA), NOT the baseline's own `engineHeadSha`
|
|
97
|
+
* which a scoped `test --build` advances. Dirty candidates are hash-checked
|
|
98
|
+
* against the anchor's fingerprints so only genuine content divergence
|
|
99
|
+
* counts. No baseline / no anchor (pre-0.38.0 marker) → fresh. Never
|
|
100
|
+
* throws — the probes it composes degrade to verbose lines and empty
|
|
101
|
+
* results on git failure, matching {@link checkStaleBuildForTest}.
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkStaticComponentsStale(engineDir: string, baseline: BuildBaseline | undefined): Promise<StaticComponentsStaleResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Formats the refusal shown when a run would dispatch against a stale
|
|
106
|
+
* compiled StaticComponents table. Same probe/copy split as
|
|
107
|
+
* {@link formatStaleBuildWarning} so tests can pin structure and wording
|
|
108
|
+
* independently.
|
|
109
|
+
*/
|
|
110
|
+
export declare function formatStaticComponentsRefusal(changedManifests: string[]): string;
|
|
@@ -28,9 +28,10 @@ import { readFile } from 'node:fs/promises';
|
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
import { toError } from '../utils/errors.js';
|
|
30
30
|
import { verbose } from '../utils/logger.js';
|
|
31
|
-
import { isPackageablePath } from './build-audit.js';
|
|
31
|
+
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
32
32
|
import { readBuildBaseline } from './build-baseline.js';
|
|
33
33
|
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
34
|
+
export { isXpcomManifestPath };
|
|
34
35
|
/** Cap on the number of changed paths rendered inline. */
|
|
35
36
|
const STALE_PATHS_LIMIT = 10;
|
|
36
37
|
/**
|
|
@@ -125,7 +126,10 @@ export const FULL_SUITE_REQUEST = '(entire suite)';
|
|
|
125
126
|
*
|
|
126
127
|
* Coverage semantics: `undefined` (pre-0.37.0 baseline) and `'full'` cover
|
|
127
128
|
* everything. A scoped list covers a request path when the request equals a
|
|
128
|
-
* covered entry
|
|
129
|
+
* covered entry, sits beneath a covered directory entry, or shares a
|
|
130
|
+
* manifest granule with a covered entry ({@link toManifestGranule} — a
|
|
131
|
+
* scoped `test --build` packages the whole manifest directory, so a
|
|
132
|
+
* same-manifest sibling of a covered file is packaged too). Both sides are
|
|
129
133
|
* normalized to forward slashes so Windows-style CLI input cannot defeat
|
|
130
134
|
* the prefix rule (baseline paths are POSIX by convention). A request with
|
|
131
135
|
* no paths is a full-suite run and is never covered by a scoped list — the
|
|
@@ -141,9 +145,29 @@ export function findUncoveredRequestPaths(coverage, requestedPaths) {
|
|
|
141
145
|
}
|
|
142
146
|
return requestedPaths.filter((requested) => {
|
|
143
147
|
const path = normalizeCoveragePath(requested);
|
|
144
|
-
|
|
148
|
+
const granule = toManifestGranule(path);
|
|
149
|
+
return !covered.some((c) => path === c || path.startsWith(`${c}/`) || granule === toManifestGranule(c));
|
|
145
150
|
});
|
|
146
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Maps a normalized request/coverage path to the "manifest granule" the
|
|
154
|
+
* packaged runtime actually staged: an extension-bearing basename (a test
|
|
155
|
+
* FILE) maps to its containing directory, a directory (no dot in the
|
|
156
|
+
* basename) maps to itself. Purely lexical — the directory-as-manifest
|
|
157
|
+
* approximation holds because xpcshell/mochitest manifests live next to
|
|
158
|
+
* their test files and a scoped `test --build` stages the whole manifest
|
|
159
|
+
* directory into `obj-*`/`_tests/`, not single files. Caveat: a DIRECTORY
|
|
160
|
+
* whose basename contains a dot is misread as a file and mapped to its
|
|
161
|
+
* parent, which widens (never narrows) the covered granule.
|
|
162
|
+
*/
|
|
163
|
+
function toManifestGranule(path) {
|
|
164
|
+
const slash = path.lastIndexOf('/');
|
|
165
|
+
const base = slash === -1 ? path : path.slice(slash + 1);
|
|
166
|
+
if (!base.includes('.')) {
|
|
167
|
+
return path;
|
|
168
|
+
}
|
|
169
|
+
return slash === -1 ? '' : path.slice(0, slash);
|
|
170
|
+
}
|
|
147
171
|
/** Normalizes a path for coverage comparison: forward slashes, no trailing slash. */
|
|
148
172
|
function normalizeCoveragePath(path) {
|
|
149
173
|
return path.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
@@ -171,3 +195,53 @@ export function formatTestCoverageRefusal(uncovered, coverage) {
|
|
|
171
195
|
`rather than fail. ${rebuildHint} ` +
|
|
172
196
|
'(--allow-stale-build does not bypass this check — it accepts stale content, not missing coverage.)');
|
|
173
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Probes whether any `components.conf` changed since the last FULL
|
|
200
|
+
* `fireforge build` — i.e. since the compiled StaticComponents table was
|
|
201
|
+
* last regenerated. `components.conf` entries bake into compiled code; a
|
|
202
|
+
* scoped `test --build` packages the file but the child process resolves
|
|
203
|
+
* the OLD table and fails with `NS_ERROR_MALFORMED_URI` that reads as a
|
|
204
|
+
* test bug.
|
|
205
|
+
*
|
|
206
|
+
* The diff anchors to the baseline's `staticComponentsBaseline` (the last
|
|
207
|
+
* full build's engine HEAD SHA), NOT the baseline's own `engineHeadSha`
|
|
208
|
+
* which a scoped `test --build` advances. Dirty candidates are hash-checked
|
|
209
|
+
* against the anchor's fingerprints so only genuine content divergence
|
|
210
|
+
* counts. No baseline / no anchor (pre-0.38.0 marker) → fresh. Never
|
|
211
|
+
* throws — the probes it composes degrade to verbose lines and empty
|
|
212
|
+
* results on git failure, matching {@link checkStaleBuildForTest}.
|
|
213
|
+
*/
|
|
214
|
+
export async function checkStaticComponentsStale(engineDir, baseline) {
|
|
215
|
+
const anchor = baseline?.staticComponentsBaseline;
|
|
216
|
+
if (baseline === undefined || anchor === undefined) {
|
|
217
|
+
return { stale: false, changedManifests: [] };
|
|
218
|
+
}
|
|
219
|
+
const changed = await collectChangedEnginePaths(engineDir, { ...baseline, engineHeadSha: anchor.engineHeadSha }, 'Static-components preflight');
|
|
220
|
+
const changedManifests = [];
|
|
221
|
+
for (const path of changed.filter((p) => isXpcomManifestPath(p))) {
|
|
222
|
+
const recorded = anchor.fingerprints[path];
|
|
223
|
+
const live = await hashEngineFile(engineDir, path);
|
|
224
|
+
if (recorded === undefined || live === undefined || recorded !== live) {
|
|
225
|
+
changedManifests.push(path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { stale: changedManifests.length > 0, changedManifests };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Formats the refusal shown when a run would dispatch against a stale
|
|
232
|
+
* compiled StaticComponents table. Same probe/copy split as
|
|
233
|
+
* {@link formatStaleBuildWarning} so tests can pin structure and wording
|
|
234
|
+
* independently.
|
|
235
|
+
*/
|
|
236
|
+
export function formatStaticComponentsRefusal(changedManifests) {
|
|
237
|
+
const head = changedManifests.slice(0, STALE_PATHS_LIMIT);
|
|
238
|
+
const truncated = changedManifests.length - head.length;
|
|
239
|
+
const list = head.join(', ') + (truncated > 0 ? `, … (+${truncated} more)` : '');
|
|
240
|
+
return (`The compiled StaticComponents table is stale: ${list} changed since the last full "fireforge build".\n` +
|
|
241
|
+
'components.conf registrations are baked into compiled code that only a FULL build ' +
|
|
242
|
+
'regenerates — a scoped "fireforge test --build" repackages files but the child process ' +
|
|
243
|
+
'resolves the old component table and fails with NS_ERROR_MALFORMED_URI that reads as a ' +
|
|
244
|
+
'test bug. Run "fireforge build" first. (--allow-stale-build does not bypass this check — ' +
|
|
245
|
+
'it accepts stale packaged content, not a stale compiled registration. Pass ' +
|
|
246
|
+
'--allow-stale-components only if you rebuilt out-of-band and accept the risk.)');
|
|
247
|
+
}
|