@hominis/fireforge 0.15.1 → 0.15.3

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +76 -3
  3. package/dist/src/commands/build.js +41 -3
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
  6. package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
  7. package/dist/src/commands/furnace/chrome-doc.js +168 -0
  8. package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
  9. package/dist/src/commands/furnace/create-mochikit.js +70 -0
  10. package/dist/src/commands/furnace/create-templates.d.ts +53 -0
  11. package/dist/src/commands/furnace/create-templates.js +118 -0
  12. package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
  13. package/dist/src/commands/furnace/create-xpcshell.js +53 -0
  14. package/dist/src/commands/furnace/create.d.ts +17 -0
  15. package/dist/src/commands/furnace/create.js +59 -12
  16. package/dist/src/commands/furnace/index.d.ts +2 -1
  17. package/dist/src/commands/furnace/index.js +20 -2
  18. package/dist/src/commands/lint.d.ts +13 -1
  19. package/dist/src/commands/lint.js +33 -7
  20. package/dist/src/commands/setup.d.ts +1 -1
  21. package/dist/src/commands/setup.js +3 -2
  22. package/dist/src/core/build-audit.d.ts +46 -0
  23. package/dist/src/core/build-audit.js +251 -0
  24. package/dist/src/core/build-baseline.d.ts +59 -0
  25. package/dist/src/core/build-baseline.js +83 -0
  26. package/dist/src/core/build-prepare.d.ts +20 -1
  27. package/dist/src/core/build-prepare.js +94 -4
  28. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  29. package/dist/src/core/furnace-config-tokens.d.ts +6 -0
  30. package/dist/src/core/furnace-config-tokens.js +15 -0
  31. package/dist/src/core/furnace-config.js +10 -4
  32. package/dist/src/core/furnace-operation.d.ts +2 -1
  33. package/dist/src/core/furnace-operation.js +13 -7
  34. package/dist/src/core/furnace-registration-ast.d.ts +2 -2
  35. package/dist/src/core/furnace-registration-ast.js +1 -1
  36. package/dist/src/core/furnace-validate-compatibility.js +18 -7
  37. package/dist/src/core/furnace-validate-helpers.d.ts +31 -1
  38. package/dist/src/core/furnace-validate-helpers.js +101 -18
  39. package/dist/src/core/furnace-validate-registration.d.ts +1 -1
  40. package/dist/src/core/furnace-validate-registration.js +1 -1
  41. package/dist/src/core/mach-error-hints.d.ts +29 -0
  42. package/dist/src/core/mach-error-hints.js +43 -0
  43. package/dist/src/core/mach.d.ts +5 -2
  44. package/dist/src/core/mach.js +31 -4
  45. package/dist/src/core/marionette-preflight.d.ts +14 -7
  46. package/dist/src/core/marionette-preflight.js +94 -44
  47. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  48. package/dist/src/core/patch-lint-cross.js +1 -1
  49. package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
  50. package/dist/src/core/patch-lint-diff-tag.js +83 -0
  51. package/dist/src/core/patch-lint.js +29 -9
  52. package/dist/src/types/commands/options.d.ts +25 -0
  53. package/dist/src/types/commands/patches.d.ts +9 -0
  54. package/dist/src/types/config.d.ts +1 -1
  55. package/dist/src/types/furnace.d.ts +13 -2
  56. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Persists a marker describing the state of the engine tree at the time of
3
+ * the last successful `fireforge build`. Two downstream consumers share this
4
+ * marker:
5
+ *
6
+ * - `build-audit`: after a build succeeds, compare engine files touched
7
+ * since the baseline against the dist bundle to flag silent
8
+ * packaging drops (e.g. a pref file never registered in moz.build).
9
+ * - `build-prepare`: before a build starts, detect whether any
10
+ * `moz.build` / `moz.configure` / `Makefile.in` changed since the
11
+ * baseline and run `mach configure` before the build step so the
12
+ * recursive-make backend isn't stale.
13
+ *
14
+ * The marker lives under `.fireforge/last-build.json`. It is written only
15
+ * on successful build completion; a failed build does not update it, so a
16
+ * subsequent run still audits against the last known-good tree.
17
+ */
18
+ /** Shape of the on-disk baseline marker. */
19
+ export interface BuildBaseline {
20
+ /** SHA of engine HEAD at the time the build succeeded. */
21
+ engineHeadSha: string;
22
+ /**
23
+ * ISO-8601 timestamp of when the baseline was recorded. Informational —
24
+ * downstream code keys off `engineHeadSha` for diffs, but the timestamp
25
+ * helps operators reason about stale markers.
26
+ */
27
+ builtAt: string;
28
+ /**
29
+ * The binaryName used at build time. Captured so the dist-tree audit
30
+ * can resolve the expected bundle root under obj-star/dist/ even when
31
+ * the project has since been renamed.
32
+ */
33
+ binaryName: string;
34
+ }
35
+ /** Name of the last-build marker file under `.fireforge/`. */
36
+ export declare const BUILD_BASELINE_FILENAME = "last-build.json";
37
+ /**
38
+ * Resolves the on-disk path of the build baseline marker.
39
+ * @param projectRoot - Root directory of the project
40
+ * @returns Absolute path of the marker file
41
+ */
42
+ export declare function getBuildBaselinePath(projectRoot: string): string;
43
+ /**
44
+ * Reads the last-build baseline if present. Returns undefined when no
45
+ * previous successful build has been recorded — callers must tolerate that
46
+ * path (first build, cleaned workspace).
47
+ * @param projectRoot - Root directory of the project
48
+ */
49
+ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBaseline | undefined>;
50
+ /**
51
+ * Records a successful build by writing a fresh baseline marker. Captures
52
+ * engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
53
+ * the current binaryName. Caller is responsible for only invoking this
54
+ * after the build exit code was zero.
55
+ * @param projectRoot - Root directory of the project
56
+ * @param engineDir - Path to the engine directory
57
+ * @param binaryName - Current `binaryName` from fireforge.json
58
+ */
59
+ export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string): Promise<void>;
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Persists a marker describing the state of the engine tree at the time of
4
+ * the last successful `fireforge build`. Two downstream consumers share this
5
+ * marker:
6
+ *
7
+ * - `build-audit`: after a build succeeds, compare engine files touched
8
+ * since the baseline against the dist bundle to flag silent
9
+ * packaging drops (e.g. a pref file never registered in moz.build).
10
+ * - `build-prepare`: before a build starts, detect whether any
11
+ * `moz.build` / `moz.configure` / `Makefile.in` changed since the
12
+ * baseline and run `mach configure` before the build step so the
13
+ * recursive-make backend isn't stale.
14
+ *
15
+ * The marker lives under `.fireforge/last-build.json`. It is written only
16
+ * on successful build completion; a failed build does not update it, so a
17
+ * subsequent run still audits against the last known-good tree.
18
+ */
19
+ import { join } from 'node:path';
20
+ import { pathExists, readJson, writeJson } from '../utils/fs.js';
21
+ import { FIREFORGE_DIR } from './config-paths.js';
22
+ import { getHead, isMissingHeadError } from './git.js';
23
+ /** Name of the last-build marker file under `.fireforge/`. */
24
+ export const BUILD_BASELINE_FILENAME = 'last-build.json';
25
+ /**
26
+ * Resolves the on-disk path of the build baseline marker.
27
+ * @param projectRoot - Root directory of the project
28
+ * @returns Absolute path of the marker file
29
+ */
30
+ export function getBuildBaselinePath(projectRoot) {
31
+ return join(projectRoot, FIREFORGE_DIR, BUILD_BASELINE_FILENAME);
32
+ }
33
+ /**
34
+ * Reads the last-build baseline if present. Returns undefined when no
35
+ * previous successful build has been recorded — callers must tolerate that
36
+ * path (first build, cleaned workspace).
37
+ * @param projectRoot - Root directory of the project
38
+ */
39
+ export async function readBuildBaseline(projectRoot) {
40
+ const path = getBuildBaselinePath(projectRoot);
41
+ if (!(await pathExists(path))) {
42
+ return undefined;
43
+ }
44
+ try {
45
+ return await readJson(path);
46
+ }
47
+ catch {
48
+ // A corrupt marker is equivalent to no marker — the audit/auto-configure
49
+ // will treat it as "first build" rather than block on the inconsistency.
50
+ return undefined;
51
+ }
52
+ }
53
+ /**
54
+ * Records a successful build by writing a fresh baseline marker. Captures
55
+ * engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
56
+ * the current binaryName. Caller is responsible for only invoking this
57
+ * after the build exit code was zero.
58
+ * @param projectRoot - Root directory of the project
59
+ * @param engineDir - Path to the engine directory
60
+ * @param binaryName - Current `binaryName` from fireforge.json
61
+ */
62
+ export async function writeBuildBaseline(projectRoot, engineDir, binaryName) {
63
+ let engineHeadSha = '';
64
+ try {
65
+ engineHeadSha = await getHead(engineDir);
66
+ }
67
+ catch (error) {
68
+ // Engine may be an unborn branch (freshly cloned + reset, or mid-import)
69
+ // — record an empty SHA and let downstream fall back to "no prior state"
70
+ // behavior. Any other git failure is bubbled up; we don't want to
71
+ // silently write a garbage marker.
72
+ if (!isMissingHeadError(error)) {
73
+ throw error;
74
+ }
75
+ }
76
+ const baseline = {
77
+ engineHeadSha,
78
+ builtAt: new Date().toISOString(),
79
+ binaryName,
80
+ };
81
+ await writeJson(getBuildBaselinePath(projectRoot), baseline);
82
+ }
83
+ //# sourceMappingURL=build-baseline.js.map
@@ -3,13 +3,32 @@
3
3
  * story cleanup, branding setup, Furnace component application, and mozconfig generation.
4
4
  */
5
5
  import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
6
+ import type { BuildBaseline } from './build-baseline.js';
6
7
  /**
7
8
  * Result of the build preparation phase.
8
9
  */
9
10
  export interface BuildPreparation {
10
11
  /** Number of Furnace components applied (0 if none or no furnace.json) */
11
12
  furnaceApplied: number;
13
+ /** True when `mach configure` was auto-run to refresh a stale backend. */
14
+ reconfigured: boolean;
12
15
  }
16
+ /** Options for {@link prepareBuildEnvironment}. */
17
+ export interface PrepareBuildOptions {
18
+ /**
19
+ * Previous successful-build baseline, used to detect `moz.build` /
20
+ * `moz.configure` / `Makefile.in` changes that require a fresh
21
+ * `mach configure` before the build. When undefined, the auto-configure
22
+ * step is skipped — there's no reference point for what "changed since"
23
+ * means.
24
+ */
25
+ previousBaseline?: BuildBaseline | undefined;
26
+ }
27
+ /**
28
+ * Returns true when the file path matches a pattern that forces
29
+ * `mach configure` to regenerate the backend. Exported for testing.
30
+ */
31
+ export declare function isBackendInvalidatingFile(path: string): boolean;
13
32
  /**
14
33
  * Runs the shared pre-flight steps for build and package commands:
15
34
  * 1. Cleans Furnace stories from engine (prevents leaking into production)
@@ -22,4 +41,4 @@ export interface BuildPreparation {
22
41
  * @param config - Loaded FireForge configuration
23
42
  * @returns Preparation results
24
43
  */
25
- export declare function prepareBuildEnvironment(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig): Promise<BuildPreparation>;
44
+ export declare function prepareBuildEnvironment(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig, options?: PrepareBuildOptions): Promise<BuildPreparation>;
@@ -4,14 +4,72 @@
4
4
  * story cleanup, branding setup, Furnace component application, and mozconfig generation.
5
5
  */
6
6
  import { FurnaceError } from '../errors/furnace.js';
7
+ import { toError } from '../utils/errors.js';
7
8
  import { pathExists } from '../utils/fs.js';
8
- import { spinner, warn } from '../utils/logger.js';
9
+ import { info, spinner, verbose, warn } from '../utils/logger.js';
9
10
  import { isBrandingSetup, setupBranding } from './branding.js';
10
11
  import { applyAllComponents } from './furnace-apply.js';
11
12
  import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
12
13
  import { runFurnaceMutation } from './furnace-operation.js';
13
14
  import { cleanStories } from './furnace-stories.js';
14
- import { generateMozconfig } from './mach.js';
15
+ import { hasChanges, isMissingHeadError } from './git.js';
16
+ import { git } from './git-base.js';
17
+ import { getUntrackedFiles } from './git-status.js';
18
+ import { generateMozconfig, runMach } from './mach.js';
19
+ /** Path fragments of files whose edits invalidate the recursive-make backend. */
20
+ const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
21
+ /**
22
+ * Returns true when the file path matches a pattern that forces
23
+ * `mach configure` to regenerate the backend. Exported for testing.
24
+ */
25
+ export function isBackendInvalidatingFile(path) {
26
+ for (const suffix of BACKEND_INVALIDATING_SUFFIXES) {
27
+ if (path === suffix || path.endsWith(`/${suffix}`))
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Collects engine-relative paths of files changed since the baseline's HEAD
34
+ * SHA plus any workdir modifications. Defensive — git failures surface as
35
+ * verbose lines and return the files collected so far. An empty result
36
+ * means "no drift we can prove" rather than "no drift occurred".
37
+ */
38
+ async function collectBackendRelevantChanges(engineDir, baseline) {
39
+ const collected = new Set();
40
+ if (baseline.engineHeadSha) {
41
+ try {
42
+ const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
43
+ for (const line of diff.split('\n')) {
44
+ const trimmed = line.trim();
45
+ if (trimmed)
46
+ collected.add(trimmed);
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (!isMissingHeadError(error)) {
51
+ verbose(`Auto-configure: could not diff engine against baseline — ${toError(error).message}`);
52
+ }
53
+ }
54
+ }
55
+ try {
56
+ if (await hasChanges(engineDir)) {
57
+ const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
58
+ for (const line of worktreeDiff.split('\n')) {
59
+ const trimmed = line.trim();
60
+ if (trimmed)
61
+ collected.add(trimmed);
62
+ }
63
+ for (const file of await getUntrackedFiles(engineDir)) {
64
+ collected.add(file);
65
+ }
66
+ }
67
+ }
68
+ catch (error) {
69
+ verbose(`Auto-configure: could not enumerate workdir changes — ${toError(error).message}`);
70
+ }
71
+ return [...collected];
72
+ }
15
73
  /**
16
74
  * Runs the shared pre-flight steps for build and package commands:
17
75
  * 1. Cleans Furnace stories from engine (prevents leaking into production)
@@ -24,7 +82,7 @@ import { generateMozconfig } from './mach.js';
24
82
  * @param config - Loaded FireForge configuration
25
83
  * @returns Preparation results
26
84
  */
27
- export async function prepareBuildEnvironment(projectRoot, paths, config) {
85
+ export async function prepareBuildEnvironment(projectRoot, paths, config, options = {}) {
28
86
  // Block the build if Furnace has an unresolved repair marker. This prevents
29
87
  // building against an engine that may be in an inconsistent state after a
30
88
  // failed rollback.
@@ -36,6 +94,33 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
36
94
  'Run "fireforge doctor --repair-furnace" to reconcile engine state before building.');
37
95
  }
38
96
  }
97
+ // Auto-configure: if any backend-invalidating file (moz.build, moz.configure,
98
+ // Makefile.in) changed since the last successful build, run `mach configure`
99
+ // before the build step. Prevents incremental builds from silently skipping
100
+ // work against a stale recursive-make backend.
101
+ let reconfigured = false;
102
+ if (options.previousBaseline) {
103
+ const changed = await collectBackendRelevantChanges(paths.engine, options.previousBaseline);
104
+ const invalidating = changed.filter(isBackendInvalidatingFile);
105
+ if (invalidating.length > 0) {
106
+ info(`Backend config changed; running mach configure first... (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched)`);
107
+ const configureSpinner = spinner('Running mach configure...');
108
+ try {
109
+ const exitCode = await runMach(['configure'], paths.engine);
110
+ if (exitCode !== 0) {
111
+ configureSpinner.error('mach configure exited non-zero; continuing with build anyway');
112
+ }
113
+ else {
114
+ configureSpinner.stop('Backend regenerated');
115
+ reconfigured = true;
116
+ }
117
+ }
118
+ catch (error) {
119
+ configureSpinner.error('mach configure failed; continuing with build anyway');
120
+ verbose(`Auto-configure error: ${toError(error).message}`);
121
+ }
122
+ }
123
+ }
39
124
  // Clean stories before build to ensure they don't leak into production binary
40
125
  await cleanStories(paths.engine);
41
126
  // Set up custom branding directory and patch moz.configure
@@ -95,7 +180,12 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
95
180
  throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
96
181
  }
97
182
  if (furnaceApplied > 0) {
183
+ const appliedNames = result.applied.map((entry) => entry.name).join(', ');
98
184
  furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
185
+ // Loud banner: the build operator needs to see that engine/ was
186
+ // updated before this build, otherwise a silent re-apply is
187
+ // indistinguishable from a build that shipped stale components.
188
+ info(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
99
189
  }
100
190
  else {
101
191
  furnaceSpinner.stop('Components up to date');
@@ -112,6 +202,6 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
112
202
  mozconfigSpinner.error('Failed to generate mozconfig');
113
203
  throw error;
114
204
  }
115
- return { furnaceApplied };
205
+ return { furnaceApplied, reconfigured };
116
206
  }
117
207
  //# sourceMappingURL=build-prepare.js.map
@@ -94,7 +94,7 @@ export declare function hasCustomEngineDrift(root: string, name: string, compone
94
94
  export interface CustomApplyOptions {
95
95
  /**
96
96
  * Trailing project marker appended to inserted `customElements.js` entries
97
- * (e.g. `"HOMINIS"` emits ` // HOMINIS:` on each line). Mirrors the
97
+ * (e.g. `"MYBROWSER"` emits ` // MYBROWSER:` on each line). Mirrors the
98
98
  * `markerComment` field in fireforge.json.
99
99
  */
100
100
  markerComment?: string;
@@ -9,3 +9,9 @@
9
9
  * violation; does nothing for `undefined` (field is optional).
10
10
  */
11
11
  export declare function validateTokenHostDocuments(raw: unknown): void;
12
+ /**
13
+ * Validates a `runtimeVariables` raw value. Each entry must start with `--`
14
+ * (it is a CSS custom property name). Throws `FurnaceError` on violation;
15
+ * does nothing for `undefined` (field is optional).
16
+ */
17
+ export declare function validateRuntimeVariables(raw: unknown): void;
@@ -25,4 +25,19 @@ export function validateTokenHostDocuments(raw) {
25
25
  }
26
26
  }
27
27
  }
28
+ /**
29
+ * Validates a `runtimeVariables` raw value. Each entry must start with `--`
30
+ * (it is a CSS custom property name). Throws `FurnaceError` on violation;
31
+ * does nothing for `undefined` (field is optional).
32
+ */
33
+ export function validateRuntimeVariables(raw) {
34
+ if (raw === undefined)
35
+ return;
36
+ const vars = parseStringArray(raw, 'runtimeVariables');
37
+ for (const name of vars) {
38
+ if (!name.startsWith('--')) {
39
+ throw new FurnaceError(`Furnace config: "runtimeVariables" entries must start with "--" (got ${JSON.stringify(name)})`);
40
+ }
41
+ }
42
+ }
28
43
  //# sourceMappingURL=furnace-config-tokens.js.map
@@ -7,7 +7,7 @@ import { warn } from '../utils/logger.js';
7
7
  import { isExplicitAbsolutePath } from '../utils/paths.js';
8
8
  import { isArray, isBoolean, isObject, isString } from '../utils/validation.js';
9
9
  import { FIREFORGE_DIR } from './config.js';
10
- import { validateTokenHostDocuments } from './furnace-config-tokens.js';
10
+ import { validateRuntimeVariables, validateTokenHostDocuments } from './furnace-config-tokens.js';
11
11
  import { resolveFtlDir } from './furnace-constants.js';
12
12
  import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
13
13
  import { quarantineStateFile, withStateFileLock } from './state-file.js';
@@ -183,6 +183,9 @@ export function validateFurnaceConfig(data) {
183
183
  if (migrated['tokenAllowlist'] !== undefined) {
184
184
  parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
185
185
  }
186
+ // Validate optional runtimeVariables — CSS runtime state channels
187
+ // (e.g. `--cam-x`) that are exempt from `token-prefix-violation`.
188
+ validateRuntimeVariables(migrated['runtimeVariables']);
186
189
  // Validate optional tokenHostDocuments — list of chrome XHTMLs that the
187
190
  // `missing-token-link` validator scans for the tokens CSS link.
188
191
  validateTokenHostDocuments(migrated['tokenHostDocuments']);
@@ -236,14 +239,17 @@ export function validateFurnaceConfig(data) {
236
239
  overrides,
237
240
  custom,
238
241
  };
239
- if (migrated['tokenPrefix'] !== undefined) {
242
+ if (migrated['tokenPrefix'] !== undefined)
240
243
  config.tokenPrefix = migrated['tokenPrefix'];
241
- }
242
244
  if (migrated['tokenAllowlist'] !== undefined) {
243
245
  config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
244
246
  }
247
+ if (migrated['runtimeVariables'] !== undefined) {
248
+ config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
249
+ }
245
250
  if (migrated['tokenHostDocuments'] !== undefined) {
246
- config.tokenHostDocuments = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
251
+ const docs = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
252
+ config.tokenHostDocuments = docs;
247
253
  }
248
254
  // Validate optional ftlBasePath
249
255
  if (migrated['ftlBasePath'] !== undefined) {
@@ -4,7 +4,8 @@ import { type RollbackJournal } from './furnace-rollback.js';
4
4
  * The signal names the lifecycle wrapper knows how to react to. Spelled out
5
5
  * as a literal union (rather than `NodeJS.Signals`) so the public type
6
6
  * surface does not depend on the NodeJS global namespace — consumers of
7
- * `@hominis/fireforge` may compile against tsconfigs that omit `@types/node`.
7
+ * FireForge's published scoped npm package may compile against tsconfigs
8
+ * that omit `@types/node`.
8
9
  */
9
10
  export type FurnaceShutdownSignal = 'SIGINT' | 'SIGTERM';
10
11
  /**
@@ -52,16 +52,22 @@ function withTimeout(promise, ms, label) {
52
52
  * timeout so a stuck I/O operation cannot hang the process indefinitely.
53
53
  */
54
54
  export async function rollbackActiveOperationsForSignal(signal) {
55
+ // Snapshot the active operations so we don't race with `runFurnaceMutation`
56
+ // clearing slots during normal completion. Filter completed bodies so a
57
+ // body sitting in its finally-block cleanup window is not counted as live
58
+ // work — this would mis-trigger the rollback banner for plain `fireforge
59
+ // run` (which never registers a mutation but can receive SIGTERM).
60
+ const snapshot = [...activeOperations.values()].filter((op) => !op.completed);
61
+ if (snapshot.length === 0) {
62
+ // Nothing to roll back. Stay silent so commands that never mutated (run,
63
+ // watch, test, doctor) don't print an alarming "rolling back mutations"
64
+ // line on Ctrl+C / SIGTERM. Leave `signalRollbackInFlight` false so a
65
+ // subsequent registrant can still trigger the full path.
66
+ return;
67
+ }
55
68
  signalRollbackInFlight = true;
56
69
  warn(`Received ${signal}; rolling back in-flight furnace mutations…`);
57
- // Snapshot the active operations so we don't race with `runFurnaceMutation`
58
- // clearing slots during normal completion.
59
- const snapshot = [...activeOperations.values()];
60
70
  for (const op of snapshot) {
61
- // If the body completed successfully and is in its finally-block cleanup
62
- // (deleting the token), skip rollback — the mutation committed cleanly.
63
- if (op.completed)
64
- continue;
65
71
  const cleanupErrors = [];
66
72
  // Run extra cleanup callbacks first (e.g. preview's cleanStories), so the
67
73
  // engine is in its tidiest possible shape before the journal restore
@@ -8,8 +8,8 @@
8
8
  */
9
9
  export interface RegistrationWriteOptions {
10
10
  /**
11
- * Trailing project marker appended to every inserted line (e.g. `"HOMINIS"`
12
- * produces ` // HOMINIS:` at end-of-line). Keeps modifications discoverable
11
+ * Trailing project marker appended to every inserted line (e.g. `"MYBROWSER"`
12
+ * produces ` // MYBROWSER:` at end-of-line). Keeps modifications discoverable
13
13
  * without requiring the operator to hand-tag them post-apply.
14
14
  */
15
15
  markerComment?: string;
@@ -16,7 +16,7 @@ import { validateRegistrationPlacement, validateTagName } from './furnace-regist
16
16
  /**
17
17
  * Returns true when `content` already contains a registration for `tagName`.
18
18
  *
19
- * Tolerates trailing line comments (e.g. a project marker like `// HOMINIS:`)
19
+ * Tolerates trailing line comments (e.g. a project marker like `// MYBROWSER:`)
20
20
  * that an operator may have appended to entries written by a previous apply.
21
21
  * Without this, a re-apply would insert a duplicate entry, and the second
22
22
  * `setElementCreationCallback` at window-load would throw `NotSupportedError`.
@@ -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 (!prop.startsWith(config.tokenPrefix) &&
35
- !allowlist.has(prop) &&
36
- !inheritedOverrideVars.has(prop)) {
37
- 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 or add to tokenAllowlist.`));
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
@@ -21,7 +21,25 @@ export declare function hasTemplateKeyboardHandler(content: string): boolean;
21
21
  * interactive element — those already fire `click` on keyboard activation.
22
22
  */
23
23
  export declare function hasTemplateClickOnSyntheticInteractive(content: string): boolean;
24
- /** Detects hardcoded user-visible template text that should usually be localized. */
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
+ */
25
43
  export declare function containsHardcodedTemplateText(content: string): boolean;
26
44
  /** Detects whether a component opts into shadow-root focus delegation. */
27
45
  export declare function hasDelegatesFocusEnabled(content: string): boolean;
@@ -35,8 +53,20 @@ export declare function hasCustomElementDefineCall(mjsContent: string): boolean;
35
53
  export declare function classExtendsMozLitElement(mjsContent: string): boolean;
36
54
  /** Collects CSS custom property references used via var(--token-name). */
37
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>;
38
67
  /** Builds token-validation context from the config allowlist and inherited override CSS. */
39
68
  export declare function getTokenPrefixContext(tagName: string, type: ComponentType, config: FurnaceConfig, root: string | undefined): Promise<{
40
69
  allowlist: Set<string>;
41
70
  inheritedOverrideVars: Set<string>;
71
+ runtimeVariables: Set<string>;
42
72
  }>;