@hominis/fireforge 0.44.2 → 0.44.4
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/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +9 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +9 -0
- package/dist/src/commands/furnace/create-browser-test.js +6 -6
- package/dist/src/commands/furnace/create-dry-run.d.ts +10 -0
- package/dist/src/commands/furnace/create-dry-run.js +16 -11
- package/dist/src/commands/furnace/create-xpcshell.js +2 -2
- package/dist/src/commands/furnace/create.js +5 -0
- package/dist/src/commands/test-build-phase.js +1 -1
- package/dist/src/commands/token.js +3 -1
- package/dist/src/core/build-audit.d.ts +20 -0
- package/dist/src/core/build-audit.js +25 -1
- package/dist/src/core/build-baseline-types.d.ts +28 -0
- package/dist/src/core/build-baseline.d.ts +16 -1
- package/dist/src/core/build-baseline.js +38 -2
- package/dist/src/core/build-prepare.js +32 -3
- package/dist/src/core/engine-changes.d.ts +18 -0
- package/dist/src/core/engine-changes.js +34 -0
- package/dist/src/core/furnace-constants.d.ts +21 -0
- package/dist/src/core/furnace-constants.js +26 -1
- package/dist/src/core/license-headers.d.ts +9 -5
- package/dist/src/core/license-headers.js +33 -7
- package/dist/src/core/test-stale-check.js +2 -13
- package/dist/src/core/token-variant.d.ts +12 -2
- package/dist/src/core/token-variant.js +106 -17
- package/docs/README.md +35 -0
- package/docs/configuration.md +116 -0
- package/docs/exit-codes.md +63 -0
- package/docs/furnace.md +57 -0
- package/docs/lifecycle-invariants.md +309 -0
- package/docs/machine-output.md +94 -0
- package/docs/patch-workflow.md +119 -0
- package/docs/run-logs.md +60 -0
- package/docs/testing.md +123 -0
- package/docs/verification-trees.md +48 -0
- package/package.json +2 -1
|
@@ -20,3 +20,21 @@ import type { BuildBaseline } from './build-baseline-types.js';
|
|
|
20
20
|
* @returns Sorted engine-relative POSIX paths
|
|
21
21
|
*/
|
|
22
22
|
export declare function collectChangedEnginePaths(engineDir: string, baseline: BuildBaseline | undefined, contextLabel: string): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Drops every path whose live content still matches the fingerprint the
|
|
25
|
+
* last successful build recorded for it, keeping the rest. Shared by the
|
|
26
|
+
* stale-build preflight (`packageableFingerprints`) and the auto-configure /
|
|
27
|
+
* full-build escalation in `build-prepare` (`buildInputFingerprints`).
|
|
28
|
+
*
|
|
29
|
+
* A path with no recorded entry, an unreadable file, or a differing hash
|
|
30
|
+
* is kept — "cannot prove unchanged" must never turn into "unchanged".
|
|
31
|
+
* With no fingerprint set at all (a baseline written before the field
|
|
32
|
+
* existed) every path is kept, which is the path-only comparison the
|
|
33
|
+
* fingerprints refine.
|
|
34
|
+
*
|
|
35
|
+
* @param engineDir - Path to the engine directory
|
|
36
|
+
* @param paths - Candidate engine-relative POSIX paths
|
|
37
|
+
* @param fingerprints - Recorded hashes keyed by engine-relative path
|
|
38
|
+
* @returns The subset of `paths` that cannot be proven unchanged
|
|
39
|
+
*/
|
|
40
|
+
export declare function dropPathsMatchingFingerprints(engineDir: string, paths: readonly string[], fingerprints: Record<string, string> | undefined): Promise<string[]>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { toError } from '../utils/errors.js';
|
|
8
8
|
import { verbose } from '../utils/logger.js';
|
|
9
|
+
import { hashEngineFile } from './coverage-extend.js';
|
|
9
10
|
import { hasChanges, isMissingHeadError } from './git.js';
|
|
10
11
|
import { git } from './git-base.js';
|
|
11
12
|
import { getUntrackedFiles } from './git-status.js';
|
|
@@ -59,3 +60,36 @@ export async function collectChangedEnginePaths(engineDir, baseline, contextLabe
|
|
|
59
60
|
}
|
|
60
61
|
return [...collected].sort();
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Drops every path whose live content still matches the fingerprint the
|
|
65
|
+
* last successful build recorded for it, keeping the rest. Shared by the
|
|
66
|
+
* stale-build preflight (`packageableFingerprints`) and the auto-configure /
|
|
67
|
+
* full-build escalation in `build-prepare` (`buildInputFingerprints`).
|
|
68
|
+
*
|
|
69
|
+
* A path with no recorded entry, an unreadable file, or a differing hash
|
|
70
|
+
* is kept — "cannot prove unchanged" must never turn into "unchanged".
|
|
71
|
+
* With no fingerprint set at all (a baseline written before the field
|
|
72
|
+
* existed) every path is kept, which is the path-only comparison the
|
|
73
|
+
* fingerprints refine.
|
|
74
|
+
*
|
|
75
|
+
* @param engineDir - Path to the engine directory
|
|
76
|
+
* @param paths - Candidate engine-relative POSIX paths
|
|
77
|
+
* @param fingerprints - Recorded hashes keyed by engine-relative path
|
|
78
|
+
* @returns The subset of `paths` that cannot be proven unchanged
|
|
79
|
+
*/
|
|
80
|
+
export async function dropPathsMatchingFingerprints(engineDir, paths, fingerprints) {
|
|
81
|
+
if (!fingerprints)
|
|
82
|
+
return [...paths];
|
|
83
|
+
const kept = [];
|
|
84
|
+
for (const path of paths) {
|
|
85
|
+
const recorded = fingerprints[path];
|
|
86
|
+
if (recorded === undefined) {
|
|
87
|
+
kept.push(path);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const live = await hashEngineFile(engineDir, path);
|
|
91
|
+
if (live === undefined || live !== recorded)
|
|
92
|
+
kept.push(path);
|
|
93
|
+
}
|
|
94
|
+
return kept;
|
|
95
|
+
}
|
|
@@ -12,12 +12,33 @@ export declare const JAR_MN = "toolkit/content/jar.mn";
|
|
|
12
12
|
export declare const WIDGETS_DIR = "toolkit/content/widgets";
|
|
13
13
|
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
14
14
|
export declare const FTL_DIR = "toolkit/locales/en-US/toolkit/global";
|
|
15
|
+
/**
|
|
16
|
+
* Engine-relative root every browser-chrome / xpcshell test scaffold lives
|
|
17
|
+
* under, trailing slash included. A `--test-dir` override must stay below
|
|
18
|
+
* it so the `browser/base/moz.build` manifest registration keeps working.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BROWSER_TEST_SCAFFOLD_ROOT = "browser/base/content/test/";
|
|
15
21
|
/**
|
|
16
22
|
* Returns the engine-relative directory that holds xpcshell scaffolds for
|
|
17
23
|
* a given binary. Matches the form `create-xpcshell.ts` writes and the
|
|
18
24
|
* path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
|
|
19
25
|
*/
|
|
20
26
|
export declare function xpcshellTestParentDir(binaryName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the engine-relative directory a browser-chrome scaffold is
|
|
29
|
+
* written to: the `--test-dir` override when given, else
|
|
30
|
+
* `browser/base/content/test/<binaryName>`. The scaffolder AND the
|
|
31
|
+
* dry-run / success formatters resolve through this one function, so the
|
|
32
|
+
* printed plan cannot disagree with the files that land on disk.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveBrowserChromeTestDir(binaryName: string, override?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolves the engine-relative directory an xpcshell scaffold is written
|
|
37
|
+
* to. A `--test-dir` override names the FINAL directory (no per-component
|
|
38
|
+
* segment is appended); the default is
|
|
39
|
+
* `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveXpcshellTestDir(binaryName: string, componentName: string, override?: string): string;
|
|
21
42
|
/** Returns true when `fileName` has one of the standard component file extensions. */
|
|
22
43
|
export declare function isComponentSourceFile(fileName: string): boolean;
|
|
23
44
|
/**
|
|
@@ -13,6 +13,12 @@ export const JAR_MN = 'toolkit/content/jar.mn';
|
|
|
13
13
|
export const WIDGETS_DIR = 'toolkit/content/widgets';
|
|
14
14
|
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
15
15
|
export const FTL_DIR = 'toolkit/locales/en-US/toolkit/global';
|
|
16
|
+
/**
|
|
17
|
+
* Engine-relative root every browser-chrome / xpcshell test scaffold lives
|
|
18
|
+
* under, trailing slash included. A `--test-dir` override must stay below
|
|
19
|
+
* it so the `browser/base/moz.build` manifest registration keeps working.
|
|
20
|
+
*/
|
|
21
|
+
export const BROWSER_TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
|
|
16
22
|
/**
|
|
17
23
|
* Suffix for the per-binary xpcshell scaffold parent directory. Components
|
|
18
24
|
* created with `furnace create --with-tests --xpcshell` land at
|
|
@@ -27,7 +33,26 @@ const XPCSHELL_TEST_DIR_SUFFIX = '-xpcshell';
|
|
|
27
33
|
* path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
|
|
28
34
|
*/
|
|
29
35
|
export function xpcshellTestParentDir(binaryName) {
|
|
30
|
-
return
|
|
36
|
+
return `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}${XPCSHELL_TEST_DIR_SUFFIX}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the engine-relative directory a browser-chrome scaffold is
|
|
40
|
+
* written to: the `--test-dir` override when given, else
|
|
41
|
+
* `browser/base/content/test/<binaryName>`. The scaffolder AND the
|
|
42
|
+
* dry-run / success formatters resolve through this one function, so the
|
|
43
|
+
* printed plan cannot disagree with the files that land on disk.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveBrowserChromeTestDir(binaryName, override) {
|
|
46
|
+
return override ?? `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the engine-relative directory an xpcshell scaffold is written
|
|
50
|
+
* to. A `--test-dir` override names the FINAL directory (no per-component
|
|
51
|
+
* segment is appended); the default is
|
|
52
|
+
* `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveXpcshellTestDir(binaryName, componentName, override) {
|
|
55
|
+
return override ?? `${xpcshellTestParentDir(binaryName)}/${componentName}`;
|
|
31
56
|
}
|
|
32
57
|
/** File extensions that constitute a Furnace component's source files. */
|
|
33
58
|
const COMPONENT_FILE_EXTENSIONS = ['.mjs', '.css', '.ftl'];
|
|
@@ -69,7 +69,9 @@ export declare function hasUpstreamMplBlockHeader(content: string): boolean;
|
|
|
69
69
|
export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
70
70
|
/**
|
|
71
71
|
* Returns true if the first few lines of `content` contain a recognized
|
|
72
|
-
* upstream license identifier string
|
|
72
|
+
* upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
|
|
73
|
+
* tag, the common permissive/copyleft license names, or the CC0
|
|
74
|
+
* public-domain dedication upstream test files carry.
|
|
73
75
|
*
|
|
74
76
|
* @param content - File content to check
|
|
75
77
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
@@ -77,10 +79,12 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
|
77
79
|
export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
|
|
78
80
|
/**
|
|
79
81
|
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
80
|
-
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0)
|
|
81
|
-
* comment style. Used by `export` to
|
|
82
|
-
* to prepend the project's own
|
|
83
|
-
* upstream bundle
|
|
82
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
|
|
83
|
+
* CC0 public-domain dedication, in any comment style. Used by `export` to
|
|
84
|
+
* treat such files as vendored: offering to prepend the project's own
|
|
85
|
+
* license header onto a byte-identity-pinned upstream bundle — or onto a
|
|
86
|
+
* test file Mozilla dedicated to the public domain — would mislicense code
|
|
87
|
+
* the project does not own.
|
|
84
88
|
*
|
|
85
89
|
* @param content - File content to check
|
|
86
90
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -227,9 +227,31 @@ function normalizeLicenseHeadForScan(head) {
|
|
|
227
227
|
s = s.replace(/\n[ \t]*\*[ \t]*/g, ' ');
|
|
228
228
|
return s.replace(/\s+/g, ' ').trim();
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Recognizes the CC0 public-domain dedication Mozilla puts on most of its
|
|
232
|
+
* test files, in every spelling the tree carries:
|
|
233
|
+
*
|
|
234
|
+
* - the prose (`Any copyright is dedicated to the Public Domain.`), which
|
|
235
|
+
* the block, `//` and `#` comment forms all reduce to after
|
|
236
|
+
* {@link normalizeLicenseHeadForScan};
|
|
237
|
+
* - the dedication URL (`creativecommons.org/publicdomain/zero/1.0/`, with
|
|
238
|
+
* either scheme);
|
|
239
|
+
* - the SPDX identifier `CC0-1.0`.
|
|
240
|
+
*
|
|
241
|
+
* Deliberately narrow: a bare "public domain" mention, a different Creative
|
|
242
|
+
* Commons license (`licenses/by/4.0/`, `CC-BY-4.0`) or a made-up
|
|
243
|
+
* `publicdomain/zero/2.0/` is NOT a CC0 dedication and must not be
|
|
244
|
+
* accepted as one. `scanText` must already be normalized (and may be
|
|
245
|
+
* lowercased — the match is case-insensitive).
|
|
246
|
+
*/
|
|
247
|
+
function hasCc0Dedication(scanText) {
|
|
248
|
+
return /dedicated to the public domain|creativecommons\.org\/publicdomain\/zero\/1\.0|spdx-license-identifier:\s*cc0-1\.0\b/i.test(scanText);
|
|
249
|
+
}
|
|
230
250
|
/**
|
|
231
251
|
* Returns true if the first few lines of `content` contain a recognized
|
|
232
|
-
* upstream license identifier string
|
|
252
|
+
* upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
|
|
253
|
+
* tag, the common permissive/copyleft license names, or the CC0
|
|
254
|
+
* public-domain dedication upstream test files carry.
|
|
233
255
|
*
|
|
234
256
|
* @param content - File content to check
|
|
235
257
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
@@ -244,14 +266,16 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
244
266
|
'MIT License',
|
|
245
267
|
'GNU General Public License',
|
|
246
268
|
];
|
|
247
|
-
return markers.some((marker) => scanText.includes(marker));
|
|
269
|
+
return markers.some((marker) => scanText.includes(marker)) || hasCc0Dedication(scanText);
|
|
248
270
|
}
|
|
249
271
|
/**
|
|
250
272
|
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
251
|
-
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0)
|
|
252
|
-
* comment style. Used by `export` to
|
|
253
|
-
* to prepend the project's own
|
|
254
|
-
* upstream bundle
|
|
273
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
|
|
274
|
+
* CC0 public-domain dedication, in any comment style. Used by `export` to
|
|
275
|
+
* treat such files as vendored: offering to prepend the project's own
|
|
276
|
+
* license header onto a byte-identity-pinned upstream bundle — or onto a
|
|
277
|
+
* test file Mozilla dedicated to the public domain — would mislicense code
|
|
278
|
+
* the project does not own.
|
|
255
279
|
*
|
|
256
280
|
* @param content - File content to check
|
|
257
281
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -271,7 +295,9 @@ export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
|
|
|
271
295
|
];
|
|
272
296
|
if (markers.some((marker) => scanText.includes(marker)))
|
|
273
297
|
return true;
|
|
274
|
-
|
|
298
|
+
if (hasCc0Dedication(scanText))
|
|
299
|
+
return true;
|
|
300
|
+
return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0|cc0-1\.0)\b/.test(scanText);
|
|
275
301
|
}
|
|
276
302
|
/**
|
|
277
303
|
* Prepends the license header to a file on disk if it is not already present.
|
|
@@ -27,7 +27,7 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
27
27
|
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
28
28
|
import { readBuildBaseline } from './build-baseline.js';
|
|
29
29
|
import { hashEngineFile } from './coverage-extend.js';
|
|
30
|
-
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
30
|
+
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
31
31
|
export { isXpcomManifestPath };
|
|
32
32
|
/** Cap on the number of changed paths rendered inline. */
|
|
33
33
|
const STALE_PATHS_LIMIT = 10;
|
|
@@ -60,18 +60,7 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
|
60
60
|
// and the stale check fires immediately after a successful one. The
|
|
61
61
|
// fingerprints capture "these files had this content when the build ran";
|
|
62
62
|
// a path stays stale only when its live hash diverges.
|
|
63
|
-
|
|
64
|
-
if (fingerprints) {
|
|
65
|
-
const staleAfterHashCheck = [];
|
|
66
|
-
for (const path of packageable) {
|
|
67
|
-
const recorded = fingerprints[path];
|
|
68
|
-
const live = await hashEngineFile(engineDir, path);
|
|
69
|
-
if (recorded === undefined || live === undefined || recorded !== live) {
|
|
70
|
-
staleAfterHashCheck.push(path);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
packageable = staleAfterHashCheck;
|
|
74
|
-
}
|
|
63
|
+
packageable = await dropPathsMatchingFingerprints(engineDir, packageable, baseline.packageableFingerprints);
|
|
75
64
|
if (packageable.length === 0) {
|
|
76
65
|
return { stale: false, changedPaths: [], truncated: 0, baseline };
|
|
77
66
|
}
|
|
@@ -23,10 +23,20 @@ export type VariantValidation = {
|
|
|
23
23
|
reason: string;
|
|
24
24
|
};
|
|
25
25
|
/**
|
|
26
|
-
* Validates a `--variant`
|
|
27
|
-
*
|
|
26
|
+
* Validates a `--variant` selector fragment and normalizes any `=value`
|
|
27
|
+
* form to the double-quoted `="value"` shape (Mozilla convention).
|
|
28
28
|
* Boolean-attribute fragments are returned unchanged.
|
|
29
29
|
*
|
|
30
|
+
* Structure is decided by {@link rootAttributeSelector} — the matcher's own
|
|
31
|
+
* parse — rather than by a second regex. That is the point: 0.44.2 widened
|
|
32
|
+
* the matcher to consume consecutive `[…]` groups and stop at a pseudo-class
|
|
33
|
+
* tail, and the validator did not move with it, so
|
|
34
|
+
* `:root[data-skin="humanist"]:not([data-private])` was a block the tool
|
|
35
|
+
* could FIND but would not let you NAME. Running both halves off one parse
|
|
36
|
+
* is what stops them drifting apart again. Character-level safety stays a
|
|
37
|
+
* separate, per-group check, because the matcher finds bracket boundaries
|
|
38
|
+
* and validates no characters at all.
|
|
39
|
+
*
|
|
30
40
|
* @param raw - Raw `--variant` value from the CLI / programmatic caller.
|
|
31
41
|
*/
|
|
32
42
|
export declare function validateVariantSelector(raw: unknown): VariantValidation;
|
|
@@ -12,36 +12,113 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { findBlockCloseIndex, stripBlockCommentsInLines } from './token-dark-mode.js';
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* ONE attribute selector fragment: `[data-private]` (boolean attribute) or
|
|
16
|
+
* `[data-skin=precision]` / `[data-skin="precision"]` (attribute with
|
|
17
|
+
* value). The name and value halves are restricted to identifier-safe
|
|
18
18
|
* characters so the fragment can be spliced into CSS verbatim without
|
|
19
19
|
* escaping concerns.
|
|
20
|
+
*
|
|
21
|
+
* A multi-fragment variant is checked one group at a time (see
|
|
22
|
+
* {@link splitAttributeGroups}) rather than by wrapping this in a `+`: the
|
|
23
|
+
* repeated form is the shape CodeQL flags as `js/polynomial-redos`, and the
|
|
24
|
+
* split the matcher already performs is available for free.
|
|
25
|
+
*/
|
|
26
|
+
const ATTRIBUTE_FRAGMENT_PATTERN = /^\[[a-zA-Z][a-zA-Z0-9_-]*(?:=(?:"[a-zA-Z0-9_-]+"|'[a-zA-Z0-9_-]+'|[a-zA-Z0-9_-]+))?\]$/;
|
|
27
|
+
/**
|
|
28
|
+
* A pseudo-class tail: `:not(…)`, `:hover`, and friends. The parenthesised
|
|
29
|
+
* argument is captured rather than validated here — it is a fragment run of
|
|
30
|
+
* its own and goes through the same per-group check.
|
|
20
31
|
*/
|
|
21
|
-
const
|
|
32
|
+
const QUALIFIER_PATTERN = /^:[a-zA-Z][a-zA-Z0-9-]*(?:\(([^()]*)\))?$/;
|
|
22
33
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
34
|
+
* Splits a run of consecutive attribute fragments into its groups, or
|
|
35
|
+
* returns `null` when the run is not exactly that — a group that does not
|
|
36
|
+
* open with `[`, an unclosed group, an empty run, or a group whose name or
|
|
37
|
+
* value is not identifier-safe.
|
|
38
|
+
*
|
|
39
|
+
* @param run - The `[…][…]` text with no `:root` prefix
|
|
40
|
+
* @returns The individual fragments, or null when the run is not valid
|
|
41
|
+
*/
|
|
42
|
+
function splitAttributeGroups(run) {
|
|
43
|
+
const groups = [];
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
while (cursor < run.length) {
|
|
46
|
+
if (run[cursor] !== '[')
|
|
47
|
+
return null;
|
|
48
|
+
const close = run.indexOf(']', cursor);
|
|
49
|
+
if (close === -1)
|
|
50
|
+
return null;
|
|
51
|
+
const group = run.slice(cursor, close + 1);
|
|
52
|
+
if (!ATTRIBUTE_FRAGMENT_PATTERN.test(group))
|
|
53
|
+
return null;
|
|
54
|
+
groups.push(group);
|
|
55
|
+
cursor = close + 1;
|
|
56
|
+
}
|
|
57
|
+
return groups.length > 0 ? groups : null;
|
|
58
|
+
}
|
|
59
|
+
/** Rewrites `=value` / `='value'` to the double-quoted Mozilla convention. */
|
|
60
|
+
function normalizeAttributeFragment(group) {
|
|
61
|
+
return group
|
|
62
|
+
.replace(/='([a-zA-Z0-9_-]+)'\]$/, '="$1"]')
|
|
63
|
+
.replace(/=([a-zA-Z0-9_-]+)\]$/, '="$1"]');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validates a `--variant` selector fragment and normalizes any `=value`
|
|
67
|
+
* form to the double-quoted `="value"` shape (Mozilla convention).
|
|
25
68
|
* Boolean-attribute fragments are returned unchanged.
|
|
26
69
|
*
|
|
70
|
+
* Structure is decided by {@link rootAttributeSelector} — the matcher's own
|
|
71
|
+
* parse — rather than by a second regex. That is the point: 0.44.2 widened
|
|
72
|
+
* the matcher to consume consecutive `[…]` groups and stop at a pseudo-class
|
|
73
|
+
* tail, and the validator did not move with it, so
|
|
74
|
+
* `:root[data-skin="humanist"]:not([data-private])` was a block the tool
|
|
75
|
+
* could FIND but would not let you NAME. Running both halves off one parse
|
|
76
|
+
* is what stops them drifting apart again. Character-level safety stays a
|
|
77
|
+
* separate, per-group check, because the matcher finds bracket boundaries
|
|
78
|
+
* and validates no characters at all.
|
|
79
|
+
*
|
|
27
80
|
* @param raw - Raw `--variant` value from the CLI / programmatic caller.
|
|
28
81
|
*/
|
|
29
82
|
export function validateVariantSelector(raw) {
|
|
30
83
|
if (typeof raw !== 'string') {
|
|
31
84
|
return { ok: false, reason: 'must be a string when set' };
|
|
32
85
|
}
|
|
86
|
+
const reject = {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: 'must be one or more attribute selectors like [data-private], [data-skin=precision] ' +
|
|
89
|
+
'or [data-skin=precision][data-theme=dark], optionally followed by a pseudo-class ' +
|
|
90
|
+
'such as :not([data-private]) (identifier-safe attribute names and values only)',
|
|
91
|
+
};
|
|
33
92
|
const value = raw.trim();
|
|
34
|
-
if (
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
93
|
+
if (value === '')
|
|
94
|
+
return reject;
|
|
95
|
+
const parts = rootAttributeSelector(`:root${value} {`);
|
|
96
|
+
// The parse must have consumed the WHOLE fragment: anything it declined to
|
|
97
|
+
// read (a stray space, a trailing combinator) is text the matcher would
|
|
98
|
+
// not see either, so accepting it would author a block nothing can find.
|
|
99
|
+
if (parts === null || `${parts.attributes}${parts.qualifier}` !== `:root${value}`)
|
|
100
|
+
return reject;
|
|
101
|
+
const groups = splitAttributeGroups(parts.attributes.slice(':root'.length));
|
|
102
|
+
if (groups === null)
|
|
103
|
+
return reject;
|
|
104
|
+
let qualifier = '';
|
|
105
|
+
if (parts.qualifier !== '') {
|
|
106
|
+
const match = QUALIFIER_PATTERN.exec(parts.qualifier);
|
|
107
|
+
if (match === null)
|
|
108
|
+
return reject;
|
|
109
|
+
const inner = match[1];
|
|
110
|
+
if (inner === undefined) {
|
|
111
|
+
qualifier = parts.qualifier;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const innerGroups = splitAttributeGroups(inner);
|
|
115
|
+
if (innerGroups === null)
|
|
116
|
+
return reject;
|
|
117
|
+
const name = parts.qualifier.slice(0, parts.qualifier.indexOf('('));
|
|
118
|
+
qualifier = `${name}(${innerGroups.map(normalizeAttributeFragment).join('')})`;
|
|
119
|
+
}
|
|
40
120
|
}
|
|
41
|
-
|
|
42
|
-
.replace(/='([a-zA-Z0-9_-]+)'\]$/, '="$1"]')
|
|
43
|
-
.replace(/=([a-zA-Z0-9_-]+)\]$/, '="$1"]');
|
|
44
|
-
return { ok: true, value: normalized };
|
|
121
|
+
return { ok: true, value: `${groups.map(normalizeAttributeFragment).join('')}${qualifier}` };
|
|
45
122
|
}
|
|
46
123
|
/**
|
|
47
124
|
* Reduces a `:root<attr>` selector to a quote- and whitespace-insensitive
|
|
@@ -105,12 +182,24 @@ function rootAttributeSelector(line) {
|
|
|
105
182
|
*/
|
|
106
183
|
function findVariantBlock(lines, variant) {
|
|
107
184
|
const stripped = stripBlockCommentsInLines(lines);
|
|
108
|
-
|
|
185
|
+
// The request goes through the same parse as the candidate lines, so a
|
|
186
|
+
// variant carrying its own `:not(…)` tail is compared tail-to-tail rather
|
|
187
|
+
// than being folded into the attribute run — where it would match nothing
|
|
188
|
+
// and every add would splice a fresh duplicate block.
|
|
189
|
+
const requested = rootAttributeSelector(`:root${variant} {`);
|
|
190
|
+
const want = canonicalSelector(requested?.attributes ?? `:root${variant}`);
|
|
191
|
+
const wantQualifier = canonicalSelector(requested?.qualifier ?? '');
|
|
109
192
|
for (let i = 0; i < stripped.length; i++) {
|
|
110
193
|
const line = stripped[i] ?? '';
|
|
111
194
|
const selector = rootAttributeSelector(line);
|
|
112
195
|
if (selector === null || canonicalSelector(selector.attributes) !== want)
|
|
113
196
|
continue;
|
|
197
|
+
// An UNqualified request still matches a qualified block: that is the
|
|
198
|
+
// 0.44.2 behaviour `variantBlockQualifier` exists to report, since
|
|
199
|
+
// silently skipping the block is the worse failure. A request that names
|
|
200
|
+
// a qualifier, though, means it — so it must match.
|
|
201
|
+
if (wantQualifier !== '' && canonicalSelector(selector.qualifier) !== wantQualifier)
|
|
202
|
+
continue;
|
|
114
203
|
let openLine = /\{/.test(line) ? i : -1;
|
|
115
204
|
if (openLine === -1) {
|
|
116
205
|
for (let j = i + 1; j < stripped.length; j++) {
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# FireForge documentation
|
|
4
|
+
|
|
5
|
+
[`../README.md`](../README.md) is the getting-started page. These files are
|
|
6
|
+
the reference material behind it. `fireforge --help` (and
|
|
7
|
+
`fireforge <command> --help`) stays the authority on flags; the docs cover
|
|
8
|
+
what a flag list cannot state.
|
|
9
|
+
|
|
10
|
+
## Using FireForge
|
|
11
|
+
|
|
12
|
+
| File | Covers |
|
|
13
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
14
|
+
| [patch-workflow.md](patch-workflow.md) | Export, re-export, deletions, queue maintenance, per-patch lint, CI gating, rebasing |
|
|
15
|
+
| [testing.md](testing.md) | `fireforge test`: scope, modes, build freshness, the verdict line, verbosity |
|
|
16
|
+
| [furnace.md](furnace.md) | Components: overrides, shared CSS fragments, typed cross-module imports |
|
|
17
|
+
| [verification-trees.md](verification-trees.md) | CoW snapshots for concurrent read-mostly verification |
|
|
18
|
+
| [configuration.md](configuration.md) | `fireforge.json` settings, design tokens, lock waits, environment variables |
|
|
19
|
+
|
|
20
|
+
## Contracts for scripts and CI
|
|
21
|
+
|
|
22
|
+
| File | Covers |
|
|
23
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
24
|
+
| [exit-codes.md](exit-codes.md) | Every exit code, which error class produces it, and how CI should branch |
|
|
25
|
+
| [machine-output.md](machine-output.md) | The `--json` / `--raw` / verdict-line output contract and its versioning |
|
|
26
|
+
| [run-logs.md](run-logs.md) | `.fireforge/logs/`: what `test` and `build` write, and what survives a pipe |
|
|
27
|
+
|
|
28
|
+
## Internals
|
|
29
|
+
|
|
30
|
+
| File | Covers |
|
|
31
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
32
|
+
| [lifecycle-invariants.md](lifecycle-invariants.md) | Locking, rollback, signal handling, and the decision table for new mutations |
|
|
33
|
+
|
|
34
|
+
Release history and the reasoning behind individual changes live in
|
|
35
|
+
[`../CHANGELOG.md`](../CHANGELOG.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# Configuration
|
|
4
|
+
|
|
5
|
+
`fireforge.json` at the project root is the project's configuration;
|
|
6
|
+
`furnace.json` holds component configuration. This file covers the settings
|
|
7
|
+
whose behaviour needs more than a schema line, plus the environment variables
|
|
8
|
+
FireForge reads.
|
|
9
|
+
|
|
10
|
+
## External toolchains
|
|
11
|
+
|
|
12
|
+
Projects with generated asset prerequisites can declare opt-in
|
|
13
|
+
`externalToolchains`. `fireforge doctor` reports missing **required** tools as
|
|
14
|
+
errors and missing **optional** tools as warnings.
|
|
15
|
+
|
|
16
|
+
Seasonal branding on macOS, for example, can declare Apple's Icon Composer
|
|
17
|
+
tool at
|
|
18
|
+
`/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
|
|
19
|
+
plus `actool` via `xcrun`, `sips`, and `iconutil`.
|
|
20
|
+
|
|
21
|
+
## `patchLint`
|
|
22
|
+
|
|
23
|
+
Per-patch lint type-checks plain Firefox JS against a built-in Firefox-globals
|
|
24
|
+
shim that tracks upstream WebIDL additions per release.
|
|
25
|
+
|
|
26
|
+
- `patchLint.checkJsExtraShim` adds members to the structured shim globals via
|
|
27
|
+
TypeScript interface merging, e.g.
|
|
28
|
+
`interface ChromeUtilsShim { newApi(): any }`.
|
|
29
|
+
- `patchLint.checkJsTestFiles: true` extends the pass to patch-owned test
|
|
30
|
+
`.js` files (`browser_*` / `test_*` / `xpcshell_*` and `/test/` paths). Each
|
|
31
|
+
is checked as its own small script-scope program against a loose built-in
|
|
32
|
+
harness shim.
|
|
33
|
+
- `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
|
|
34
|
+
declarations (e.g. a real `TestUtils` interface) override the loose
|
|
35
|
+
baseline, so calls to nonexistent harness members fail at export time.
|
|
36
|
+
|
|
37
|
+
## `patchPolicy`
|
|
38
|
+
|
|
39
|
+
`patchPolicy.ranges` makes `patch compact` range-aware, so closing ordinal
|
|
40
|
+
gaps does not move a patch out of the band its category owns.
|
|
41
|
+
|
|
42
|
+
## `test`
|
|
43
|
+
|
|
44
|
+
`test.canaryPath` and `test.canaryTimeoutSeconds` supply the defaults for
|
|
45
|
+
`fireforge test --canary`. See [`testing.md`](testing.md).
|
|
46
|
+
|
|
47
|
+
## Design tokens
|
|
48
|
+
|
|
49
|
+
Tokens are managed with `fireforge token add`.
|
|
50
|
+
|
|
51
|
+
- `--create-category` declares a new category banner and inserts the token in
|
|
52
|
+
one step. It is incompatible with `--variant`; author the category with the
|
|
53
|
+
base token first, then add each variant.
|
|
54
|
+
- `--category` is required for a base declaration and optional under
|
|
55
|
+
`--variant`, where the declaration is routed into a `:root<selector>` block
|
|
56
|
+
and never into a category section. It is still validated when supplied.
|
|
57
|
+
- `--variant` takes the same selector grammar the block matcher understands:
|
|
58
|
+
one attribute fragment (`[data-private]`, `[data-skin=precision]`), a run of
|
|
59
|
+
them (`[data-skin=precision][data-theme=dark]`), and an optional pseudo-class
|
|
60
|
+
tail (`[data-skin=precision]:not([data-private])`). Names and values must be
|
|
61
|
+
identifier-safe, and `=value` is normalized to `="value"`. A variant naming a
|
|
62
|
+
qualifier matches only a block carrying that qualifier; one without a
|
|
63
|
+
qualifier still matches a qualified block, and the write says so.
|
|
64
|
+
- `--mode override` mirrors into both `:root[data-theme="dark"]` and
|
|
65
|
+
`:root[data-theme="light"]`. A qualified block such as
|
|
66
|
+
`:root[data-theme="light"]:not([data-private])` is matched and written
|
|
67
|
+
through, with a warning naming the selector — the qualifier is
|
|
68
|
+
semantically load-bearing, but silently skipping half the mirror is the
|
|
69
|
+
worse failure.
|
|
70
|
+
- A token already present in the target block is reported with its location
|
|
71
|
+
(`already exists in :root[…] (line N), unchanged`) rather than a bare
|
|
72
|
+
no-op.
|
|
73
|
+
|
|
74
|
+
## Smoke runs
|
|
75
|
+
|
|
76
|
+
For unattended smoke checks, `fireforge run --smoke-exit <s> --headless`
|
|
77
|
+
launches the browser headless. A headed smoke window on a shared desktop
|
|
78
|
+
absorbs live input, which contaminates the console capture — headed non-CI
|
|
79
|
+
launches print a warning saying so. Exit codes 12 and 13 separate a console
|
|
80
|
+
regression from a launch failure; see [`exit-codes.md`](exit-codes.md).
|
|
81
|
+
|
|
82
|
+
## Lock waits
|
|
83
|
+
|
|
84
|
+
Every FireForge command that mutates the engine takes the engine-session lock,
|
|
85
|
+
and some take a second subsystem lock as well. Contention fails fast after
|
|
86
|
+
about a second by default.
|
|
87
|
+
|
|
88
|
+
`--wait-lock [seconds]` (bare flag = 60, valid 1–3600) replaces that with a
|
|
89
|
+
bounded wait, and the budget covers **every** lock the command takes — for
|
|
90
|
+
`furnace apply`/`deploy`/`sync` that means the furnace lock as well as the
|
|
91
|
+
engine-session lock. While waiting, a progress line roughly every 5 seconds
|
|
92
|
+
identifies the holder by PID, command and start time.
|
|
93
|
+
|
|
94
|
+
Commands that take no lock accept `--wait-lock` and ignore it, so a scripted
|
|
95
|
+
sequence can blanket-append the flag without a usage error killing the run.
|
|
96
|
+
|
|
97
|
+
`FIREFORGE_WAIT_LOCK=<seconds>` applies the same budget to every command in a
|
|
98
|
+
session, using the same bounds as the flag. It is the only way to give a
|
|
99
|
+
budget to the furnace mutators that declare no flag (`create`, `remove`,
|
|
100
|
+
`rename`, `refresh`, `scan`, `override`, `chrome-doc`, …). An explicit
|
|
101
|
+
`--wait-lock` always wins.
|
|
102
|
+
|
|
103
|
+
`fireforge status --lock` reports the current holder and queue depth.
|
|
104
|
+
|
|
105
|
+
## Environment variables
|
|
106
|
+
|
|
107
|
+
| Variable | Effect |
|
|
108
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `FIREFORGE_WAIT_LOCK` | Session-wide lock wait budget in seconds (1–3600). See above. |
|
|
110
|
+
| `FIREFORGE_MAX_UNTRACKED_FILES` | Per-directory cap when expanding collapsed untracked directories in `status` (default 5000). A non-positive value warns and is ignored. |
|
|
111
|
+
| `FIREFORGE_GIT_ADD_TIMEOUT_MS` | Timeout for the monolithic `git add -A` baseline pass (default 10 min). For slow or loaded filesystems. |
|
|
112
|
+
| `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` | Timeout for the chunked `git add -- <dir>` fallback (default 30 min). Paired with the above. |
|
|
113
|
+
|
|
114
|
+
`CLAUDECODE` is read by mozbuild, not by FireForge, and `fireforge test
|
|
115
|
+
--full-output` unsets it for test dispatches only — see
|
|
116
|
+
[`testing.md`](testing.md#output-verbosity).
|