@hominis/fireforge 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/export-flow.js +14 -6
- package/dist/src/commands/export-shared.js +14 -3
- package/dist/src/commands/lint-per-patch.js +83 -14
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +22 -18
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- package/dist/src/commands/status.js +16 -5
- package/dist/src/commands/test-run.js +18 -3
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/config-validate.js +17 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +1 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +19 -1
- package/dist/src/core/patch-lint-checkjs.js +56 -3
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +30 -2
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +33 -2
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Verification-tree storage and lifecycle for `fireforge tree` (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* A tree is a FULL project-root CoW snapshot under
|
|
6
|
+
* `.fireforge/trees/<name>` — `getProjectRoot()` walks up from cwd, so
|
|
7
|
+
* every read command (status, lint, typecheck, verify, doctor,
|
|
8
|
+
* `export --dry-run`) runs inside the tree unmodified and its build /
|
|
9
|
+
* engine-session locks key on the tree root automatically. Trees are
|
|
10
|
+
* snapshots by design: there is no merge-back model, mutation commands
|
|
11
|
+
* are refused via the tree marker (see `tree-guard.ts`), and refresh is
|
|
12
|
+
* `tree remove` + `tree create`.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { readdir, readFile, rm } from 'node:fs/promises';
|
|
16
|
+
import { basename, join, resolve, sep } from 'node:path';
|
|
17
|
+
import { GeneralError } from '../errors/base.js';
|
|
18
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
19
|
+
import { verbose } from '../utils/logger.js';
|
|
20
|
+
import { withFileLock } from './file-lock.js';
|
|
21
|
+
import { git } from './git-base.js';
|
|
22
|
+
import { cloneEntry } from './tree-cow.js';
|
|
23
|
+
const TREE_MARKER_FILENAME = 'tree.json';
|
|
24
|
+
const TREES_DIRNAME = 'trees';
|
|
25
|
+
/**
|
|
26
|
+
* Top-level entries never cloned into a tree: per-tree state must start
|
|
27
|
+
* clean (`.fireforge/`), locks are never inherited, the destructive-
|
|
28
|
+
* operation audit log stays primary-only, and dependency dirs are
|
|
29
|
+
* neither needed nor cheap.
|
|
30
|
+
*/
|
|
31
|
+
function isExcludedRootEntry(name) {
|
|
32
|
+
return (name === '.fireforge' ||
|
|
33
|
+
name === 'node_modules' ||
|
|
34
|
+
name === '.fireforge-history.jsonl' ||
|
|
35
|
+
name.endsWith('.lock'));
|
|
36
|
+
}
|
|
37
|
+
/** Absolute trees directory for a primary root. */
|
|
38
|
+
export function getTreesDir(primaryRoot) {
|
|
39
|
+
return join(primaryRoot, '.fireforge', TREES_DIRNAME);
|
|
40
|
+
}
|
|
41
|
+
/** Serialises tree create/remove against each other (primary-side sidecar lock). */
|
|
42
|
+
export async function withTreeLifecycleLock(primaryRoot, operation) {
|
|
43
|
+
return withFileLock(join(primaryRoot, '.fireforge', 'trees.lock'), operation, {
|
|
44
|
+
timeoutMs: 30_000,
|
|
45
|
+
onTimeoutMessage: 'Another fireforge tree create/remove is in progress. Wait for it to finish, then retry.',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Reads the tree marker for a project root, or undefined when not a tree. */
|
|
49
|
+
export async function readTreeMarker(root) {
|
|
50
|
+
const markerPath = join(root, '.fireforge', TREE_MARKER_FILENAME);
|
|
51
|
+
if (!(await pathExists(markerPath)))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readJson(markerPath);
|
|
55
|
+
if (raw.schemaVersion !== 1 || typeof raw.name !== 'string')
|
|
56
|
+
return undefined;
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
verbose(`Unreadable tree marker at ${markerPath}: ${String(error)}`);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Current primary-side snapshot identity (engine HEAD + patches.json hash). */
|
|
65
|
+
export async function computePrimaryFingerprint(primaryRoot) {
|
|
66
|
+
let engineHead;
|
|
67
|
+
try {
|
|
68
|
+
engineHead = (await git(['rev-parse', 'HEAD'], join(primaryRoot, 'engine'))).trim();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
engineHead = null;
|
|
72
|
+
}
|
|
73
|
+
const patchesJson = join(primaryRoot, 'patches', 'patches.json');
|
|
74
|
+
const patchesFingerprint = (await pathExists(patchesJson))
|
|
75
|
+
? createHash('sha256')
|
|
76
|
+
.update(await readFile(patchesJson))
|
|
77
|
+
.digest('hex')
|
|
78
|
+
: null;
|
|
79
|
+
return { engineHead, patchesFingerprint };
|
|
80
|
+
}
|
|
81
|
+
/** Validates a tree name: one path segment, no traversal. */
|
|
82
|
+
export function assertValidTreeName(name) {
|
|
83
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes('..')) {
|
|
84
|
+
throw new GeneralError(`Invalid tree name "${name}". Use a single path segment of letters, digits, ".", "_" or "-".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Materialises the tree at `treeRoot` from `primaryRoot` using
|
|
89
|
+
* `capability` ('none' = plain copy, already gated on --force-copy by
|
|
90
|
+
* the caller). The engine tree is cloned whole and its `obj-*` build
|
|
91
|
+
* directories removed afterwards — CoW removal is cheap, and per-entry
|
|
92
|
+
* recursion into a Firefox tree is fragile. A fresh `.fireforge/` is
|
|
93
|
+
* created with only `state.json` (when present) and the tree marker.
|
|
94
|
+
*/
|
|
95
|
+
export async function cloneTree(args) {
|
|
96
|
+
const { primaryRoot, treeRoot, name, capability, createdAt } = args;
|
|
97
|
+
const { mkdir, cp } = await import('node:fs/promises');
|
|
98
|
+
await mkdir(treeRoot, { recursive: true });
|
|
99
|
+
for (const entry of await readdir(primaryRoot)) {
|
|
100
|
+
if (isExcludedRootEntry(entry))
|
|
101
|
+
continue;
|
|
102
|
+
await cloneEntry(capability, join(primaryRoot, entry), join(treeRoot, entry));
|
|
103
|
+
}
|
|
104
|
+
// A cloned objdir embeds absolute topsrcdir/topobjdir paths back into
|
|
105
|
+
// the PRIMARY tree (mach-build-artifacts.ts parses exactly those), so
|
|
106
|
+
// in-tree builds/tests would silently operate against the primary —
|
|
107
|
+
// remove the clones; `tree` is read-verification only in 0.40.0.
|
|
108
|
+
const engineDir = join(treeRoot, 'engine');
|
|
109
|
+
if (await pathExists(engineDir)) {
|
|
110
|
+
for (const entry of await readdir(engineDir)) {
|
|
111
|
+
if (entry.startsWith('obj-')) {
|
|
112
|
+
await rm(join(engineDir, entry), { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Defensive: never inherit a mid-operation git index lock.
|
|
116
|
+
await rm(join(engineDir, '.git', 'index.lock'), { force: true });
|
|
117
|
+
}
|
|
118
|
+
await mkdir(join(treeRoot, '.fireforge'), { recursive: true });
|
|
119
|
+
const stateJson = join(primaryRoot, '.fireforge', 'state.json');
|
|
120
|
+
if (await pathExists(stateJson)) {
|
|
121
|
+
await cp(stateJson, join(treeRoot, '.fireforge', 'state.json'));
|
|
122
|
+
}
|
|
123
|
+
const { engineHead, patchesFingerprint } = await computePrimaryFingerprint(primaryRoot);
|
|
124
|
+
const marker = {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
name,
|
|
127
|
+
primaryRoot,
|
|
128
|
+
createdAt,
|
|
129
|
+
engineHead,
|
|
130
|
+
patchesFingerprint,
|
|
131
|
+
};
|
|
132
|
+
await writeJson(join(treeRoot, '.fireforge', TREE_MARKER_FILENAME), marker);
|
|
133
|
+
return marker;
|
|
134
|
+
}
|
|
135
|
+
/** Lists trees with staleness against the current primary state. */
|
|
136
|
+
export async function listTrees(primaryRoot) {
|
|
137
|
+
const treesDir = getTreesDir(primaryRoot);
|
|
138
|
+
if (!(await pathExists(treesDir)))
|
|
139
|
+
return [];
|
|
140
|
+
const current = await computePrimaryFingerprint(primaryRoot);
|
|
141
|
+
const entries = [];
|
|
142
|
+
for (const name of (await readdir(treesDir)).sort((a, b) => a.localeCompare(b))) {
|
|
143
|
+
if (name.endsWith('.lock'))
|
|
144
|
+
continue;
|
|
145
|
+
const path = join(treesDir, name);
|
|
146
|
+
const marker = await readTreeMarker(path);
|
|
147
|
+
if (!marker) {
|
|
148
|
+
entries.push({ name, path, createdAt: '(no marker)', staleness: 'unknown' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const staleness = marker.engineHead !== current.engineHead
|
|
152
|
+
? 'stale (engine advanced)'
|
|
153
|
+
: marker.patchesFingerprint !== current.patchesFingerprint
|
|
154
|
+
? 'stale (patches changed)'
|
|
155
|
+
: 'fresh';
|
|
156
|
+
entries.push({ name, path, createdAt: marker.createdAt, staleness });
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
/** Uses `kill(pid, 0)` — no signal sent, just an existence check. */
|
|
161
|
+
function isProcessAlive(pid) {
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 0);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Returns the live holder PID of a mkdir-style lock directory, if any.
|
|
172
|
+
* Reads the lock's `pid` file directly (line 1 = PID — the format
|
|
173
|
+
* `file-lock.ts` documents as stable) instead of exporting file-lock
|
|
174
|
+
* internals.
|
|
175
|
+
*/
|
|
176
|
+
async function liveLockHolder(lockPath) {
|
|
177
|
+
try {
|
|
178
|
+
const pidLine = (await readFile(join(lockPath, 'pid'), 'utf-8')).split('\n')[0] ?? '';
|
|
179
|
+
const pid = parseInt(pidLine.trim(), 10);
|
|
180
|
+
if (Number.isFinite(pid) && isProcessAlive(pid))
|
|
181
|
+
return pid;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// No lock / unreadable pid file — treat as not held.
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Removes one tree. Refuses when a live process holds the tree's build
|
|
190
|
+
* or engine-session lock, and never deletes a path outside the trees
|
|
191
|
+
* directory (containment check before `rm -rf`).
|
|
192
|
+
*/
|
|
193
|
+
export async function removeTree(primaryRoot, name) {
|
|
194
|
+
assertValidTreeName(name);
|
|
195
|
+
const treesDir = getTreesDir(primaryRoot);
|
|
196
|
+
const treeRoot = resolve(treesDir, name);
|
|
197
|
+
if (!treeRoot.startsWith(treesDir + sep) || basename(treeRoot) !== name) {
|
|
198
|
+
throw new GeneralError(`Refusing to remove "${name}": resolved path escapes the trees directory.`);
|
|
199
|
+
}
|
|
200
|
+
if (!(await pathExists(treeRoot))) {
|
|
201
|
+
throw new GeneralError(`No verification tree named "${name}" exists under ${treesDir}.`);
|
|
202
|
+
}
|
|
203
|
+
for (const lockPath of [
|
|
204
|
+
join(treeRoot, '.fireforge-build.lock'),
|
|
205
|
+
join(treeRoot, '.fireforge', 'engine-session.lock'),
|
|
206
|
+
]) {
|
|
207
|
+
const holder = await liveLockHolder(lockPath);
|
|
208
|
+
if (holder !== undefined) {
|
|
209
|
+
throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(holder)}) holds ${lockPath}. ` +
|
|
210
|
+
'Wait for it to finish, then retry.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await rm(treeRoot, { recursive: true, force: true });
|
|
214
|
+
}
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Filename used for the synthetic Firefox-globals shim source file. */
|
|
13
13
|
export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Loose declarations for Firefox test-harness globals (mochitest
|
|
16
|
+
* browser-chrome and xpcshell), appended AFTER the composed
|
|
17
|
+
* Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
|
|
18
|
+
* extends the checkJs pass to patch-adopted test scripts (FORGE G5).
|
|
19
|
+
* Deliberately `any`-typed — the pragmatic posture matches the main
|
|
20
|
+
* shim. A consumer that wants TYPED harness members (so e.g. a call to
|
|
21
|
+
* a method the harness does not declare fails at the patch boundary)
|
|
22
|
+
* declares them in `patchLint.checkJsTestShim`; because that shim
|
|
23
|
+
* composes BEFORE this baseline and TypeScript resolves conflicting
|
|
24
|
+
* `declare var` redeclarations to the FIRST declaration, the typed
|
|
25
|
+
* consumer declaration wins over the loose fallback here.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TEST_HARNESS_SHIM = "\n// \u2500\u2500 FireForge test-harness shim (loose baseline) \u2500\u2500\ndeclare var TestUtils: any;\ndeclare var BrowserTestUtils: any;\ndeclare var SpecialPowers: any;\ndeclare var EventUtils: any;\ndeclare var SimpleTest: any;\ndeclare var Assert: any;\ndeclare var gTestPath: string;\ndeclare var content: any;\ndeclare function add_task(task: (...args: any[]) => any): any;\ndeclare function add_setup(task: (...args: any[]) => any): any;\ndeclare function ok(condition: any, message?: string): void;\ndeclare function is(actual: any, expected: any, message?: string): void;\ndeclare function isnot(actual: any, expected: any, message?: string): void;\ndeclare function todo(condition: any, message?: string): void;\ndeclare function todo_is(actual: any, expected: any, message?: string): void;\ndeclare function info(message: string): void;\ndeclare function registerCleanupFunction(callback: (...args: any[]) => any): void;\ndeclare function waitForExplicitFinish(): void;\ndeclare function finish(): void;\ndeclare function do_get_profile(): any;\ndeclare function run_test(): any;\n\n";
|
|
14
28
|
/**
|
|
15
29
|
* TS diagnostic codes suppressed by both the patch-lint checkJs pass
|
|
16
30
|
* and the whole-project typecheck command. Each is a known false
|
|
@@ -128,6 +128,44 @@ declare var Localization: LocalizationShim;
|
|
|
128
128
|
declare module 'resource:*';
|
|
129
129
|
declare module 'chrome:*';
|
|
130
130
|
|
|
131
|
+
`;
|
|
132
|
+
/**
|
|
133
|
+
* Loose declarations for Firefox test-harness globals (mochitest
|
|
134
|
+
* browser-chrome and xpcshell), appended AFTER the composed
|
|
135
|
+
* Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
|
|
136
|
+
* extends the checkJs pass to patch-adopted test scripts (FORGE G5).
|
|
137
|
+
* Deliberately `any`-typed — the pragmatic posture matches the main
|
|
138
|
+
* shim. A consumer that wants TYPED harness members (so e.g. a call to
|
|
139
|
+
* a method the harness does not declare fails at the patch boundary)
|
|
140
|
+
* declares them in `patchLint.checkJsTestShim`; because that shim
|
|
141
|
+
* composes BEFORE this baseline and TypeScript resolves conflicting
|
|
142
|
+
* `declare var` redeclarations to the FIRST declaration, the typed
|
|
143
|
+
* consumer declaration wins over the loose fallback here.
|
|
144
|
+
*/
|
|
145
|
+
export const TEST_HARNESS_SHIM = `
|
|
146
|
+
// ── FireForge test-harness shim (loose baseline) ──
|
|
147
|
+
declare var TestUtils: any;
|
|
148
|
+
declare var BrowserTestUtils: any;
|
|
149
|
+
declare var SpecialPowers: any;
|
|
150
|
+
declare var EventUtils: any;
|
|
151
|
+
declare var SimpleTest: any;
|
|
152
|
+
declare var Assert: any;
|
|
153
|
+
declare var gTestPath: string;
|
|
154
|
+
declare var content: any;
|
|
155
|
+
declare function add_task(task: (...args: any[]) => any): any;
|
|
156
|
+
declare function add_setup(task: (...args: any[]) => any): any;
|
|
157
|
+
declare function ok(condition: any, message?: string): void;
|
|
158
|
+
declare function is(actual: any, expected: any, message?: string): void;
|
|
159
|
+
declare function isnot(actual: any, expected: any, message?: string): void;
|
|
160
|
+
declare function todo(condition: any, message?: string): void;
|
|
161
|
+
declare function todo_is(actual: any, expected: any, message?: string): void;
|
|
162
|
+
declare function info(message: string): void;
|
|
163
|
+
declare function registerCleanupFunction(callback: (...args: any[]) => any): void;
|
|
164
|
+
declare function waitForExplicitFinish(): void;
|
|
165
|
+
declare function finish(): void;
|
|
166
|
+
declare function do_get_profile(): any;
|
|
167
|
+
declare function run_test(): any;
|
|
168
|
+
|
|
131
169
|
`;
|
|
132
170
|
/**
|
|
133
171
|
* TS diagnostic codes suppressed by both the patch-lint checkJs pass
|
|
@@ -226,8 +264,9 @@ async function inlineTripleSlashReferences(source, baseDir, seen) {
|
|
|
226
264
|
* @param extraShimPath - Optional project-relative path to an extra `.d.ts`
|
|
227
265
|
*/
|
|
228
266
|
export async function composeShimSource(projectRoot, extraShimPath) {
|
|
267
|
+
const base = FIREFOX_GLOBALS_SHIM;
|
|
229
268
|
if (!extraShimPath) {
|
|
230
|
-
return { source:
|
|
269
|
+
return { source: base, extraShimAppended: false };
|
|
231
270
|
}
|
|
232
271
|
const absoluteShim = resolve(projectRoot, extraShimPath);
|
|
233
272
|
if (!(await pathExists(absoluteShim))) {
|
|
@@ -237,7 +276,7 @@ export async function composeShimSource(projectRoot, extraShimPath) {
|
|
|
237
276
|
const extra = await readText(absoluteShim);
|
|
238
277
|
const inlinedExtra = await inlineTripleSlashReferences(extra, dirname(absoluteShim), new Set([absoluteShim]));
|
|
239
278
|
return {
|
|
240
|
-
source: `${
|
|
279
|
+
source: `${base}\n// ── extraShim: ${extraShimPath} ──\n${inlinedExtra}`,
|
|
241
280
|
extraShimAppended: true,
|
|
242
281
|
};
|
|
243
282
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const XPCSHELL_PROFILE_ENV_VAR = "XPCSHELL_TEST_PROFILE_DIR";
|
|
2
|
+
/**
|
|
3
|
+
* Runs `fn` with a fresh per-invocation xpcshell profile directory
|
|
4
|
+
* injected into the environment, removing the directory afterwards
|
|
5
|
+
* (best-effort — a failure to clean up warns and leaves the mkdtemp dir
|
|
6
|
+
* for the OS tmp reaper; the harness may still hold files briefly).
|
|
7
|
+
*
|
|
8
|
+
* An operator-provided `XPCSHELL_TEST_PROFILE_DIR` (in `baseEnv` or the
|
|
9
|
+
* process environment) is respected verbatim: no directory is minted and
|
|
10
|
+
* NOTHING is deleted — we never remove a directory we did not create.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withXpcshellProfileDir<T>(baseEnv: Record<string, string> | undefined, fn: (env: Record<string, string>) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Per-invocation xpcshell profile directory (FORGE G15b).
|
|
4
|
+
*
|
|
5
|
+
* Firefox's xpcshell harness defaults `XPCSHELL_TEST_PROFILE_DIR` to a
|
|
6
|
+
* FIXED path (`$TMPDIR/firefox/xpcshellprofile`), so two overlapping
|
|
7
|
+
* harness invocations — concurrent `fireforge test` processes, or future
|
|
8
|
+
* verification-tree shards — corrupt each other's profiles. FireForge
|
|
9
|
+
* never set the variable; this helper mints a fresh `mkdtemp` directory
|
|
10
|
+
* per harness invocation and exports it through the mach env channel.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { toError } from '../utils/errors.js';
|
|
16
|
+
import { warn } from '../utils/logger.js';
|
|
17
|
+
export const XPCSHELL_PROFILE_ENV_VAR = 'XPCSHELL_TEST_PROFILE_DIR';
|
|
18
|
+
/**
|
|
19
|
+
* Runs `fn` with a fresh per-invocation xpcshell profile directory
|
|
20
|
+
* injected into the environment, removing the directory afterwards
|
|
21
|
+
* (best-effort — a failure to clean up warns and leaves the mkdtemp dir
|
|
22
|
+
* for the OS tmp reaper; the harness may still hold files briefly).
|
|
23
|
+
*
|
|
24
|
+
* An operator-provided `XPCSHELL_TEST_PROFILE_DIR` (in `baseEnv` or the
|
|
25
|
+
* process environment) is respected verbatim: no directory is minted and
|
|
26
|
+
* NOTHING is deleted — we never remove a directory we did not create.
|
|
27
|
+
*/
|
|
28
|
+
export async function withXpcshellProfileDir(baseEnv, fn) {
|
|
29
|
+
const operatorProvided = baseEnv?.[XPCSHELL_PROFILE_ENV_VAR] ?? process.env[XPCSHELL_PROFILE_ENV_VAR];
|
|
30
|
+
if (operatorProvided !== undefined && operatorProvided !== '') {
|
|
31
|
+
return fn({ ...baseEnv, [XPCSHELL_PROFILE_ENV_VAR]: operatorProvided });
|
|
32
|
+
}
|
|
33
|
+
const profileDir = await mkdtemp(join(tmpdir(), 'fireforge-xpcshell-profile-'));
|
|
34
|
+
try {
|
|
35
|
+
return await fn({ ...baseEnv, [XPCSHELL_PROFILE_ENV_VAR]: profileDir });
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
try {
|
|
39
|
+
await rm(profileDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
warn(`Could not clean up xpcshell profile dir ${profileDir}: ${toError(error).message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/src/errors/git.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export declare class GitIndexLockError extends GitError {
|
|
|
39
39
|
constructor(lockPath: string, ageMs?: number | undefined);
|
|
40
40
|
get userMessage(): string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Detects transient git `index.lock` contention — an external git process
|
|
44
|
+
* holding `.git/index.lock` while we stage or diff. Mirrors the message
|
|
45
|
+
* heuristics `core/git.ts` uses when wrapping stale-lock failures during
|
|
46
|
+
* download, but lives here so command modules can consult it without
|
|
47
|
+
* importing (frequently vi.mocked) core git internals.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isGitIndexLockConflict(error: unknown): boolean;
|
|
42
50
|
/**
|
|
43
51
|
* Error thrown when `git add` (monolithic or chunked) exceeds the
|
|
44
52
|
* configured timeout while indexing the Firefox source tree.
|
package/dist/src/errors/git.js
CHANGED
|
@@ -96,6 +96,23 @@ export class GitIndexLockError extends GitError {
|
|
|
96
96
|
' 3. Re-run "fireforge download --force"');
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Detects transient git `index.lock` contention — an external git process
|
|
101
|
+
* holding `.git/index.lock` while we stage or diff. Mirrors the message
|
|
102
|
+
* heuristics `core/git.ts` uses when wrapping stale-lock failures during
|
|
103
|
+
* download, but lives here so command modules can consult it without
|
|
104
|
+
* importing (frequently vi.mocked) core git internals.
|
|
105
|
+
*/
|
|
106
|
+
export function isGitIndexLockConflict(error) {
|
|
107
|
+
if (error instanceof GitIndexLockError) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (!(error instanceof GitError)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return (/index\.lock/i.test(error.message) &&
|
|
114
|
+
/(unable to create|another git process seems to be running|file exists|locked)/i.test(error.message));
|
|
115
|
+
}
|
|
99
116
|
/**
|
|
100
117
|
* Error thrown when `git add` (monolithic or chunked) exceeds the
|
|
101
118
|
* configured timeout while indexing the Firefox source tree.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { loadConfig, validateConfig } from './core/config.js';
|
|
|
14
14
|
export { applyAllComponents } from './core/furnace-apply.js';
|
|
15
15
|
export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
|
|
16
16
|
export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
|
|
17
|
+
export type { PatchSizeTierDecision } from './core/patch-lint.js';
|
|
18
|
+
export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
|
|
17
19
|
export type { AddTokenOptions, AddTokenResult, TokenMode } from './core/token-manager.js';
|
|
18
20
|
export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
|
|
19
21
|
export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
|
package/dist/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { loadConfig, validateConfig } from './core/config.js';
|
|
|
15
15
|
export { applyAllComponents } from './core/furnace-apply.js';
|
|
16
16
|
export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
|
|
17
17
|
export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
|
|
18
|
+
export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
|
|
18
19
|
export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
|
|
19
20
|
export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
|
|
20
21
|
export { ExitCode } from './errors/codes.js';
|
|
@@ -279,6 +279,16 @@ export interface ReExportOptions {
|
|
|
279
279
|
* the command implementation ignores it.
|
|
280
280
|
*/
|
|
281
281
|
waitLock?: number | boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Refuse (non-zero exit, patch not written) a scan-less re-export when
|
|
284
|
+
* unmanaged files exist adjacent to the patch's ownership, instead of
|
|
285
|
+
* warning. Gate-driven workflows use this so a freshly created file
|
|
286
|
+
* beside a patch's owned files cannot be silently left out of the
|
|
287
|
+
* refreshed body. Only meaningful on the plain path — mutually
|
|
288
|
+
* exclusive with `--scan` and `--files`, which set filesAffected
|
|
289
|
+
* explicitly.
|
|
290
|
+
*/
|
|
291
|
+
refuseAdjacentUnmanaged?: boolean;
|
|
282
292
|
}
|
|
283
293
|
/**
|
|
284
294
|
* Options for the `fireforge patch tier` subcommand. Sets or clears the
|
|
@@ -809,6 +819,19 @@ export interface StatusOptions {
|
|
|
809
819
|
testCoverage?: boolean;
|
|
810
820
|
/** Output machine-readable JSON instead of human-readable text. */
|
|
811
821
|
json?: boolean;
|
|
822
|
+
/**
|
|
823
|
+
* Exit non-zero when any classification in the fail policy is
|
|
824
|
+
* non-empty (default policy: unmanaged, patch-owned-drift, conflict).
|
|
825
|
+
* Composes with the default view and `--json`; refused alongside
|
|
826
|
+
* `--raw`/`--unmanaged`/`--ownership`/`--test-coverage` (FORGE G1).
|
|
827
|
+
*/
|
|
828
|
+
check?: boolean;
|
|
829
|
+
/**
|
|
830
|
+
* Comma-separated classification list replacing the default `--check`
|
|
831
|
+
* policy. Implies `--check`. Unknown values refuse, naming the valid
|
|
832
|
+
* classification set.
|
|
833
|
+
*/
|
|
834
|
+
failOn?: string;
|
|
812
835
|
}
|
|
813
836
|
/**
|
|
814
837
|
* Options for the token add command.
|
|
@@ -900,8 +923,9 @@ export interface LintCommandOptions {
|
|
|
900
923
|
* the cross-patch rules once over the whole queue so queue-level
|
|
901
924
|
* findings (duplicate creations, forward imports) still surface.
|
|
902
925
|
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
926
|
+
* Positional file arguments change meaning under this flag: they are
|
|
927
|
+
* PATCH selectors resolved like {@link patches} entries, not engine
|
|
928
|
+
* file paths (FORGE G14).
|
|
905
929
|
*/
|
|
906
930
|
perPatch?: boolean;
|
|
907
931
|
/**
|
|
@@ -923,4 +947,11 @@ export interface LintCommandOptions {
|
|
|
923
947
|
* for CLI consistency, but only `--per-patch` currently uses the cache.
|
|
924
948
|
*/
|
|
925
949
|
noCache?: boolean;
|
|
950
|
+
/**
|
|
951
|
+
* With `--per-patch`, write a machine-readable JSON report (schemaVersion
|
|
952
|
+
* 1: per-patch lineCount, filesAffected, tier, thresholds, issues, and
|
|
953
|
+
* lintIgnore-suppressed issues) to this path (FORGE G9/G10). Requires
|
|
954
|
+
* {@link perPatch}.
|
|
955
|
+
*/
|
|
956
|
+
report?: string;
|
|
926
957
|
}
|
|
@@ -253,6 +253,25 @@ export interface PatchLintConfig {
|
|
|
253
253
|
* behaviour (built-in shim only).
|
|
254
254
|
*/
|
|
255
255
|
checkJsExtraShim?: string;
|
|
256
|
+
/**
|
|
257
|
+
* Extend the checkJs pass to patch-owned test `.js` files
|
|
258
|
+
* (`browser_*` / `test_*` / `xpcshell_*` basenames and files under a
|
|
259
|
+
* `/test/` path), each checked as its own small script-scope program
|
|
260
|
+
* with same-directory patch-owned `head*.js` helpers included (FORGE
|
|
261
|
+
* G5 — a harness global that does not exist should fail at the patch
|
|
262
|
+
* boundary where the test was authored, not in a downstream
|
|
263
|
+
* jsconfig-project run). Opt-in: it is a new failure surface with
|
|
264
|
+
* nonzero compile cost. Requires `checkJs: true`.
|
|
265
|
+
*/
|
|
266
|
+
checkJsTestFiles?: boolean;
|
|
267
|
+
/**
|
|
268
|
+
* Project-relative `.d.ts` appended to the built-in test-harness shim
|
|
269
|
+
* (loose `TestUtils`/`BrowserTestUtils`/`add_task`/… declarations) for
|
|
270
|
+
* the `checkJsTestFiles` pass — a consumer-typed `TestUtils` here is
|
|
271
|
+
* what turns a call to a nonexistent harness member into a TS2339 at
|
|
272
|
+
* export time. Requires `checkJsTestFiles: true`.
|
|
273
|
+
*/
|
|
274
|
+
checkJsTestShim?: string;
|
|
256
275
|
/** File paths exempt from the raw-color-value check (exact or basename match) */
|
|
257
276
|
rawColorAllowlist?: string[];
|
|
258
277
|
/** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
|
|
@@ -128,6 +128,16 @@ export declare function validateTokenName(name: string): string | undefined;
|
|
|
128
128
|
* @throws InvalidArgumentError if the resulting name is not a valid CSS custom property.
|
|
129
129
|
*/
|
|
130
130
|
export declare function normalizeTokenName(name: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* Normalizes a patch display name against its category (FORGE G13):
|
|
133
|
+
* strips a trailing `.patch` and any redundant `NNN-<category>-` /
|
|
134
|
+
* `<category>-` prefixes, case-insensitively and repeatedly, mirroring
|
|
135
|
+
* the filename slug pipeline's require-the-category-token rule — a bare
|
|
136
|
+
* leading number is never stripped, so names like `2-step-verification`
|
|
137
|
+
* survive intact. Falls back to the `.patch`-stripped stem when the
|
|
138
|
+
* strip would empty the name.
|
|
139
|
+
*/
|
|
140
|
+
export declare function normalizePatchDisplayName(name: string, category: string): string;
|
|
131
141
|
/**
|
|
132
142
|
* Validates a patch name.
|
|
133
143
|
* @param name - The patch name to validate
|
|
@@ -251,6 +251,35 @@ export function normalizeTokenName(name) {
|
|
|
251
251
|
const stripped = name.replace(/^--/, '');
|
|
252
252
|
return `--${stripped}`;
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Normalizes a patch display name against its category (FORGE G13):
|
|
256
|
+
* strips a trailing `.patch` and any redundant `NNN-<category>-` /
|
|
257
|
+
* `<category>-` prefixes, case-insensitively and repeatedly, mirroring
|
|
258
|
+
* the filename slug pipeline's require-the-category-token rule — a bare
|
|
259
|
+
* leading number is never stripped, so names like `2-step-verification`
|
|
260
|
+
* survive intact. Falls back to the `.patch`-stripped stem when the
|
|
261
|
+
* strip would empty the name.
|
|
262
|
+
*/
|
|
263
|
+
export function normalizePatchDisplayName(name, category) {
|
|
264
|
+
const stem = name.trim().replace(/\.patch$/i, '');
|
|
265
|
+
const escaped = category.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
266
|
+
const prefixes = [
|
|
267
|
+
new RegExp(`^\\d+[-_ ]+${escaped}[-_ ]+`, 'i'),
|
|
268
|
+
new RegExp(`^${escaped}[-_ ]+`, 'i'),
|
|
269
|
+
];
|
|
270
|
+
let stripped = stem;
|
|
271
|
+
let changed = true;
|
|
272
|
+
while (changed) {
|
|
273
|
+
changed = false;
|
|
274
|
+
for (const prefix of prefixes) {
|
|
275
|
+
if (prefix.test(stripped)) {
|
|
276
|
+
stripped = stripped.replace(prefix, '');
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return stripped.length > 0 ? stripped : stem;
|
|
282
|
+
}
|
|
254
283
|
/**
|
|
255
284
|
* Validates a patch name.
|
|
256
285
|
* @param name - The patch name to validate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"commander": "^15.0.0",
|
|
59
59
|
"estree-walker": "^3.0.3",
|
|
60
60
|
"jsonc-parser": "^3.3.1",
|
|
61
|
-
"magic-string": "^
|
|
61
|
+
"magic-string": "^1.1.0",
|
|
62
62
|
"picocolors": "^1.1.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -66,16 +66,16 @@
|
|
|
66
66
|
"@types/estree": "^1.0.8",
|
|
67
67
|
"@types/node": "^26.0.0",
|
|
68
68
|
"@vitest/coverage-v8": "^4.1.2",
|
|
69
|
-
"dpdm": "4.
|
|
69
|
+
"dpdm": "4.3.0",
|
|
70
70
|
"eslint": "^10.0.0",
|
|
71
71
|
"eslint-config-prettier": "^10.1.8",
|
|
72
72
|
"eslint-plugin-jsdoc": "^63.0.0",
|
|
73
|
-
"eslint-plugin-simple-import-sort": "^
|
|
73
|
+
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
74
74
|
"fast-check": "^4.6.0",
|
|
75
75
|
"husky": "^9.1.7",
|
|
76
|
-
"knip": "6.
|
|
76
|
+
"knip": "6.29.0",
|
|
77
77
|
"lint-staged": "^17.0.4",
|
|
78
|
-
"prettier": "3.9.
|
|
78
|
+
"prettier": "3.9.6",
|
|
79
79
|
"tsx": "^4.7.0",
|
|
80
80
|
"typescript": "~6.0.0",
|
|
81
81
|
"typescript-eslint": "^8.0.0",
|