@hominis/fireforge 0.14.0 → 0.15.2
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 +44 -1
- package/README.md +41 -3
- package/dist/src/commands/build.js +12 -1
- package/dist/src/commands/furnace/create-templates.d.ts +47 -0
- package/dist/src/commands/furnace/create-templates.js +135 -0
- package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
- package/dist/src/commands/furnace/create-xpcshell.js +53 -0
- package/dist/src/commands/furnace/create.js +81 -109
- package/dist/src/commands/furnace/deploy.js +3 -3
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/commands/test.js +20 -0
- package/dist/src/core/build-prepare.js +6 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +32 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +33 -0
- package/dist/src/core/furnace-apply-ftl.js +102 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +10 -1
- package/dist/src/core/furnace-apply-helpers.js +16 -12
- package/dist/src/core/furnace-apply.js +7 -4
- package/dist/src/core/furnace-config-tokens.d.ts +17 -0
- package/dist/src/core/furnace-config-tokens.js +43 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +16 -3
- package/dist/src/core/furnace-constants.d.ts +20 -0
- package/dist/src/core/furnace-constants.js +32 -0
- package/dist/src/core/furnace-registration-ast.d.ts +13 -1
- package/dist/src/core/furnace-registration-ast.js +58 -25
- package/dist/src/core/furnace-registration.d.ts +27 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +39 -1
- package/dist/src/core/furnace-validate-helpers.js +182 -18
- package/dist/src/core/furnace-validate-registration.d.ts +8 -2
- package/dist/src/core/furnace-validate-registration.js +34 -9
- package/dist/src/core/furnace-validate.js +2 -2
- package/dist/src/core/marionette-preflight.d.ts +46 -0
- package/dist/src/core/marionette-preflight.js +260 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +1 -1
- package/dist/src/core/patch-lint.js +29 -9
- package/dist/src/types/commands/options.d.ts +16 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +19 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
|
-
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasGenericInteractiveElement, hasPositiveTabindex, hasTemplateClickHandler, hasTemplateKeyboardHandler, hasUnlabelledFormInput, } from './furnace-validate-helpers.js';
|
|
4
|
+
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasGenericInteractiveElement, hasPositiveTabindex, hasTemplateClickHandler, hasTemplateClickOnSyntheticInteractive, hasTemplateKeyboardHandler, hasUnlabelledFormInput, } from './furnace-validate-helpers.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validates accessibility patterns in a component's .mjs file.
|
|
7
7
|
* Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
|
|
@@ -17,7 +17,13 @@ export async function validateAccessibility(componentDir, tagName) {
|
|
|
17
17
|
}
|
|
18
18
|
const hasClick = hasTemplateClickHandler(content);
|
|
19
19
|
const hasKeyboardHandler = hasTemplateKeyboardHandler(content);
|
|
20
|
-
|
|
20
|
+
// Native interactive elements (<button>, <a href>, form controls,
|
|
21
|
+
// moz-button/moz-toggle/etc.) dispatch click on Enter/Space via the
|
|
22
|
+
// platform, so a duplicate keyboard handler would usually double-fire.
|
|
23
|
+
// Only flag synthetic markup (e.g. `<div @click>`) where the activation
|
|
24
|
+
// path has to be wired manually.
|
|
25
|
+
const hasClickOnSynthetic = hasTemplateClickOnSyntheticInteractive(content);
|
|
26
|
+
if (hasClickOnSynthetic && !hasKeyboardHandler) {
|
|
21
27
|
issues.push(createIssue(tagName, 'warning', 'no-keyboard-handler', 'Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup).'));
|
|
22
28
|
}
|
|
23
29
|
if (containsHardcodedTemplateText(content)) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
4
|
import { hasRawCssColors } from '../utils/regex.js';
|
|
5
|
-
import { classExtendsMozLitElement, collectCssVariableReferences, createIssue, getTokenPrefixContext, hasCustomElementDefineCall, hasRelativeModuleImport, stripCssBlockComments, } from './furnace-validate-helpers.js';
|
|
5
|
+
import { classExtendsMozLitElement, collectCssVariableDeclarations, collectCssVariableReferences, createIssue, getTokenPrefixContext, hasCustomElementDefineCall, hasRelativeModuleImport, stripCssBlockComments, } from './furnace-validate-helpers.js';
|
|
6
6
|
async function validateMjsCompatibility(mjsPath, tagName) {
|
|
7
7
|
if (!(await pathExists(mjsPath)))
|
|
8
8
|
return [];
|
|
@@ -29,13 +29,24 @@ async function validateCssCompatibility(cssPath, tagName, type, config, root) {
|
|
|
29
29
|
issues.push(createIssue(tagName, 'error', 'raw-color-value', 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.'));
|
|
30
30
|
}
|
|
31
31
|
if (config?.tokenPrefix) {
|
|
32
|
-
const { allowlist, inheritedOverrideVars } = await getTokenPrefixContext(tagName, type, config, root);
|
|
32
|
+
const { allowlist, inheritedOverrideVars, runtimeVariables } = await getTokenPrefixContext(tagName, type, config, root);
|
|
33
|
+
// Auto-exempt component-local runtime channels: a CSS custom property
|
|
34
|
+
// both declared and consumed in the same file is a runtime state
|
|
35
|
+
// channel (e.g. `--cam-x`), not a design-token reference. See
|
|
36
|
+
// `runtimeVariables` in furnace.json for cross-component cases.
|
|
37
|
+
const localDeclarations = collectCssVariableDeclarations(cssContent);
|
|
33
38
|
for (const prop of collectCssVariableReferences(cssContent)) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if (prop.startsWith(config.tokenPrefix))
|
|
40
|
+
continue;
|
|
41
|
+
if (allowlist.has(prop))
|
|
42
|
+
continue;
|
|
43
|
+
if (inheritedOverrideVars.has(prop))
|
|
44
|
+
continue;
|
|
45
|
+
if (runtimeVariables.has(prop))
|
|
46
|
+
continue;
|
|
47
|
+
if (localDeclarations.has(prop))
|
|
48
|
+
continue;
|
|
49
|
+
issues.push(createIssue(tagName, 'error', 'token-prefix-violation', `CSS references var(${prop}) which does not match the required token prefix "${config.tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`));
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
// Flag excessive !important usage
|
|
@@ -13,7 +13,33 @@ export declare function hasUnlabelledFormInput(content: string): boolean;
|
|
|
13
13
|
export declare function hasTemplateClickHandler(content: string): boolean;
|
|
14
14
|
/** Detects Lit-style template keyboard handlers. */
|
|
15
15
|
export declare function hasTemplateKeyboardHandler(content: string): boolean;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when `content` has at least one `@click=${...}` handler on a
|
|
18
|
+
* *synthetic* interactive element (e.g. `<div @click>`), which lacks native
|
|
19
|
+
* keyboard activation and therefore needs an explicit key handler for
|
|
20
|
+
* Enter/Space. Returns false when every `@click` handler sits on a native
|
|
21
|
+
* interactive element — those already fire `click` on keyboard activation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasTemplateClickOnSyntheticInteractive(content: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Detects hardcoded user-visible template text that should usually be
|
|
26
|
+
* localized.
|
|
27
|
+
*
|
|
28
|
+
* Scoped to three positive contexts rather than scanning the whole file,
|
|
29
|
+
* because a bare `>…<` regex catches JS comparisons (`if (x > 0 && y <
|
|
30
|
+
* 100)`), diagnostic strings (`console.error("Failed <id> lookup")`), and
|
|
31
|
+
* identifier literals that are never shown to a user. Only matches that
|
|
32
|
+
* actually enter a UI render path count:
|
|
33
|
+
*
|
|
34
|
+
* 1. Content inside a Lit `` html`…` `` tagged template literal.
|
|
35
|
+
* 2. The string literal on the RHS of `.textContent = "…"` or
|
|
36
|
+
* `.innerHTML = "…"`.
|
|
37
|
+
* 3. The string literal assigned to an XUL-widget `label=`,
|
|
38
|
+
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
39
|
+
*
|
|
40
|
+
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
41
|
+
* findings (matches the pre-existing escape hatch).
|
|
42
|
+
*/
|
|
17
43
|
export declare function containsHardcodedTemplateText(content: string): boolean;
|
|
18
44
|
/** Detects whether a component opts into shadow-root focus delegation. */
|
|
19
45
|
export declare function hasDelegatesFocusEnabled(content: string): boolean;
|
|
@@ -27,8 +53,20 @@ export declare function hasCustomElementDefineCall(mjsContent: string): boolean;
|
|
|
27
53
|
export declare function classExtendsMozLitElement(mjsContent: string): boolean;
|
|
28
54
|
/** Collects CSS custom property references used via var(--token-name). */
|
|
29
55
|
export declare function collectCssVariableReferences(cssContent: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Collects CSS custom property *declarations* — names appearing on the
|
|
58
|
+
* left-hand side of a `--name:` declaration. Used to auto-exempt
|
|
59
|
+
* component-local runtime variables from the token-prefix check: if the
|
|
60
|
+
* component both declares and consumes a variable in its own CSS file, it
|
|
61
|
+
* is a local runtime channel, not a design-token reference.
|
|
62
|
+
*
|
|
63
|
+
* The anchor `(?:^|[{;,\s])` rules out `var(--name)` occurrences (which are
|
|
64
|
+
* always preceded by `(`), so references are not mistaken for declarations.
|
|
65
|
+
*/
|
|
66
|
+
export declare function collectCssVariableDeclarations(cssContent: string): Set<string>;
|
|
30
67
|
/** Builds token-validation context from the config allowlist and inherited override CSS. */
|
|
31
68
|
export declare function getTokenPrefixContext(tagName: string, type: ComponentType, config: FurnaceConfig, root: string | undefined): Promise<{
|
|
32
69
|
allowlist: Set<string>;
|
|
33
70
|
inheritedOverrideVars: Set<string>;
|
|
71
|
+
runtimeVariables: Set<string>;
|
|
34
72
|
}>;
|
|
@@ -52,6 +52,87 @@ export function hasTemplateClickHandler(content) {
|
|
|
52
52
|
export function hasTemplateKeyboardHandler(content) {
|
|
53
53
|
return /@key(down|press|up)\s*=\s*\$\{/.test(content);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Native HTML elements that dispatch `click` on Enter and Space via the
|
|
57
|
+
* platform. Attaching `@click` to these is NOT a keyboard-a11y bug because
|
|
58
|
+
* the browser already handles the keyboard activation path — a duplicate
|
|
59
|
+
* `@keydown`/`@keypress` handler would usually double-fire the action.
|
|
60
|
+
*
|
|
61
|
+
* `<a>` is accepted only when an `href` attribute is present; bare `<a>` is
|
|
62
|
+
* non-interactive and is treated as synthetic.
|
|
63
|
+
*/
|
|
64
|
+
const NATIVE_CLICK_INTERACTIVE_TAGS = new Set([
|
|
65
|
+
'button',
|
|
66
|
+
'input',
|
|
67
|
+
'select',
|
|
68
|
+
'textarea',
|
|
69
|
+
'summary',
|
|
70
|
+
'details',
|
|
71
|
+
// Mozilla widgets that extend the native pattern and keep Enter/Space activation.
|
|
72
|
+
'moz-button',
|
|
73
|
+
'moz-toggle',
|
|
74
|
+
'moz-checkbox',
|
|
75
|
+
'moz-radio',
|
|
76
|
+
'moz-radio-group',
|
|
77
|
+
'moz-menulist',
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* Returns true when `content` has at least one `@click=${...}` handler on a
|
|
81
|
+
* *synthetic* interactive element (e.g. `<div @click>`), which lacks native
|
|
82
|
+
* keyboard activation and therefore needs an explicit key handler for
|
|
83
|
+
* Enter/Space. Returns false when every `@click` handler sits on a native
|
|
84
|
+
* interactive element — those already fire `click` on keyboard activation.
|
|
85
|
+
*/
|
|
86
|
+
export function hasTemplateClickOnSyntheticInteractive(content) {
|
|
87
|
+
const pattern = /@click\s*=\s*\$\{/g;
|
|
88
|
+
let match;
|
|
89
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
90
|
+
if (isClickOnSyntheticInteractive(content, match.index)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Given the offset of an `@click=${...}` occurrence, walks backwards to find
|
|
98
|
+
* the opening `<tag` that owns it and decides whether that tag is a native
|
|
99
|
+
* interactive element (no warning needed) or a synthetic one (warning needed).
|
|
100
|
+
*/
|
|
101
|
+
function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
102
|
+
// Find the nearest preceding `<` that starts a tag (skip `</` closers).
|
|
103
|
+
let i = clickIndex - 1;
|
|
104
|
+
let tagOpenIndex = -1;
|
|
105
|
+
while (i >= 0) {
|
|
106
|
+
if (content[i] === '<' && content[i + 1] !== '/') {
|
|
107
|
+
tagOpenIndex = i;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
// A `>` before an unclosed `<` means we're outside the attribute list,
|
|
111
|
+
// which shouldn't happen for a well-formed template but we defensively
|
|
112
|
+
// treat it as synthetic to preserve the prior-behaviour warning.
|
|
113
|
+
if (content[i] === '>') {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
i--;
|
|
117
|
+
}
|
|
118
|
+
if (tagOpenIndex < 0)
|
|
119
|
+
return true;
|
|
120
|
+
const tagMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(content.slice(tagOpenIndex));
|
|
121
|
+
if (!tagMatch?.[1])
|
|
122
|
+
return true;
|
|
123
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
124
|
+
if (tagName === 'a') {
|
|
125
|
+
// Bare <a> (no href) is non-interactive; require a keyboard handler.
|
|
126
|
+
// Look forward from the tag open for the closing `>` and scan the
|
|
127
|
+
// attribute text in between for an href attribute.
|
|
128
|
+
const tagEnd = content.indexOf('>', tagOpenIndex);
|
|
129
|
+
if (tagEnd < 0)
|
|
130
|
+
return true;
|
|
131
|
+
const attrs = content.slice(tagOpenIndex, tagEnd);
|
|
132
|
+
return !/\shref\s*=/.test(attrs);
|
|
133
|
+
}
|
|
134
|
+
return !NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName);
|
|
135
|
+
}
|
|
55
136
|
function isSymbolOnlyText(text) {
|
|
56
137
|
return Array.from(text).every((character) => {
|
|
57
138
|
const code = character.codePointAt(0) ?? 0;
|
|
@@ -67,28 +148,88 @@ function isWithinLocalizedElement(content, matchIndex) {
|
|
|
67
148
|
const tagContent = contentBefore.slice(lastTagOpen, matchIndex + 1);
|
|
68
149
|
return /data-l10n-id\s*=/.test(tagContent);
|
|
69
150
|
}
|
|
70
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Detects hardcoded user-visible template text that should usually be
|
|
153
|
+
* localized.
|
|
154
|
+
*
|
|
155
|
+
* Scoped to three positive contexts rather than scanning the whole file,
|
|
156
|
+
* because a bare `>…<` regex catches JS comparisons (`if (x > 0 && y <
|
|
157
|
+
* 100)`), diagnostic strings (`console.error("Failed <id> lookup")`), and
|
|
158
|
+
* identifier literals that are never shown to a user. Only matches that
|
|
159
|
+
* actually enter a UI render path count:
|
|
160
|
+
*
|
|
161
|
+
* 1. Content inside a Lit `` html`…` `` tagged template literal.
|
|
162
|
+
* 2. The string literal on the RHS of `.textContent = "…"` or
|
|
163
|
+
* `.innerHTML = "…"`.
|
|
164
|
+
* 3. The string literal assigned to an XUL-widget `label=`,
|
|
165
|
+
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
166
|
+
*
|
|
167
|
+
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
168
|
+
* findings (matches the pre-existing escape hatch).
|
|
169
|
+
*/
|
|
71
170
|
export function containsHardcodedTemplateText(content) {
|
|
72
171
|
if (/furnace-ignore:\s*hardcoded-text/.test(content)) {
|
|
73
172
|
return false;
|
|
74
173
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
174
|
+
return (hasFlaggedTextInLitTemplates(content) ||
|
|
175
|
+
hasFlaggedTextInDomAssignment(content) ||
|
|
176
|
+
hasFlaggedTextInXulAttribute(content));
|
|
177
|
+
}
|
|
178
|
+
function isFlaggableText(text) {
|
|
179
|
+
const trimmed = text.trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return false;
|
|
182
|
+
if (/\$\{/.test(trimmed))
|
|
183
|
+
return false;
|
|
184
|
+
if (Array.from(trimmed).length <= 1)
|
|
185
|
+
return false;
|
|
186
|
+
if (isSymbolOnlyText(trimmed))
|
|
187
|
+
return false;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
function hasFlaggedTextInLitTemplates(content) {
|
|
191
|
+
// Match `html\`…\`` regions, anchored on a non-identifier char before `html`
|
|
192
|
+
// so substrings like `otherhtml` do not spuriously open a template.
|
|
193
|
+
const htmlPattern = /(?:^|[^a-zA-Z0-9_$])html`([\s\S]*?)`/g;
|
|
194
|
+
let litMatch;
|
|
195
|
+
while ((litMatch = htmlPattern.exec(content)) !== null) {
|
|
196
|
+
const region = litMatch[1] ?? '';
|
|
197
|
+
const textPattern = />([^<$\s][^<$]*)</g;
|
|
198
|
+
let textMatch;
|
|
199
|
+
while ((textMatch = textPattern.exec(region)) !== null) {
|
|
200
|
+
const text = textMatch[1] ?? '';
|
|
201
|
+
if (!isFlaggableText(text))
|
|
202
|
+
continue;
|
|
203
|
+
if (isWithinLocalizedElement(region, textMatch.index))
|
|
204
|
+
continue;
|
|
205
|
+
return true;
|
|
90
206
|
}
|
|
91
|
-
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function hasFlaggedTextInDomAssignment(content) {
|
|
211
|
+
// `<expr>.textContent = "abc"` and `<expr>.innerHTML = "abc"` — these are
|
|
212
|
+
// user-visible render paths. Template-literal RHS is excluded (usually
|
|
213
|
+
// dynamic), matching the `${` guard used elsewhere in this helper.
|
|
214
|
+
const assignPattern = /\.(?:textContent|innerHTML)\s*=\s*(["'])((?:\\.|(?!\1).)*)\1/g;
|
|
215
|
+
let match;
|
|
216
|
+
while ((match = assignPattern.exec(content)) !== null) {
|
|
217
|
+
const text = match[2] ?? '';
|
|
218
|
+
if (isFlaggableText(text))
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
function hasFlaggedTextInXulAttribute(content) {
|
|
224
|
+
// Assignments like `node.setAttribute("label", "Save")` or JS-built XUL
|
|
225
|
+
// attributes `label="…"` / `title="…"` / `tooltiptext="…"` in template
|
|
226
|
+
// literals outside Lit blocks. Covers DOM built via createXULElement.
|
|
227
|
+
const setAttrPattern = /setAttribute\s*\(\s*["'](?:label|title|tooltiptext)["']\s*,\s*(["'])((?:\\.|(?!\1).)*)\1/g;
|
|
228
|
+
let setAttrMatch;
|
|
229
|
+
while ((setAttrMatch = setAttrPattern.exec(content)) !== null) {
|
|
230
|
+
const text = setAttrMatch[2] ?? '';
|
|
231
|
+
if (isFlaggableText(text))
|
|
232
|
+
return true;
|
|
92
233
|
}
|
|
93
234
|
return false;
|
|
94
235
|
}
|
|
@@ -133,6 +274,27 @@ export function collectCssVariableReferences(cssContent) {
|
|
|
133
274
|
}
|
|
134
275
|
return referencedVariables;
|
|
135
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Collects CSS custom property *declarations* — names appearing on the
|
|
279
|
+
* left-hand side of a `--name:` declaration. Used to auto-exempt
|
|
280
|
+
* component-local runtime variables from the token-prefix check: if the
|
|
281
|
+
* component both declares and consumes a variable in its own CSS file, it
|
|
282
|
+
* is a local runtime channel, not a design-token reference.
|
|
283
|
+
*
|
|
284
|
+
* The anchor `(?:^|[{;,\s])` rules out `var(--name)` occurrences (which are
|
|
285
|
+
* always preceded by `(`), so references are not mistaken for declarations.
|
|
286
|
+
*/
|
|
287
|
+
export function collectCssVariableDeclarations(cssContent) {
|
|
288
|
+
const declared = new Set();
|
|
289
|
+
const pattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = pattern.exec(cssContent)) !== null) {
|
|
292
|
+
const name = match[1];
|
|
293
|
+
if (name)
|
|
294
|
+
declared.add(name);
|
|
295
|
+
}
|
|
296
|
+
return declared;
|
|
297
|
+
}
|
|
136
298
|
async function collectInheritedOverrideVariables(tagName, config, root) {
|
|
137
299
|
const inheritedVariables = new Set();
|
|
138
300
|
const basePath = config.overrides[tagName]?.basePath;
|
|
@@ -153,12 +315,14 @@ async function collectInheritedOverrideVariables(tagName, config, root) {
|
|
|
153
315
|
/** Builds token-validation context from the config allowlist and inherited override CSS. */
|
|
154
316
|
export async function getTokenPrefixContext(tagName, type, config, root) {
|
|
155
317
|
const allowlist = new Set(config.tokenAllowlist ?? []);
|
|
318
|
+
const runtimeVariables = new Set(config.runtimeVariables ?? []);
|
|
156
319
|
if (type !== 'override' || !root) {
|
|
157
|
-
return { allowlist, inheritedOverrideVars: new Set() };
|
|
320
|
+
return { allowlist, inheritedOverrideVars: new Set(), runtimeVariables };
|
|
158
321
|
}
|
|
159
322
|
return {
|
|
160
323
|
allowlist,
|
|
161
324
|
inheritedOverrideVars: await collectInheritedOverrideVariables(tagName, config, root),
|
|
325
|
+
runtimeVariables,
|
|
162
326
|
};
|
|
163
327
|
}
|
|
164
328
|
//# sourceMappingURL=furnace-validate-helpers.js.map
|
|
@@ -32,9 +32,15 @@ export declare function checkRegistrationConsistency(root: string, name: string,
|
|
|
32
32
|
export declare function validateJarMnEntries(root: string, config: FurnaceConfig): Promise<ValidationIssue[]>;
|
|
33
33
|
/**
|
|
34
34
|
* Validates that components using design tokens have the tokens CSS
|
|
35
|
-
* linked in
|
|
35
|
+
* linked in at least one chrome host document. Without the link, tokens
|
|
36
|
+
* silently resolve to nothing at runtime.
|
|
37
|
+
*
|
|
38
|
+
* Forks with multiple chrome host documents (e.g. `mybrowser.xhtml` beside
|
|
39
|
+
* `browser.xhtml`) can enumerate them via `tokenHostDocuments` in
|
|
40
|
+
* furnace.json; the warning fires only when NONE of the configured
|
|
41
|
+
* documents link the tokens CSS.
|
|
36
42
|
*/
|
|
37
|
-
export declare function validateTokenLink(componentDir: string, tagName: string, root: string, tokenPrefix?: string): Promise<ValidationIssue[]>;
|
|
43
|
+
export declare function validateTokenLink(componentDir: string, tagName: string, root: string, tokenPrefix?: string, tokenHostDocuments?: string[]): Promise<ValidationIssue[]>;
|
|
38
44
|
/**
|
|
39
45
|
* Post-apply registration consistency check for custom components.
|
|
40
46
|
*
|
|
@@ -206,11 +206,22 @@ export async function validateJarMnEntries(root, config) {
|
|
|
206
206
|
}
|
|
207
207
|
return issues;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Default chrome host document scanned by `validateTokenLink` when
|
|
211
|
+
* `tokenHostDocuments` is not configured in furnace.json.
|
|
212
|
+
*/
|
|
213
|
+
const DEFAULT_TOKEN_HOST_DOCUMENTS = ['browser/base/content/browser.xhtml'];
|
|
209
214
|
/**
|
|
210
215
|
* Validates that components using design tokens have the tokens CSS
|
|
211
|
-
* linked in
|
|
216
|
+
* linked in at least one chrome host document. Without the link, tokens
|
|
217
|
+
* silently resolve to nothing at runtime.
|
|
218
|
+
*
|
|
219
|
+
* Forks with multiple chrome host documents (e.g. `mybrowser.xhtml` beside
|
|
220
|
+
* `browser.xhtml`) can enumerate them via `tokenHostDocuments` in
|
|
221
|
+
* furnace.json; the warning fires only when NONE of the configured
|
|
222
|
+
* documents link the tokens CSS.
|
|
212
223
|
*/
|
|
213
|
-
export async function validateTokenLink(componentDir, tagName, root, tokenPrefix) {
|
|
224
|
+
export async function validateTokenLink(componentDir, tagName, root, tokenPrefix, tokenHostDocuments) {
|
|
214
225
|
const issues = [];
|
|
215
226
|
const cssPath = join(componentDir, `${tagName}.css`);
|
|
216
227
|
if (!(await pathExists(cssPath)))
|
|
@@ -221,11 +232,10 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
221
232
|
// Check if the component CSS references any tokens with the configured prefix
|
|
222
233
|
if (!cssContent.includes(tokenPrefix))
|
|
223
234
|
return issues;
|
|
224
|
-
// Check if browser.xhtml links the token CSS file
|
|
225
235
|
const { engine: engineDir } = getProjectPaths(root);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
const hostDocuments = tokenHostDocuments && tokenHostDocuments.length > 0
|
|
237
|
+
? tokenHostDocuments
|
|
238
|
+
: DEFAULT_TOKEN_HOST_DOCUMENTS;
|
|
229
239
|
let tokensCssFile;
|
|
230
240
|
try {
|
|
231
241
|
const forgeConfig = await loadConfig(root);
|
|
@@ -237,13 +247,28 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
237
247
|
warn(`Could not resolve token CSS link target for ${tagName} during validation: ${reason}`);
|
|
238
248
|
return issues;
|
|
239
249
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
250
|
+
const checkedDocuments = [];
|
|
251
|
+
let anyLinks = false;
|
|
252
|
+
for (const relDocPath of hostDocuments) {
|
|
253
|
+
const absPath = join(engineDir, relDocPath);
|
|
254
|
+
if (!(await pathExists(absPath)))
|
|
255
|
+
continue;
|
|
256
|
+
checkedDocuments.push(relDocPath);
|
|
257
|
+
const xhtmlContent = await readText(absPath);
|
|
258
|
+
if (xhtmlContent.includes(tokensCssFile)) {
|
|
259
|
+
anyLinks = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (checkedDocuments.length === 0)
|
|
264
|
+
return issues;
|
|
265
|
+
if (!anyLinks) {
|
|
266
|
+
const docsList = checkedDocuments.join(', ');
|
|
242
267
|
issues.push({
|
|
243
268
|
component: tagName,
|
|
244
269
|
severity: 'warning',
|
|
245
270
|
check: 'missing-token-link',
|
|
246
|
-
message: `Component uses ${tokenPrefix}* tokens but
|
|
271
|
+
message: `Component uses ${tokenPrefix}* tokens but none of the configured chrome host documents (${docsList}) link ${tokensCssFile}. Tokens will silently resolve to nothing. Configure additional hosts via furnace.json "tokenHostDocuments" if needed.`,
|
|
247
272
|
});
|
|
248
273
|
}
|
|
249
274
|
return issues;
|
|
@@ -46,9 +46,9 @@ export async function validateComponent(componentDir, tagName, type, config, roo
|
|
|
46
46
|
const forgeConfig = await loadConfig(root);
|
|
47
47
|
issues.push(...buildOverrideVersionDriftIssues(config, forgeConfig.firefox.version, tagName));
|
|
48
48
|
}
|
|
49
|
-
// Check for missing token link
|
|
49
|
+
// Check for missing token link across configured chrome host documents.
|
|
50
50
|
if (root) {
|
|
51
|
-
issues.push(...(await validateTokenLink(componentDir, tagName, root, config?.tokenPrefix)));
|
|
51
|
+
issues.push(...(await validateTokenLink(componentDir, tagName, root, config?.tokenPrefix, config?.tokenHostDocuments)));
|
|
52
52
|
}
|
|
53
53
|
// When root is provided and this is a custom component with registration,
|
|
54
54
|
// also run registration pattern and jar.mn validation for this component.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marionette handshake preflight for `fireforge test --doctor`.
|
|
3
|
+
*
|
|
4
|
+
* Answers a single question before tests run: "does marionette come up?" A
|
|
5
|
+
* silent 360-second mach-test hang is indistinguishable from "tests failed
|
|
6
|
+
* to discover"; this helper surfaces the failure in under a minute with a
|
|
7
|
+
* clear PASS/FAIL line and the tail of the browser's stderr.
|
|
8
|
+
*
|
|
9
|
+
* The probe is a cascade of layered checks (engine → mach → python →
|
|
10
|
+
* profile → spawn → handshake). Each layer has a tight per-attempt budget
|
|
11
|
+
* so a broken earlier layer fails fast with a specific diagnosis rather
|
|
12
|
+
* than blocking on the final socket poll for the full overall budget.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import net from 'node:net';
|
|
16
|
+
export interface MarionettePreflightResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
/** Human-readable summary. On FAIL, prefixed with `[layer N/6: <name>]`. */
|
|
20
|
+
detail: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MarionettePreflightOptions {
|
|
23
|
+
/** Total budget in ms. Defaults to 30 seconds. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Overrides marionette TCP port — primarily used in tests. */
|
|
26
|
+
port?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Grace window after spawn() before the browser is considered "running
|
|
29
|
+
* OK." Catches immediate crashes (missing dylib, wrong CPU arch, corrupt
|
|
30
|
+
* profile) at the spawn layer rather than the handshake layer. Default:
|
|
31
|
+
* {@link SPAWN_SETTLE_MS}. Tests may set this to 0 to skip the settle.
|
|
32
|
+
*/
|
|
33
|
+
spawnSettleMs?: number;
|
|
34
|
+
/** Test seam: spawn and socket connect factories. */
|
|
35
|
+
spawner?: typeof spawn;
|
|
36
|
+
connect?: typeof net.createConnection;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs the marionette preflight. Returns PASS on first byte read from the
|
|
40
|
+
* marionette socket within the budget; FAIL otherwise, with a diagnostic
|
|
41
|
+
* identifying which layer of the cascade broke. Always tears down the
|
|
42
|
+
* spawned browser before returning.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runMarionettePreflight(engineDir: string, options?: MarionettePreflightOptions): Promise<MarionettePreflightResult>;
|
|
45
|
+
/** Renders a PASS/FAIL banner to the CLI using the shared logger helpers. */
|
|
46
|
+
export declare function reportMarionettePreflight(result: MarionettePreflightResult): void;
|