@hominis/fireforge 0.32.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 +11 -0
- 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/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 +26 -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/mach-resource-shim.d.ts +21 -0
- package/dist/src/core/mach-resource-shim.js +92 -0
- package/dist/src/core/mach.js +9 -2
- package/dist/src/core/manifest-helpers.js +29 -4
- 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-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- 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/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.js +56 -28
- package/dist/src/types/commands/options.d.ts +5 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +3 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Attribute-variant block helpers for the tokens CSS scaffold.
|
|
4
|
+
*
|
|
5
|
+
* `fireforge token add --mode` can author the base `:root { }` block and the
|
|
6
|
+
* dark `@media (prefers-color-scheme: dark)` block, but there was no way to
|
|
7
|
+
* target an attribute-keyed selector such as `:root[data-skin="precision"]`
|
|
8
|
+
* or `:root[data-private]` — forcing those override blocks to be hand-edited.
|
|
9
|
+
* These helpers locate (or compute the insertion point for) a top-level
|
|
10
|
+
* `:root<variant>` block so `token-manager.ts` can splice a declaration into
|
|
11
|
+
* it, keeping all token authoring in the CLI.
|
|
12
|
+
*/
|
|
13
|
+
import { findBlockCloseIndex, stripBlockCommentsInLines } from './token-dark-mode.js';
|
|
14
|
+
/**
|
|
15
|
+
* Accepts a single attribute selector fragment: `[data-private]` (boolean
|
|
16
|
+
* attribute) or `[data-skin=precision]` / `[data-skin="precision"]`
|
|
17
|
+
* (attribute with value). The value half is restricted to identifier-safe
|
|
18
|
+
* characters so the fragment can be spliced into CSS verbatim without
|
|
19
|
+
* escaping concerns.
|
|
20
|
+
*/
|
|
21
|
+
const VARIANT_PATTERN = /^\[[a-zA-Z][a-zA-Z0-9_-]*(?:=(?:"[a-zA-Z0-9_-]+"|'[a-zA-Z0-9_-]+'|[a-zA-Z0-9_-]+))?\]$/;
|
|
22
|
+
/**
|
|
23
|
+
* Validates a `--variant` attribute selector fragment and normalizes any
|
|
24
|
+
* `=value` form to the double-quoted `="value"` shape (Mozilla convention).
|
|
25
|
+
* Boolean-attribute fragments are returned unchanged.
|
|
26
|
+
*
|
|
27
|
+
* @param raw - Raw `--variant` value from the CLI / programmatic caller.
|
|
28
|
+
*/
|
|
29
|
+
export function validateVariantSelector(raw) {
|
|
30
|
+
if (typeof raw !== 'string') {
|
|
31
|
+
return { ok: false, reason: 'must be a string when set' };
|
|
32
|
+
}
|
|
33
|
+
const value = raw.trim();
|
|
34
|
+
if (!VARIANT_PATTERN.test(value)) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: 'must be a single attribute selector like [data-private] or [data-skin=precision] ' +
|
|
38
|
+
'(identifier-safe attribute name and value only)',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const normalized = value
|
|
42
|
+
.replace(/='([a-zA-Z0-9_-]+)'\]$/, '="$1"]')
|
|
43
|
+
.replace(/=([a-zA-Z0-9_-]+)\]$/, '="$1"]');
|
|
44
|
+
return { ok: true, value: normalized };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reduces a `:root<attr>` selector to a quote- and whitespace-insensitive
|
|
48
|
+
* canonical form so `[data-skin="precision"]`, `[data-skin='precision']`,
|
|
49
|
+
* and `[data-skin=precision]` all compare equal when matching an existing
|
|
50
|
+
* block against the requested variant.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalSelector(selector) {
|
|
53
|
+
return selector.replace(/\s+/g, '').replace(/["']/g, '');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Finds the top-level `:root<variant>` block whose attribute selector
|
|
57
|
+
* matches `variant` (compared canonically). Returns the opening-brace and
|
|
58
|
+
* closing-brace line indices, or `null` when no such block exists.
|
|
59
|
+
*
|
|
60
|
+
* Scans a comment-stripped mirror of `lines` so braces inside comments do
|
|
61
|
+
* not offset the depth counter; the returned indices line up with the
|
|
62
|
+
* original array.
|
|
63
|
+
*/
|
|
64
|
+
function findVariantBlock(lines, variant) {
|
|
65
|
+
const stripped = stripBlockCommentsInLines(lines);
|
|
66
|
+
const want = canonicalSelector(`:root${variant}`);
|
|
67
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
68
|
+
const line = stripped[i] ?? '';
|
|
69
|
+
const match = /:root\[[^{]*\]/.exec(line);
|
|
70
|
+
if (!match || canonicalSelector(match[0]) !== want)
|
|
71
|
+
continue;
|
|
72
|
+
let openLine = /\{/.test(line) ? i : -1;
|
|
73
|
+
if (openLine === -1) {
|
|
74
|
+
for (let j = i + 1; j < stripped.length; j++) {
|
|
75
|
+
const next = stripped[j] ?? '';
|
|
76
|
+
if (/\{/.test(next)) {
|
|
77
|
+
openLine = j;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (/[};]/.test(next))
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (openLine === -1)
|
|
85
|
+
continue;
|
|
86
|
+
const close = findBlockCloseIndex(stripped, openLine);
|
|
87
|
+
if (close === -1)
|
|
88
|
+
continue;
|
|
89
|
+
return { open: openLine, close };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the line index at which a brand-new `:root<variant>` block should
|
|
95
|
+
* be spliced: immediately after the base `:root { }` block's closing brace
|
|
96
|
+
* (so attribute variants sit between the base block and any dark `@media`
|
|
97
|
+
* block). Falls back to end-of-file when no base `:root` block is found.
|
|
98
|
+
*/
|
|
99
|
+
function findVariantBlockInsertionPoint(lines) {
|
|
100
|
+
const stripped = stripBlockCommentsInLines(lines);
|
|
101
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
102
|
+
if (/^\s*:root\s*\{/.test(stripped[i] ?? '')) {
|
|
103
|
+
const close = findBlockCloseIndex(stripped, i);
|
|
104
|
+
if (close !== -1)
|
|
105
|
+
return close + 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return lines.length;
|
|
109
|
+
}
|
|
110
|
+
/** True when the `:root<variant>` block already declares `tokenName`. */
|
|
111
|
+
export function variantBlockHasToken(lines, variant, tokenName) {
|
|
112
|
+
const block = findVariantBlock(lines, variant);
|
|
113
|
+
if (!block)
|
|
114
|
+
return false;
|
|
115
|
+
const blockText = lines
|
|
116
|
+
.slice(block.open, block.close + 1)
|
|
117
|
+
.join('\n')
|
|
118
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
119
|
+
return blockText.includes(`${tokenName}:`);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Splices `declLine` into the `:root<variant>` block, creating the block
|
|
123
|
+
* (after the base `:root` block) when absent or appending after the last
|
|
124
|
+
* non-blank line when present. Mutates `lines` in place.
|
|
125
|
+
*/
|
|
126
|
+
export function insertVariantDeclaration(lines, variant, declLine) {
|
|
127
|
+
const block = findVariantBlock(lines, variant);
|
|
128
|
+
if (block) {
|
|
129
|
+
let insertIndex = block.close;
|
|
130
|
+
for (let i = block.close - 1; i > block.open; i--) {
|
|
131
|
+
if ((lines[i] ?? '').trim()) {
|
|
132
|
+
insertIndex = i + 1;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
lines.splice(insertIndex, 0, declLine);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
lines.splice(findVariantBlockInsertionPoint(lines), 0, '', `:root${variant} {`, declLine, '}');
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=token-variant.js.map
|
|
@@ -84,42 +84,70 @@ export async function runTypecheck(projectRoot, cfg) {
|
|
|
84
84
|
filesChecked: 0,
|
|
85
85
|
}));
|
|
86
86
|
}
|
|
87
|
-
// Compose the shim
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
// Compose the shim PER project: the effective extraShim is the per-project
|
|
88
|
+
// override (a path, or `null` to opt out) when present, else the shared
|
|
89
|
+
// top-level extraShim. A project that narrows `lib`/`types` can opt out of
|
|
90
|
+
// a Gecko-lib shim hub that another project needs, so the composed shim is
|
|
91
|
+
// no longer injected identically everywhere. Compositions are cached by the
|
|
92
|
+
// resolved extraShim path so projects sharing a shim don't recompose it.
|
|
93
|
+
const shimCache = new Map();
|
|
94
|
+
const composeForProject = async (extraShim) => {
|
|
95
|
+
const key = extraShim ?? '';
|
|
96
|
+
const cached = shimCache.get(key);
|
|
97
|
+
if (cached !== undefined)
|
|
98
|
+
return cached;
|
|
99
|
+
const composed = await composeShimSource(projectRoot, extraShim);
|
|
95
100
|
if (composed.extraShimAppended) {
|
|
96
|
-
verbose(`typecheck: extra shim ${
|
|
101
|
+
verbose(`typecheck: extra shim ${extraShim ?? ''} appended to Firefox globals shim`);
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return cfg.projects.map((project) => ({
|
|
102
|
-
project,
|
|
103
|
-
issues: [
|
|
104
|
-
{
|
|
105
|
-
file: cfg.extraShim ?? '(typecheck)',
|
|
106
|
-
line: 1,
|
|
107
|
-
column: 1,
|
|
108
|
-
code: 0,
|
|
109
|
-
category: 'error',
|
|
110
|
-
message,
|
|
111
|
-
project,
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
filesChecked: 0,
|
|
115
|
-
}));
|
|
116
|
-
}
|
|
103
|
+
shimCache.set(key, composed.source);
|
|
104
|
+
return composed.source;
|
|
105
|
+
};
|
|
117
106
|
const results = [];
|
|
118
107
|
for (const projectPath of cfg.projects) {
|
|
108
|
+
const extraShim = resolveProjectExtraShim(cfg, projectPath);
|
|
109
|
+
let shimSource;
|
|
110
|
+
try {
|
|
111
|
+
shimSource = await composeForProject(extraShim);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// A missing or unreadable shim fails only the project(s) that use it,
|
|
115
|
+
// not the whole run — projects with a different (or no) shim still run.
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
results.push({
|
|
118
|
+
project: projectPath,
|
|
119
|
+
issues: [
|
|
120
|
+
{
|
|
121
|
+
file: extraShim ?? '(typecheck)',
|
|
122
|
+
line: 1,
|
|
123
|
+
column: 1,
|
|
124
|
+
code: 0,
|
|
125
|
+
category: 'error',
|
|
126
|
+
message,
|
|
127
|
+
project: projectPath,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
filesChecked: 0,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
119
134
|
results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource));
|
|
120
135
|
}
|
|
121
136
|
return results;
|
|
122
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolves the effective extra shim for a single project: a `projectOverrides`
|
|
140
|
+
* entry wins (a string path overrides; `null` opts out → `undefined`), else
|
|
141
|
+
* the shared top-level `extraShim` applies.
|
|
142
|
+
*/
|
|
143
|
+
function resolveProjectExtraShim(cfg, projectPath) {
|
|
144
|
+
const overrides = cfg.projectOverrides;
|
|
145
|
+
if (overrides && Object.prototype.hasOwnProperty.call(overrides, projectPath)) {
|
|
146
|
+
const value = overrides[projectPath];
|
|
147
|
+
return value === null ? undefined : value;
|
|
148
|
+
}
|
|
149
|
+
return cfg.extraShim;
|
|
150
|
+
}
|
|
123
151
|
/** Runs typecheck for a single jsconfig path, isolating its failures. */
|
|
124
152
|
async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource) {
|
|
125
153
|
const absConfig = resolve(projectRoot, projectPath);
|
|
@@ -705,6 +705,11 @@ export interface TokenAddOptions {
|
|
|
705
705
|
dryRun?: boolean;
|
|
706
706
|
/** Declare the category banner in the tokens CSS when it does not exist yet. */
|
|
707
707
|
createCategory?: boolean;
|
|
708
|
+
/**
|
|
709
|
+
* Attribute selector fragment (e.g. `[data-skin=precision]` or
|
|
710
|
+
* `[data-private]`) routing the declaration into a `:root<variant>` block.
|
|
711
|
+
*/
|
|
712
|
+
variant?: string;
|
|
708
713
|
}
|
|
709
714
|
/**
|
|
710
715
|
* Options for the doctor command.
|
|
@@ -129,6 +129,19 @@ export interface TypecheckConfig {
|
|
|
129
129
|
* built-in shim first, extraShim second — augment, don't redeclare.
|
|
130
130
|
*/
|
|
131
131
|
extraShim?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Per-project override of {@link extraShim}, keyed by the project's path
|
|
134
|
+
* exactly as it appears in {@link projects}. A string value points the
|
|
135
|
+
* project at a different `.d.ts`; `null` opts the project out of the shared
|
|
136
|
+
* extra shim entirely (it absorbs only the built-in Firefox globals shim).
|
|
137
|
+
*
|
|
138
|
+
* Needed because the shared shim is injected into every project: a shim hub
|
|
139
|
+
* that references Gecko declaration libs (`lib.gecko.dom.d.ts`, …) is wanted
|
|
140
|
+
* by projects that include it but collides with a project that narrows
|
|
141
|
+
* `lib: ["ES2024", "DOM"]` (Element/Node identity splits, nsIPrincipal
|
|
142
|
+
* mismatch). A narrowed project sets `null` here to stay clean.
|
|
143
|
+
*/
|
|
144
|
+
projectOverrides?: Record<string, string | null>;
|
|
132
145
|
}
|
|
133
146
|
/**
|
|
134
147
|
* Wire command configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "^10.0.0",
|
|
65
65
|
"@types/estree": "^1.0.8",
|
|
66
|
-
"@types/node": "^
|
|
66
|
+
"@types/node": "^26.0.0",
|
|
67
67
|
"@vitest/coverage-v8": "^4.1.2",
|
|
68
68
|
"dpdm": "4.2.0",
|
|
69
69
|
"eslint": "^10.0.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
73
73
|
"fast-check": "^4.6.0",
|
|
74
74
|
"husky": "^9.1.7",
|
|
75
|
-
"knip": "6.
|
|
75
|
+
"knip": "6.17.1",
|
|
76
76
|
"lint-staged": "^17.0.4",
|
|
77
77
|
"prettier": "^3.7.4",
|
|
78
78
|
"tsx": "^4.7.0",
|