@hominis/fireforge 0.37.0 → 0.39.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 +33 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +13 -8
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +6 -2
- package/dist/src/commands/export-flow.js +17 -11
- package/dist/src/commands/export-placement-gate.js +4 -6
- 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-shared.js +17 -2
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +15 -5
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- 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-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +74 -15
- package/dist/src/commands/patch/rename.js +4 -1
- 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/reset.js +5 -0
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +17 -3
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +8 -2
- 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 +52 -81
- 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 +41 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +48 -7
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- 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 +14 -1
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- 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 +24 -0
- package/dist/src/core/license-headers.js +103 -1
- package/dist/src/core/patch-export.d.ts +10 -1
- package/dist/src/core/patch-export.js +12 -2
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
- package/dist/src/core/patch-lint-checkjs.js +30 -14
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +29 -8
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +56 -1
- package/dist/src/core/test-stale-check.js +114 -4
- package/dist/src/core/token-manager.js +39 -2
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/typecheck-shim.d.ts +13 -0
- package/dist/src/core/typecheck-shim.js +17 -2
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/types/commands/options.d.ts +69 -3
- package/dist/src/types/config.d.ts +22 -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
|
@@ -10,6 +10,7 @@ import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-dif
|
|
|
10
10
|
import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
|
|
11
11
|
import { hasRelativeImport } from './patch-lint-imports.js';
|
|
12
12
|
import { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
13
|
+
import { lintMozBuildSortedLists } from './patch-lint-mozbuild.js';
|
|
13
14
|
import { lintObserverTopics } from './patch-lint-observer.js';
|
|
14
15
|
import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -35,15 +36,33 @@ const FILE_SIZE_THRESHOLDS = {
|
|
|
35
36
|
general: { notice: 500, warning: 750, error: 900 },
|
|
36
37
|
test: { notice: 1200, warning: 1400, error: 1600 },
|
|
37
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Counts the lines of `content` the way `wc -l` reports them: a trailing
|
|
41
|
+
* newline terminates the last line rather than starting an empty extra
|
|
42
|
+
* one. A naive `split('\n').length` over-counts by one for any content
|
|
43
|
+
* ending in `\n` (the common case), which made the size rules fire one
|
|
44
|
+
* line early against the operator's own `wc -l` measurement.
|
|
45
|
+
*/
|
|
46
|
+
function countContentLines(content) {
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
if (lines[lines.length - 1] === '')
|
|
49
|
+
lines.pop();
|
|
50
|
+
return lines.length;
|
|
51
|
+
}
|
|
38
52
|
/**
|
|
39
53
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
40
54
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
41
55
|
*
|
|
56
|
+
* Line counting agrees with `wc -l`: a trailing newline does not add a
|
|
57
|
+
* phantom empty line to either `total` or the binary-hunk accounting.
|
|
58
|
+
*
|
|
42
59
|
* @param diffContent - Raw unified diff string
|
|
43
60
|
* @returns Object with `total` line count and `textLines` (total minus binary hunk lines)
|
|
44
61
|
*/
|
|
45
62
|
export function countNonBinaryDiffLines(diffContent) {
|
|
46
63
|
const lines = diffContent.split('\n');
|
|
64
|
+
if (lines[lines.length - 1] === '')
|
|
65
|
+
lines.pop();
|
|
47
66
|
const total = lines.length;
|
|
48
67
|
let binaryLines = 0;
|
|
49
68
|
let inBinaryHunk = false;
|
|
@@ -329,12 +348,12 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
329
348
|
}
|
|
330
349
|
// 2. File size check (new files only)
|
|
331
350
|
if (isNew) {
|
|
332
|
-
const lineCount = content
|
|
351
|
+
const lineCount = countContentLines(content);
|
|
333
352
|
const isTest = isTestFile(file);
|
|
334
353
|
const thresholds = isTest ? FILE_SIZE_THRESHOLDS.test : FILE_SIZE_THRESHOLDS.general;
|
|
335
354
|
const label = isTest ? 'Test file' : 'New file';
|
|
336
355
|
const verb = isTest ? 'splitting' : 'decomposing';
|
|
337
|
-
if (lineCount
|
|
356
|
+
if (lineCount > thresholds.error) {
|
|
338
357
|
issues.push({
|
|
339
358
|
file,
|
|
340
359
|
check: 'file-too-large',
|
|
@@ -342,7 +361,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
342
361
|
severity: 'error',
|
|
343
362
|
});
|
|
344
363
|
}
|
|
345
|
-
else if (lineCount
|
|
364
|
+
else if (lineCount > thresholds.warning) {
|
|
346
365
|
issues.push({
|
|
347
366
|
file,
|
|
348
367
|
check: 'file-too-large',
|
|
@@ -350,7 +369,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
350
369
|
severity: 'warning',
|
|
351
370
|
});
|
|
352
371
|
}
|
|
353
|
-
else if (lineCount
|
|
372
|
+
else if (lineCount > thresholds.notice) {
|
|
354
373
|
issues.push({
|
|
355
374
|
file,
|
|
356
375
|
check: 'file-too-large',
|
|
@@ -507,7 +526,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
507
526
|
severity: 'warning',
|
|
508
527
|
});
|
|
509
528
|
}
|
|
510
|
-
if (lineCount
|
|
529
|
+
if (lineCount > lineThresholds.error) {
|
|
511
530
|
issues.push({
|
|
512
531
|
file: AGGREGATE_PATCH_FILE,
|
|
513
532
|
check: 'large-patch-lines',
|
|
@@ -515,7 +534,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
515
534
|
severity: 'error',
|
|
516
535
|
});
|
|
517
536
|
}
|
|
518
|
-
else if (lineCount
|
|
537
|
+
else if (lineCount > lineThresholds.warning) {
|
|
519
538
|
issues.push({
|
|
520
539
|
file: AGGREGATE_PATCH_FILE,
|
|
521
540
|
check: 'large-patch-lines',
|
|
@@ -523,7 +542,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
523
542
|
severity: 'warning',
|
|
524
543
|
});
|
|
525
544
|
}
|
|
526
|
-
else if (lineCount
|
|
545
|
+
else if (lineCount > lineThresholds.notice) {
|
|
527
546
|
issues.push({
|
|
528
547
|
file: AGGREGATE_PATCH_FILE,
|
|
529
548
|
check: 'large-patch-lines',
|
|
@@ -598,11 +617,12 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
598
617
|
const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
|
|
599
618
|
const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
|
|
600
619
|
const patchOwnedChromeScripts = resolvePatchOwnedChromeScripts(newFiles, patchQueueCtx);
|
|
601
|
-
const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
|
|
620
|
+
const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues, mozBuildIssues] = await Promise.all([
|
|
602
621
|
lintPatchedCss(repoDir, affectedFiles, diffContent, config),
|
|
603
622
|
lintNewFileHeaders(repoDir, [...newFiles], config),
|
|
604
623
|
lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles, patchOwnedChromeScripts),
|
|
605
624
|
lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
|
|
625
|
+
lintMozBuildSortedLists(repoDir, affectedFiles),
|
|
606
626
|
]);
|
|
607
627
|
const modCommentIssues = lintModificationComments(diffContent, config);
|
|
608
628
|
const sizeIssues = options?.skipPatchSize
|
|
@@ -615,6 +635,7 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
615
635
|
...modifiedHeaderIssues,
|
|
616
636
|
...jsIssues,
|
|
617
637
|
...modCommentIssues,
|
|
638
|
+
...mozBuildIssues,
|
|
618
639
|
];
|
|
619
640
|
if (options?.precomputedCheckJs) {
|
|
620
641
|
// Per-patch lint built one queue-wide program and already attributed
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `patch reorder`, etc.).
|
|
8
8
|
*/
|
|
9
9
|
import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
|
|
10
|
-
import type { FireForgeConfig } from '../types/config.js';
|
|
10
|
+
import type { FireForgeConfig, PatchPolicyConfig, PatchPolicyRange } from '../types/config.js';
|
|
11
11
|
/** Stable issue codes returned by patch policy evaluation. */
|
|
12
12
|
export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
|
|
13
13
|
/** A single patch policy validation finding. */
|
|
@@ -28,6 +28,19 @@ export interface PatchPolicyEnforcementInput {
|
|
|
28
28
|
export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
|
|
29
29
|
/** Checks whether a category is accepted by legacy defaults or the policy ranges. */
|
|
30
30
|
export declare function isCategoryAllowedByConfig(config: FireForgeConfig, category: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Human-readable label for a category's configured ranges, e.g. `300-399`.
|
|
33
|
+
* Exported for the forward-import hint (FORGE F14), which must render the
|
|
34
|
+
* range it found no legal ordinal in.
|
|
35
|
+
*/
|
|
36
|
+
export declare function categoryRangeLabel(ranges: readonly PatchPolicyRange[], category: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the configured range that contains `order` for `category`, or
|
|
39
|
+
* null when no such range exists. Exported for the forward-import hint
|
|
40
|
+
* (FORGE F14), which suppresses ordinal suggestions the reorder policy
|
|
41
|
+
* would refuse.
|
|
42
|
+
*/
|
|
43
|
+
export declare function categoryRangeForOrder(cfg: PatchPolicyConfig, category: string, order: number): PatchPolicyRange | null;
|
|
31
44
|
/** Evaluates an entire patch manifest against the configured policy. */
|
|
32
45
|
export declare function evaluatePatchPolicy(config: FireForgeConfig, manifest: PatchesManifest): PatchPolicyIssue[];
|
|
33
46
|
/** Builds a sorted manifest snapshot from projected patch metadata. */
|
|
@@ -45,7 +45,12 @@ export function isCategoryAllowedByConfig(config, category) {
|
|
|
45
45
|
function rangeLabel(range) {
|
|
46
46
|
return `${String(range.from).padStart(3, '0')}-${String(range.to).padStart(3, '0')}`;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Human-readable label for a category's configured ranges, e.g. `300-399`.
|
|
50
|
+
* Exported for the forward-import hint (FORGE F14), which must render the
|
|
51
|
+
* range it found no legal ordinal in.
|
|
52
|
+
*/
|
|
53
|
+
export function categoryRangeLabel(ranges, category) {
|
|
49
54
|
const matches = ranges.filter((range) => range.category === category);
|
|
50
55
|
if (matches.length === 0)
|
|
51
56
|
return '(no configured range)';
|
|
@@ -54,7 +59,13 @@ function categoryRangeLabel(ranges, category) {
|
|
|
54
59
|
function reservedRangeForOrder(cfg, order) {
|
|
55
60
|
return cfg.reservedRanges?.find((range) => order >= range.from && order <= range.to) ?? null;
|
|
56
61
|
}
|
|
57
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Returns the configured range that contains `order` for `category`, or
|
|
64
|
+
* null when no such range exists. Exported for the forward-import hint
|
|
65
|
+
* (FORGE F14), which suppresses ordinal suggestions the reorder policy
|
|
66
|
+
* would refuse.
|
|
67
|
+
*/
|
|
68
|
+
export function categoryRangeForOrder(cfg, category, order) {
|
|
58
69
|
return (cfg.ranges.find((range) => {
|
|
59
70
|
return range.category === category && order >= range.from && order <= range.to;
|
|
60
71
|
}) ?? null);
|
|
@@ -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
|
}
|
|
@@ -154,6 +154,16 @@ export declare function classifyHarnessRun(exitCode: number, output: string, req
|
|
|
154
154
|
export declare function buildGreenSummaryRejectedMessage(rejection: GreenSummaryRejection, exitCode: number): string;
|
|
155
155
|
/** Builds the operator-facing failure message after retries are exhausted. */
|
|
156
156
|
export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number, commandLabel?: string): string;
|
|
157
|
+
/**
|
|
158
|
+
* Optional hint appended to the harness-crash message when a HEADED run on
|
|
159
|
+
* macOS died at the no-output timeout: the display may have slept and frozen
|
|
160
|
+
* the headed browser before any test started (FORGE F17). Pure — the
|
|
161
|
+
* platform is injected so unit tests need no mocking.
|
|
162
|
+
*/
|
|
163
|
+
export declare function headedNoOutputTimeoutHint(signature: HarnessCrashSignature, context: {
|
|
164
|
+
headless: boolean;
|
|
165
|
+
platform: string;
|
|
166
|
+
}): string | undefined;
|
|
157
167
|
/**
|
|
158
168
|
* Builds the message for a run that produced no `TEST-START` despite
|
|
159
169
|
* requesting paths — including exit-code-zero runs whose `Passed: 0`
|
|
@@ -537,6 +537,22 @@ export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'ma
|
|
|
537
537
|
'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
|
|
538
538
|
'patching it manually does not stick).');
|
|
539
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Optional hint appended to the harness-crash message when a HEADED run on
|
|
542
|
+
* macOS died at the no-output timeout: the display may have slept and frozen
|
|
543
|
+
* the headed browser before any test started (FORGE F17). Pure — the
|
|
544
|
+
* platform is injected so unit tests need no mocking.
|
|
545
|
+
*/
|
|
546
|
+
export function headedNoOutputTimeoutHint(signature, context) {
|
|
547
|
+
if (context.platform !== 'darwin' || context.headless)
|
|
548
|
+
return undefined;
|
|
549
|
+
if (!signature.reason.includes('no-output timeout'))
|
|
550
|
+
return undefined;
|
|
551
|
+
return ('Hint: this was a HEADED run on macOS that died at the no-output timeout. ' +
|
|
552
|
+
'A common cause is the display going to sleep mid-run, which freezes the headed browser ' +
|
|
553
|
+
'before any test starts. Wrap headed runs in `caffeinate -dimsu <command>` to keep the ' +
|
|
554
|
+
'display awake, or pass --headless.');
|
|
555
|
+
}
|
|
540
556
|
/**
|
|
541
557
|
* Builds the message for a run that produced no `TEST-START` despite
|
|
542
558
|
* requesting paths — including exit-code-zero runs whose `Passed: 0`
|
|
@@ -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,53 @@ 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;
|
|
111
|
+
/**
|
|
112
|
+
* Formats the post-mutation advisory printed by `fireforge reset` /
|
|
113
|
+
* `fireforge import` when `components.conf` diverged from the last full
|
|
114
|
+
* build (FORGE F13). Same probe as {@link formatStaticComponentsRefusal},
|
|
115
|
+
* different moment: this fires at mutation time so the operator learns a
|
|
116
|
+
* full build is needed BEFORE the next gate run refuses.
|
|
117
|
+
*/
|
|
118
|
+
export declare function formatPostMutationStaticComponentsWarning(changedManifests: string[]): string;
|
|
119
|
+
/**
|
|
120
|
+
* Post-mutation advisory used by `fireforge reset` / `fireforge import`:
|
|
121
|
+
* warns (never throws, never blocks) when `components.conf` now differs
|
|
122
|
+
* from the last full-build anchor. Silently skipped when no baseline or
|
|
123
|
+
* anchor exists, or when any probe fails.
|
|
124
|
+
*/
|
|
125
|
+
export declare function warnIfStaticComponentsStale(projectRoot: string, engineDir: string): Promise<void>;
|
|
@@ -27,10 +27,11 @@ import { createHash } from 'node:crypto';
|
|
|
27
27
|
import { readFile } from 'node:fs/promises';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
import { toError } from '../utils/errors.js';
|
|
30
|
-
import { verbose } from '../utils/logger.js';
|
|
31
|
-
import { isPackageablePath } from './build-audit.js';
|
|
30
|
+
import { verbose, warn } from '../utils/logger.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,89 @@ 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
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Formats the post-mutation advisory printed by `fireforge reset` /
|
|
250
|
+
* `fireforge import` when `components.conf` diverged from the last full
|
|
251
|
+
* build (FORGE F13). Same probe as {@link formatStaticComponentsRefusal},
|
|
252
|
+
* different moment: this fires at mutation time so the operator learns a
|
|
253
|
+
* full build is needed BEFORE the next gate run refuses.
|
|
254
|
+
*/
|
|
255
|
+
export function formatPostMutationStaticComponentsWarning(changedManifests) {
|
|
256
|
+
const head = changedManifests.slice(0, STALE_PATHS_LIMIT);
|
|
257
|
+
const truncated = changedManifests.length - head.length;
|
|
258
|
+
const list = head.join(', ') + (truncated > 0 ? `, … (+${truncated} more)` : '');
|
|
259
|
+
return (`components.conf changed relative to the last full "fireforge build": ${list}.\n` +
|
|
260
|
+
'The compiled StaticComponents table in obj-* no longer matches the tree, so the next ' +
|
|
261
|
+
'"fireforge test" will require a full "fireforge build" first — a scoped ' +
|
|
262
|
+
'"fireforge test --build" cannot regenerate the compiled table.');
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Post-mutation advisory used by `fireforge reset` / `fireforge import`:
|
|
266
|
+
* warns (never throws, never blocks) when `components.conf` now differs
|
|
267
|
+
* from the last full-build anchor. Silently skipped when no baseline or
|
|
268
|
+
* anchor exists, or when any probe fails.
|
|
269
|
+
*/
|
|
270
|
+
export async function warnIfStaticComponentsStale(projectRoot, engineDir) {
|
|
271
|
+
try {
|
|
272
|
+
const baseline = await readBuildBaseline(projectRoot);
|
|
273
|
+
if (baseline === undefined)
|
|
274
|
+
return;
|
|
275
|
+
const result = await checkStaticComponentsStale(engineDir, baseline);
|
|
276
|
+
if (!result.stale)
|
|
277
|
+
return;
|
|
278
|
+
warn(formatPostMutationStaticComponentsWarning(result.changedManifests));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
verbose(`Static-components post-mutation check skipped: ${toError(error).message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -4,14 +4,14 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
|
4
4
|
import { FurnaceError } from '../errors/furnace.js';
|
|
5
5
|
import { toError } from '../utils/errors.js';
|
|
6
6
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
7
|
-
import { warn } from '../utils/logger.js';
|
|
7
|
+
import { info, warn } from '../utils/logger.js';
|
|
8
8
|
import { escapeRegex } from '../utils/regex.js';
|
|
9
9
|
import { validateTokenName } from '../utils/validation.js';
|
|
10
10
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
11
11
|
import { loadFurnaceConfig } from './furnace-config.js';
|
|
12
12
|
import { findDarkMediaCloseIndex, findDarkRootInsertionIndex } from './token-dark-mode.js';
|
|
13
13
|
import { addTokenToDocs } from './token-docs.js';
|
|
14
|
-
import { insertVariantDeclaration, validateVariantSelector, variantBlockHasToken, } from './token-variant.js';
|
|
14
|
+
import { insertVariantDeclaration, validateVariantSelector, variantBlockExists, variantBlockHasToken, } from './token-variant.js';
|
|
15
15
|
/** Returns the token CSS path relative to engine root for a given binary name. */
|
|
16
16
|
export function getTokensCssPath(binaryName) {
|
|
17
17
|
return `browser/themes/shared/${binaryName}-tokens.css`;
|
|
@@ -389,6 +389,39 @@ function findCategorySection(lines, category, tokensCssPath) {
|
|
|
389
389
|
}
|
|
390
390
|
return { categoryLine, sectionEnd };
|
|
391
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Explicit theme-attribute selectors the override path keeps in sync with
|
|
394
|
+
* the `@media (prefers-color-scheme: dark)` block. Consumer scaffolds that
|
|
395
|
+
* pair the media query with `:root[data-theme="dark"]` /
|
|
396
|
+
* `:root[data-theme="light"]` blocks (viewer-toggle theming) require every
|
|
397
|
+
* themed list to declare identical token sets, so an override add that only
|
|
398
|
+
* wrote the media block was a guaranteed half-finished edit (FORGE F8).
|
|
399
|
+
*/
|
|
400
|
+
const THEME_ATTRIBUTE_VARIANTS = [
|
|
401
|
+
{ variant: '[data-theme="dark"]', pick: 'dark' },
|
|
402
|
+
{ variant: '[data-theme="light"]', pick: 'light' },
|
|
403
|
+
];
|
|
404
|
+
/**
|
|
405
|
+
* Mirrors an override's light/dark values into existing top-level
|
|
406
|
+
* `:root[data-theme="dark"]` / `:root[data-theme="light"]` blocks. Blocks
|
|
407
|
+
* that do not exist are left alone (scaffolded files have none; behavior is
|
|
408
|
+
* unchanged there). Idempotent per block. Returns the selectors written.
|
|
409
|
+
*/
|
|
410
|
+
function insertThemeAttributeOverrides(lines, options) {
|
|
411
|
+
if (options.mode !== 'override' || !options.darkValue)
|
|
412
|
+
return [];
|
|
413
|
+
const written = [];
|
|
414
|
+
for (const { variant, pick } of THEME_ATTRIBUTE_VARIANTS) {
|
|
415
|
+
if (!variantBlockExists(lines, variant))
|
|
416
|
+
continue;
|
|
417
|
+
if (variantBlockHasToken(lines, variant, options.tokenName))
|
|
418
|
+
continue;
|
|
419
|
+
const value = pick === 'dark' ? options.darkValue : options.value;
|
|
420
|
+
insertVariantDeclaration(lines, variant, ` ${options.tokenName}: ${value};`);
|
|
421
|
+
written.push(`:root${variant}`);
|
|
422
|
+
}
|
|
423
|
+
return written;
|
|
424
|
+
}
|
|
392
425
|
function insertDarkModeOverride(lines, options) {
|
|
393
426
|
if (options.mode !== 'override' || !options.darkValue)
|
|
394
427
|
return;
|
|
@@ -451,6 +484,10 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
|
|
|
451
484
|
}
|
|
452
485
|
lines.splice(insertIndex, 0, ...insertLines);
|
|
453
486
|
insertDarkModeOverride(lines, options);
|
|
487
|
+
const themeBlocksWritten = insertThemeAttributeOverrides(lines, options);
|
|
488
|
+
if (themeBlocksWritten.length > 0) {
|
|
489
|
+
info(`Override also written to ${themeBlocksWritten.join(' and ')}.`);
|
|
490
|
+
}
|
|
454
491
|
content = lines.join('\n');
|
|
455
492
|
await writeText(filePath, content);
|
|
456
493
|
return { added: true, categoryCreated };
|
|
@@ -29,6 +29,13 @@ export type VariantValidation = {
|
|
|
29
29
|
* @param raw - Raw `--variant` value from the CLI / programmatic caller.
|
|
30
30
|
*/
|
|
31
31
|
export declare function validateVariantSelector(raw: unknown): VariantValidation;
|
|
32
|
+
/**
|
|
33
|
+
* True when a top-level `:root<variant>` block exists in `lines`. Used by
|
|
34
|
+
* the override path to write theme-attribute blocks only when the scaffold
|
|
35
|
+
* already declares them — `insertVariantDeclaration` would otherwise create
|
|
36
|
+
* the block, which is only wanted for the explicit `--variant` flow.
|
|
37
|
+
*/
|
|
38
|
+
export declare function variantBlockExists(lines: string[], variant: string): boolean;
|
|
32
39
|
/** True when the `:root<variant>` block already declares `tokenName`. */
|
|
33
40
|
export declare function variantBlockHasToken(lines: string[], variant: string, tokenName: string): boolean;
|
|
34
41
|
/**
|