@hominis/fireforge 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/src/commands/export-all.js +4 -1
- package/dist/src/commands/export-shared.js +10 -1
- package/dist/src/commands/export.js +5 -1
- package/dist/src/commands/lint-per-patch.d.ts +2 -0
- package/dist/src/commands/lint-per-patch.js +206 -44
- package/dist/src/commands/lint.js +100 -7
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/test-run.d.ts +10 -0
- package/dist/src/commands/test-run.js +13 -4
- package/dist/src/commands/test.js +46 -7
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +52 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-jsconfig.js +22 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -0
- package/dist/src/core/git-file-ops.js +82 -1
- package/dist/src/core/mach-resource-shim.d.ts +21 -0
- package/dist/src/core/mach-resource-shim.js +92 -0
- package/dist/src/core/mach.d.ts +17 -0
- package/dist/src/core/mach.js +30 -2
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +213 -67
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +19 -163
- package/dist/src/core/test-harness-crash.d.ts +6 -3
- package/dist/src/core/test-harness-crash.js +32 -4
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +9 -4
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck-shim.d.ts +3 -1
- package/dist/src/core/typecheck-shim.js +43 -3
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/types/commands/options.d.ts +22 -0
- package/dist/src/types/config.d.ts +24 -2
- package/package.json +3 -3
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import { toError } from '../utils/errors.js';
|
|
4
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
5
|
-
import {
|
|
6
|
-
import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
|
|
7
|
-
import { loadFurnaceConfig } from './furnace-config.js';
|
|
4
|
+
import { stripJsComments } from '../utils/regex.js';
|
|
8
5
|
import { containsUpstreamLicenseText, getLicenseHeader, hasAnyLicenseHeader, hasAnyLicenseHeaderAnyStyle, } from './license-headers.js';
|
|
9
6
|
import { invokePatchLintCheckJs } from './patch-lint-checkjs.js';
|
|
10
7
|
import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
|
|
8
|
+
import { lintPatchedCss } from './patch-lint-css.js';
|
|
11
9
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
12
10
|
import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
|
|
13
11
|
import { hasRelativeImport } from './patch-lint-imports.js';
|
|
@@ -25,6 +23,10 @@ import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch
|
|
|
25
23
|
// public surface from `patch-lint-reexports.ts` so callers continue to
|
|
26
24
|
// import from a single module.
|
|
27
25
|
export * from './patch-lint-reexports.js';
|
|
26
|
+
// The CSS rule bodies live in `patch-lint-css.ts` (same per-file-budget
|
|
27
|
+
// split as the other rule families); re-export the imported binding so
|
|
28
|
+
// callers and tests keep importing `lintPatchedCss` from this module.
|
|
29
|
+
export { lintPatchedCss };
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// Helpers
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
@@ -205,158 +207,6 @@ export function commentStyleForFile(file) {
|
|
|
205
207
|
return 'js';
|
|
206
208
|
return null;
|
|
207
209
|
}
|
|
208
|
-
/**
|
|
209
|
-
* Loads the furnace token-prefix lint inputs gracefully — returns
|
|
210
|
-
* undefined (skipping the token-prefix check) when furnace.json cannot
|
|
211
|
-
* be loaded or no tokenPrefix is configured.
|
|
212
|
-
*/
|
|
213
|
-
async function loadCssTokenContext(repoDir) {
|
|
214
|
-
try {
|
|
215
|
-
const root = join(repoDir, '..');
|
|
216
|
-
const furnaceConfig = await loadFurnaceConfig(root);
|
|
217
|
-
if (furnaceConfig.tokenPrefix) {
|
|
218
|
-
return {
|
|
219
|
-
tokenPrefix: furnaceConfig.tokenPrefix,
|
|
220
|
-
tokenAllowlist: new Set(furnaceConfig.tokenAllowlist ?? []),
|
|
221
|
-
runtimeVariables: new Set(furnaceConfig.runtimeVariables ?? []),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
verbose(`Skipping furnace token-prefix lint hints because furnace.json could not be loaded: ${toError(error).message}`);
|
|
227
|
-
}
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Raw-color check for one patched CSS file, scoped to introduced lines
|
|
232
|
-
* when diff context is available. Pushes onto `issues`.
|
|
233
|
-
*/
|
|
234
|
-
function checkRawColorValues(file, rawCss, addedLinesByFile, config, issues) {
|
|
235
|
-
// Check only introduced raw color values when diff context is available.
|
|
236
|
-
// Skip files on the raw-color allowlist (exact path or basename match) and
|
|
237
|
-
// auto-exempt files under `browser/branding/` — those are the fork's
|
|
238
|
-
// visual identity assets (app-about dialogs, installer pages, branded
|
|
239
|
-
// CSS copied from Firefox's `unofficial` template) and belong to the
|
|
240
|
-
// design-decision layer the design-token system does not govern.
|
|
241
|
-
// Without this auto-exemption, every first-time setup's copied CSS
|
|
242
|
-
// failed `raw-color-value` with no actionable fix other than manually
|
|
243
|
-
// listing each path in `rawColorAllowlist`.
|
|
244
|
-
const allowlist = config?.patchLint?.rawColorAllowlist;
|
|
245
|
-
const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
|
|
246
|
-
const isBranding = file.startsWith('browser/branding/');
|
|
247
|
-
if (!isAllowlisted && !isBranding) {
|
|
248
|
-
// Strip lines with inline fireforge-ignore: raw-color-value suppression.
|
|
249
|
-
// Check against rawCss (before comment stripping) so the CSS comment marker is still present.
|
|
250
|
-
const sourceForSuppression = addedLinesByFile
|
|
251
|
-
? (addedLinesByFile.get(file) ?? []).join('\n')
|
|
252
|
-
: rawCss;
|
|
253
|
-
const suppressedContent = sourceForSuppression
|
|
254
|
-
.split('\n')
|
|
255
|
-
.filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
|
|
256
|
-
.join('\n')
|
|
257
|
-
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
258
|
-
if (hasRawCssColors(suppressedContent)) {
|
|
259
|
-
issues.push({
|
|
260
|
-
file,
|
|
261
|
-
check: 'raw-color-value',
|
|
262
|
-
message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
|
|
263
|
-
severity: 'error',
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Token-prefix check for one patched CSS file: flags `var(--x)` references
|
|
270
|
-
* that match neither the configured prefix, the allowlist, the runtime
|
|
271
|
-
* variables, nor a same-file declaration. Pushes onto `issues`.
|
|
272
|
-
*/
|
|
273
|
-
function checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues) {
|
|
274
|
-
// Check for non-tokenized custom properties. A variable that is both
|
|
275
|
-
// declared and consumed inside the same file is auto-exempted as a
|
|
276
|
-
// runtime state channel (see furnace.json → runtimeVariables).
|
|
277
|
-
//
|
|
278
|
-
// When diff context is available, scope the `var(...)` scan to
|
|
279
|
-
// added/modified lines only. `cssContent` (full-file) is still the
|
|
280
|
-
// source of `localDeclarations` so vars declared anywhere in the file
|
|
281
|
-
// are recognised as same-file refs regardless of where the consuming
|
|
282
|
-
// `var(...)` appears. Before this scoping change, a small edit to a
|
|
283
|
-
// Furnace override of a stock component (e.g. moz-card) produced a
|
|
284
|
-
// `token-prefix-violation` for every stock `var(--moz-card-*)` the
|
|
285
|
-
// upstream file already carried, because the scanner saw the full
|
|
286
|
-
// applied file and flagged each inherited reference as if the fork
|
|
287
|
-
// had introduced it.
|
|
288
|
-
if (tokenContext) {
|
|
289
|
-
const declarationPattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
|
|
290
|
-
const localDeclarations = new Set();
|
|
291
|
-
let declMatch;
|
|
292
|
-
while ((declMatch = declarationPattern.exec(cssContent)) !== null) {
|
|
293
|
-
const name = declMatch[1];
|
|
294
|
-
if (name)
|
|
295
|
-
localDeclarations.add(name);
|
|
296
|
-
}
|
|
297
|
-
const prefixScanSource = addedLinesByFile
|
|
298
|
-
? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
299
|
-
: cssContent;
|
|
300
|
-
if (prefixScanSource.length > 0) {
|
|
301
|
-
const varPattern = /var\(\s*(--[\w-]+)/g;
|
|
302
|
-
const flaggedProps = new Set();
|
|
303
|
-
let match;
|
|
304
|
-
while ((match = varPattern.exec(prefixScanSource)) !== null) {
|
|
305
|
-
const prop = match[1];
|
|
306
|
-
if (!prop)
|
|
307
|
-
continue;
|
|
308
|
-
if (prop.startsWith(tokenContext.tokenPrefix))
|
|
309
|
-
continue;
|
|
310
|
-
if (tokenContext.tokenAllowlist.has(prop))
|
|
311
|
-
continue;
|
|
312
|
-
if (tokenContext.runtimeVariables.has(prop))
|
|
313
|
-
continue;
|
|
314
|
-
if (localDeclarations.has(prop))
|
|
315
|
-
continue;
|
|
316
|
-
// De-duplicate per (file, prop) pair so the same introduced var
|
|
317
|
-
// used five times in the added hunk doesn't produce five
|
|
318
|
-
// identical issue entries.
|
|
319
|
-
if (flaggedProps.has(prop))
|
|
320
|
-
continue;
|
|
321
|
-
flaggedProps.add(prop);
|
|
322
|
-
issues.push({
|
|
323
|
-
file,
|
|
324
|
-
check: 'token-prefix-violation',
|
|
325
|
-
message: `CSS references var(${prop}) which does not match the required token prefix "${tokenContext.tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`,
|
|
326
|
-
severity: 'error',
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Lints patched CSS files for introduced raw color values and non-tokenized
|
|
334
|
-
* custom properties.
|
|
335
|
-
*
|
|
336
|
-
* @param repoDir - Absolute path to the engine (repository) directory
|
|
337
|
-
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
338
|
-
* @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
|
|
339
|
-
* @returns Array of lint issues found
|
|
340
|
-
*/
|
|
341
|
-
export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config) {
|
|
342
|
-
const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
|
|
343
|
-
if (cssFiles.length === 0)
|
|
344
|
-
return [];
|
|
345
|
-
const tokenContext = await loadCssTokenContext(repoDir);
|
|
346
|
-
const issues = [];
|
|
347
|
-
const addedLinesByFile = diffContent ? extractAddedLinesPerFile(diffContent) : undefined;
|
|
348
|
-
for (const file of cssFiles) {
|
|
349
|
-
const filePath = join(repoDir, file);
|
|
350
|
-
if (!(await pathExists(filePath)))
|
|
351
|
-
continue;
|
|
352
|
-
const rawCss = await readText(filePath);
|
|
353
|
-
// Strip block comments before scanning
|
|
354
|
-
const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
355
|
-
checkRawColorValues(file, rawCss, addedLinesByFile, config, issues);
|
|
356
|
-
checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues);
|
|
357
|
-
}
|
|
358
|
-
return issues;
|
|
359
|
-
}
|
|
360
210
|
// ---------------------------------------------------------------------------
|
|
361
211
|
// License header lint
|
|
362
212
|
// ---------------------------------------------------------------------------
|
|
@@ -695,9 +545,6 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
|
|
|
695
545
|
}
|
|
696
546
|
return issues;
|
|
697
547
|
}
|
|
698
|
-
// ---------------------------------------------------------------------------
|
|
699
|
-
// Orchestrator
|
|
700
|
-
// ---------------------------------------------------------------------------
|
|
701
548
|
/**
|
|
702
549
|
* Runs all patch lint checks and returns combined issues.
|
|
703
550
|
*
|
|
@@ -717,9 +564,11 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
|
|
|
717
564
|
* per-patch manifest context (re-export, per-patch lint) should
|
|
718
565
|
* pass this; aggregate-mode callers without a specific patch
|
|
719
566
|
* context skip it and fall through to auto-detection.
|
|
567
|
+
* @param options - Optional behaviour switches; see
|
|
568
|
+
* {@link LintExportedPatchOptions}.
|
|
720
569
|
* @returns Array of all lint issues found
|
|
721
570
|
*/
|
|
722
|
-
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx, ignoreChecks, patchTier) {
|
|
571
|
+
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx, ignoreChecks, patchTier, options) {
|
|
723
572
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
724
573
|
const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
|
|
725
574
|
const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
|
|
@@ -731,7 +580,9 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
731
580
|
lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
|
|
732
581
|
]);
|
|
733
582
|
const modCommentIssues = lintModificationComments(diffContent, config);
|
|
734
|
-
const sizeIssues =
|
|
583
|
+
const sizeIssues = options?.skipPatchSize
|
|
584
|
+
? []
|
|
585
|
+
: lintPatchSize(affectedFiles, lineCount, patchTier);
|
|
735
586
|
const issues = [
|
|
736
587
|
...sizeIssues,
|
|
737
588
|
...cssIssues,
|
|
@@ -740,8 +591,13 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
740
591
|
...jsIssues,
|
|
741
592
|
...modCommentIssues,
|
|
742
593
|
];
|
|
743
|
-
if (
|
|
744
|
-
|
|
594
|
+
if (options?.precomputedCheckJs) {
|
|
595
|
+
// Per-patch lint built one queue-wide program and already attributed
|
|
596
|
+
// this patch's findings — append them instead of rebuilding the program.
|
|
597
|
+
issues.push(...options.precomputedCheckJs);
|
|
598
|
+
}
|
|
599
|
+
else if (config.patchLint?.checkJs) {
|
|
600
|
+
issues.push(...(await invokePatchLintCheckJs(repoDir, patchOwnedFiles, config.patchLint, dirname(repoDir), options?.checkJsReportScope)));
|
|
745
601
|
}
|
|
746
602
|
// Filter out ignored checks last so every rule still runs (keeps the
|
|
747
603
|
// implementation uniform) but suppressed rules do not surface. We do not
|
|
@@ -44,9 +44,12 @@ export declare function detectHarnessCrashSignature(output: string): HarnessCras
|
|
|
44
44
|
* 1. A recognized crash signature wins regardless of exit code (the
|
|
45
45
|
* shutdown re-entry shape exits non-zero on an otherwise green run;
|
|
46
46
|
* the hang shape can even exit zero with a `Passed: 0` summary).
|
|
47
|
-
* 2. No
|
|
48
|
-
* `no-tests`, even when the exit code is zero.
|
|
49
|
-
*
|
|
47
|
+
* 2. No execution signal with explicit paths requested means no test ran —
|
|
48
|
+
* `no-tests`, even when the exit code is zero. The execution signal is
|
|
49
|
+
* a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
|
|
50
|
+
* OR the suite-specific xpcshell result-summary block (the xpcshell
|
|
51
|
+
* dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
|
|
52
|
+
* lines are still not trusted as evidence of execution.
|
|
50
53
|
* 3. Exit code zero with tests started is a pass; anything else is a
|
|
51
54
|
* test failure for the regular diagnosis chain.
|
|
52
55
|
*/
|
|
@@ -23,6 +23,20 @@
|
|
|
23
23
|
* of reporting phantom test failures (or phantom passes).
|
|
24
24
|
*/
|
|
25
25
|
const TEST_START_PATTERN = /\bTEST-START\b/;
|
|
26
|
+
/**
|
|
27
|
+
* Execution signals emitted by the suite-specific xpcshell dispatch
|
|
28
|
+
* (`mach xpcshell-test`), which does NOT print `TEST-START` lines the way
|
|
29
|
+
* the generic `mach test` / browser-chrome dispatch does. A passing
|
|
30
|
+
* single-file xpcshell run prints a result-summary block instead
|
|
31
|
+
* (`TEST_END: Test PASS`, `Ran 16 checks`, `Unexpected results: 0`), so
|
|
32
|
+
* keying execution purely on `TEST-START` mis-reads a green xpcshell run as
|
|
33
|
+
* "no tests started" (field report: a single-file xpcshell pass exited 1).
|
|
34
|
+
*
|
|
35
|
+
* These markers are xpcshell-specific on purpose: the bare
|
|
36
|
+
* `Passed: 0` / `Failed: 0` summary that the no-output hang shape prints is
|
|
37
|
+
* deliberately NOT matched here — that case must still read as `no-tests`.
|
|
38
|
+
*/
|
|
39
|
+
const XPCSHELL_RESULT_SUMMARY_PATTERN = /\bTEST_END\b|\bRan \d+ checks?\b|\bResult summary:/i;
|
|
26
40
|
const UNEXPECTED_LINE_PATTERN = /^.*\bTEST-UNEXPECTED-[A-Z-]+\b.*$/gm;
|
|
27
41
|
const SHUTDOWN_REENTRY_PATTERN = /Application shut down \(without crashing\) in the middle of a test/i;
|
|
28
42
|
const FOCUS_STALL_PATTERN = /must wait for focus/i;
|
|
@@ -49,6 +63,16 @@ function findLine(output, patterns) {
|
|
|
49
63
|
}
|
|
50
64
|
return undefined;
|
|
51
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* True when the captured output carries the suite-specific xpcshell
|
|
68
|
+
* result-summary block, which proves tests executed even though the
|
|
69
|
+
* xpcshell dispatch emits no `TEST-START` line. Used alongside
|
|
70
|
+
* `TEST_START_PATTERN` so a green single-file xpcshell run is not
|
|
71
|
+
* mis-classified as `no-tests`. Exported for direct unit testing.
|
|
72
|
+
*/
|
|
73
|
+
function hasXpcshellResultSummary(output) {
|
|
74
|
+
return XPCSHELL_RESULT_SUMMARY_PATTERN.test(output);
|
|
75
|
+
}
|
|
52
76
|
/** Unexpected-failure lines that are NOT the shutdown re-entry artifact. */
|
|
53
77
|
function realUnexpectedFailureLines(output) {
|
|
54
78
|
const matches = output.match(UNEXPECTED_LINE_PATTERN) ?? [];
|
|
@@ -95,9 +119,12 @@ export function detectHarnessCrashSignature(output) {
|
|
|
95
119
|
* 1. A recognized crash signature wins regardless of exit code (the
|
|
96
120
|
* shutdown re-entry shape exits non-zero on an otherwise green run;
|
|
97
121
|
* the hang shape can even exit zero with a `Passed: 0` summary).
|
|
98
|
-
* 2. No
|
|
99
|
-
* `no-tests`, even when the exit code is zero.
|
|
100
|
-
*
|
|
122
|
+
* 2. No execution signal with explicit paths requested means no test ran —
|
|
123
|
+
* `no-tests`, even when the exit code is zero. The execution signal is
|
|
124
|
+
* a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
|
|
125
|
+
* OR the suite-specific xpcshell result-summary block (the xpcshell
|
|
126
|
+
* dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
|
|
127
|
+
* lines are still not trusted as evidence of execution.
|
|
101
128
|
* 3. Exit code zero with tests started is a pass; anything else is a
|
|
102
129
|
* test failure for the regular diagnosis chain.
|
|
103
130
|
*/
|
|
@@ -106,7 +133,8 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
|
106
133
|
if (signature) {
|
|
107
134
|
return { kind: 'harness-crash', signature };
|
|
108
135
|
}
|
|
109
|
-
|
|
136
|
+
const ranTests = TEST_START_PATTERN.test(output) || hasXpcshellResultSummary(output);
|
|
137
|
+
if (!ranTests && requestedPaths.length > 0) {
|
|
110
138
|
return { kind: 'no-tests' };
|
|
111
139
|
}
|
|
112
140
|
return exitCode === 0 ? { kind: 'tests-ran-ok' } : { kind: 'test-failures' };
|
|
@@ -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,16 +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
16
|
return env
|
|
12
|
-
?
|
|
13
|
-
:
|
|
17
|
+
? dispatch(engineDir, normalizedPaths, extraArgs, env)
|
|
18
|
+
: dispatch(engineDir, normalizedPaths, extraArgs);
|
|
14
19
|
}
|
|
15
20
|
}
|
|
16
21
|
return result;
|
|
@@ -38,6 +38,15 @@ export declare function stripBlockCommentsInLines(lines: string[]): string[];
|
|
|
38
38
|
* can splice into the original `lines` array at the returned index.
|
|
39
39
|
*/
|
|
40
40
|
export declare function findDarkRootInsertionIndex(lines: string[]): number | null;
|
|
41
|
+
/**
|
|
42
|
+
* Depth-counts braces from `startLine` (whose lines must already have
|
|
43
|
+
* block comments stripped), returning the index of the line on which the
|
|
44
|
+
* block opened there returns to its entry depth — i.e. the line carrying
|
|
45
|
+
* the block's closing `}` — or -1 when the block never closes. The first
|
|
46
|
+
* `{` encountered sets the entry depth, so the scan may start on the
|
|
47
|
+
* selector/at-rule line itself rather than on the opener.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findBlockCloseIndex(stripped: string[], startLine: number): number;
|
|
41
50
|
/**
|
|
42
51
|
* Finds the closing `}` of the outermost
|
|
43
52
|
* `@media (prefers-color-scheme: dark)` block. Used as the fallback
|
|
@@ -122,7 +122,7 @@ export function findDarkRootInsertionIndex(lines) {
|
|
|
122
122
|
* `{` encountered sets the entry depth, so the scan may start on the
|
|
123
123
|
* selector/at-rule line itself rather than on the opener.
|
|
124
124
|
*/
|
|
125
|
-
function findBlockCloseIndex(stripped, startLine) {
|
|
125
|
+
export function findBlockCloseIndex(stripped, startLine) {
|
|
126
126
|
let depth = 0;
|
|
127
127
|
let entryDepth = 0;
|
|
128
128
|
let enteredBlock = false;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation-table updates for `fireforge token add`. Extracted from
|
|
3
|
+
* `token-manager.ts` so the CSS-mutation path and the Markdown-table path
|
|
4
|
+
* each stay within the per-file line budget. Consumed only by token-manager.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Minimal token shape the docs updater needs. Declared locally (rather than
|
|
8
|
+
* importing `AddTokenOptions`) so this module has no edge back to
|
|
9
|
+
* `token-manager.ts` — `AddTokenOptions` is structurally compatible.
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenDocInput {
|
|
12
|
+
tokenName: string;
|
|
13
|
+
value: string;
|
|
14
|
+
category: string;
|
|
15
|
+
mode: string;
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Adds a token row to the main token table, the unmapped table (for
|
|
20
|
+
* literal values), and bumps the mode count table. Each sub-update runs
|
|
21
|
+
* against a freshly parsed view of the document so that splice indices
|
|
22
|
+
* stay valid as rewrites are layered.
|
|
23
|
+
*
|
|
24
|
+
* @param annotation - The mode annotation string the caller already computed
|
|
25
|
+
* (kept here as a parameter so this module needs no dependency on
|
|
26
|
+
* token-manager's `getModeAnnotation`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function addTokenToDocs(engineDir: string, options: TokenDocInput, annotation: string): Promise<{
|
|
29
|
+
docsAdded: boolean;
|
|
30
|
+
unmappedAdded: boolean;
|
|
31
|
+
countUpdated: boolean;
|
|
32
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Documentation-table updates for `fireforge token add`. Extracted from
|
|
4
|
+
* `token-manager.ts` so the CSS-mutation path and the Markdown-table path
|
|
5
|
+
* each stay within the per-file line budget. Consumed only by token-manager.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
9
|
+
import { findTableAfterHeading, findTableByColumns, insertRow, rewriteTableRows, updateCellByKey, } from './markdown-table.js';
|
|
10
|
+
const TOKENS_DOC = 'docs/design/SRC_TOKENS.md';
|
|
11
|
+
/**
|
|
12
|
+
* Strips surrounding backticks from a cell, if present. Token cells are
|
|
13
|
+
* usually wrapped in inline code fences (`` `--foo` ``) and the parser
|
|
14
|
+
* returns them verbatim.
|
|
15
|
+
*/
|
|
16
|
+
function stripInlineCode(cell) {
|
|
17
|
+
const trimmed = cell.trim();
|
|
18
|
+
if (trimmed.startsWith('`') && trimmed.endsWith('`') && trimmed.length >= 2) {
|
|
19
|
+
return trimmed.slice(1, -1);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Adds a token row to the main token table, the unmapped table (for
|
|
25
|
+
* literal values), and bumps the mode count table. Each sub-update runs
|
|
26
|
+
* against a freshly parsed view of the document so that splice indices
|
|
27
|
+
* stay valid as rewrites are layered.
|
|
28
|
+
*
|
|
29
|
+
* @param annotation - The mode annotation string the caller already computed
|
|
30
|
+
* (kept here as a parameter so this module needs no dependency on
|
|
31
|
+
* token-manager's `getModeAnnotation`).
|
|
32
|
+
*/
|
|
33
|
+
export async function addTokenToDocs(engineDir, options, annotation) {
|
|
34
|
+
const filePath = join(engineDir, '..', TOKENS_DOC);
|
|
35
|
+
if (!(await pathExists(filePath))) {
|
|
36
|
+
// Docs file is optional
|
|
37
|
+
return { docsAdded: false, unmappedAdded: false, countUpdated: false };
|
|
38
|
+
}
|
|
39
|
+
const originalContent = await readText(filePath);
|
|
40
|
+
let lines = originalContent.split('\n');
|
|
41
|
+
let docsAdded = false;
|
|
42
|
+
let unmappedAdded = false;
|
|
43
|
+
let countUpdated = false;
|
|
44
|
+
const isLiteral = !options.value.startsWith('var(');
|
|
45
|
+
const mapsTo = isLiteral ? '—' : options.value.replace(/var\(([^)]+)\)/, '$1');
|
|
46
|
+
const tokenCell = `\`${options.tokenName}\``;
|
|
47
|
+
const valueCell = `\`${options.value}\``;
|
|
48
|
+
// --- Main token table: Category | Token | Value | Maps to | Mode ---
|
|
49
|
+
const mainTable = findTableByColumns(lines, ['Category', 'Token', 'Value', 'Mode']);
|
|
50
|
+
if (mainTable) {
|
|
51
|
+
// The doc convention allows the Category cell to be blank on
|
|
52
|
+
// continuation rows that belong to the previous category. Group rows
|
|
53
|
+
// by carrying the last non-empty Category value forward.
|
|
54
|
+
let lastGroupRowIndex = -1;
|
|
55
|
+
let currentCategory = '';
|
|
56
|
+
for (let i = 0; i < mainTable.rows.length; i++) {
|
|
57
|
+
const row = mainTable.rows[i];
|
|
58
|
+
if (!row)
|
|
59
|
+
continue;
|
|
60
|
+
const cell = row[0]?.trim() ?? '';
|
|
61
|
+
if (cell) {
|
|
62
|
+
currentCategory = cell;
|
|
63
|
+
}
|
|
64
|
+
if (currentCategory === options.category) {
|
|
65
|
+
lastGroupRowIndex = i;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (lastGroupRowIndex !== -1) {
|
|
69
|
+
insertRow(mainTable, ['', tokenCell, valueCell, mapsTo, annotation], lastGroupRowIndex + 1);
|
|
70
|
+
lines = rewriteTableRows(lines, mainTable);
|
|
71
|
+
docsAdded = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- Unmapped table: populated for literal (non-var()) values only ---
|
|
75
|
+
if (isLiteral) {
|
|
76
|
+
const unmappedTable = findTableAfterHeading(lines, /not yet mapped|unmapped/i);
|
|
77
|
+
if (unmappedTable) {
|
|
78
|
+
insertRow(unmappedTable, [tokenCell, valueCell, options.description ?? ''], unmappedTable.rows.length);
|
|
79
|
+
lines = rewriteTableRows(lines, unmappedTable);
|
|
80
|
+
unmappedAdded = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// --- Mode behavior count table: Mode | Count ---
|
|
84
|
+
const modeTable = findTableByColumns(lines, ['Mode', 'Count']);
|
|
85
|
+
if (modeTable) {
|
|
86
|
+
const modeIndex = modeTable.headers.indexOf('Mode');
|
|
87
|
+
const countIndex = modeTable.headers.indexOf('Count');
|
|
88
|
+
const existing = modeTable.rows.find((row) => stripInlineCode(row[modeIndex] ?? '') === options.mode);
|
|
89
|
+
if (existing) {
|
|
90
|
+
const oldCount = parseInt(existing[countIndex] ?? '0', 10);
|
|
91
|
+
const updated = updateCellByKey(modeTable, 'Mode', existing[modeIndex] ?? options.mode, 'Count', String((Number.isNaN(oldCount) ? 0 : oldCount) + 1));
|
|
92
|
+
if (updated) {
|
|
93
|
+
lines = rewriteTableRows(lines, modeTable);
|
|
94
|
+
countUpdated = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await writeText(filePath, lines.join('\n'));
|
|
99
|
+
return { docsAdded, unmappedAdded, countUpdated };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=token-docs.js.map
|
|
@@ -22,6 +22,14 @@ export interface AddTokenOptions {
|
|
|
22
22
|
dryRun?: boolean | undefined;
|
|
23
23
|
/** Declare the category banner in the tokens CSS when it does not exist yet. */
|
|
24
24
|
createCategory?: boolean | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Attribute selector fragment (e.g. `[data-skin="precision"]` or
|
|
27
|
+
* `[data-private]`) that routes the declaration into a top-level
|
|
28
|
+
* `:root<variant>` block instead of the base `:root` / category section.
|
|
29
|
+
* The block is created if absent and appended to if present. Variant
|
|
30
|
+
* overrides are CSS-only — the base token already owns its docs row.
|
|
31
|
+
*/
|
|
32
|
+
variant?: string | undefined;
|
|
25
33
|
}
|
|
26
34
|
/**
|
|
27
35
|
* Result of adding a token.
|