@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.
- package/CHANGELOG.md +39 -3
- package/README.md +76 -3
- package/dist/src/commands/build.js +41 -3
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
- package/dist/src/commands/furnace/chrome-doc.js +168 -0
- package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
- package/dist/src/commands/furnace/create-mochikit.js +70 -0
- package/dist/src/commands/furnace/create-templates.d.ts +53 -0
- package/dist/src/commands/furnace/create-templates.js +118 -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.d.ts +17 -0
- package/dist/src/commands/furnace/create.js +59 -12
- package/dist/src/commands/furnace/index.d.ts +2 -1
- package/dist/src/commands/furnace/index.js +20 -2
- package/dist/src/commands/lint.d.ts +13 -1
- package/dist/src/commands/lint.js +33 -7
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/core/build-audit.d.ts +46 -0
- package/dist/src/core/build-audit.js +251 -0
- package/dist/src/core/build-baseline.d.ts +59 -0
- package/dist/src/core/build-baseline.js +83 -0
- package/dist/src/core/build-prepare.d.ts +20 -1
- package/dist/src/core/build-prepare.js +94 -4
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-config-tokens.d.ts +6 -0
- package/dist/src/core/furnace-config-tokens.js +15 -0
- package/dist/src/core/furnace-config.js +10 -4
- package/dist/src/core/furnace-operation.d.ts +2 -1
- package/dist/src/core/furnace-operation.js +13 -7
- package/dist/src/core/furnace-registration-ast.d.ts +2 -2
- package/dist/src/core/furnace-registration-ast.js +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +31 -1
- package/dist/src/core/furnace-validate-helpers.js +101 -18
- package/dist/src/core/furnace-validate-registration.d.ts +1 -1
- package/dist/src/core/furnace-validate-registration.js +1 -1
- package/dist/src/core/mach-error-hints.d.ts +29 -0
- package/dist/src/core/mach-error-hints.js +43 -0
- package/dist/src/core/mach.d.ts +5 -2
- package/dist/src/core/mach.js +31 -4
- package/dist/src/core/marionette-preflight.d.ts +14 -7
- package/dist/src/core/marionette-preflight.js +94 -44
- 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-diff-tag.d.ts +33 -0
- package/dist/src/core/patch-lint-diff-tag.js +83 -0
- package/dist/src/core/patch-lint.js +29 -9
- package/dist/src/types/commands/options.d.ts +25 -0
- package/dist/src/types/commands/patches.d.ts +9 -0
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +13 -2
- 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 {
|
|
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. `"
|
|
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
|
-
|
|
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
|
-
*
|
|
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. `"
|
|
12
|
-
* produces ` //
|
|
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 `//
|
|
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 (
|
|
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
|
|
@@ -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
|
-
/**
|
|
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
|
}>;
|