@hominis/fireforge 0.17.0 → 0.18.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +40 -20
  3. package/dist/src/commands/build.js +18 -4
  4. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
  5. package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
  6. package/dist/src/commands/doctor-furnace.js +2 -0
  7. package/dist/src/commands/doctor-working-tree.d.ts +29 -0
  8. package/dist/src/commands/doctor-working-tree.js +93 -0
  9. package/dist/src/commands/doctor.js +9 -11
  10. package/dist/src/commands/export-all.js +11 -3
  11. package/dist/src/commands/export-shared.d.ts +7 -1
  12. package/dist/src/commands/export-shared.js +21 -3
  13. package/dist/src/commands/furnace/override.js +23 -13
  14. package/dist/src/commands/furnace/remove.js +8 -0
  15. package/dist/src/commands/furnace/rename.js +23 -4
  16. package/dist/src/commands/lint.js +19 -6
  17. package/dist/src/commands/patch/delete.js +4 -1
  18. package/dist/src/commands/patch/reorder.js +4 -1
  19. package/dist/src/commands/re-export-files.js +3 -1
  20. package/dist/src/commands/re-export.js +4 -1
  21. package/dist/src/commands/register.js +11 -0
  22. package/dist/src/commands/test.js +53 -12
  23. package/dist/src/commands/token-coverage.js +10 -3
  24. package/dist/src/commands/wire.js +16 -0
  25. package/dist/src/core/browser-wire.js +21 -4
  26. package/dist/src/core/build-audit.js +10 -0
  27. package/dist/src/core/git-diff.js +21 -2
  28. package/dist/src/core/mach.d.ts +12 -6
  29. package/dist/src/core/mach.js +12 -6
  30. package/dist/src/core/manifest-rules.js +10 -1
  31. package/dist/src/core/manifest-tokenizers.d.ts +6 -0
  32. package/dist/src/core/manifest-tokenizers.js +28 -0
  33. package/dist/src/core/patch-lint.d.ts +47 -2
  34. package/dist/src/core/patch-lint.js +89 -14
  35. package/dist/src/core/patch-manifest-consistency.js +15 -2
  36. package/dist/src/core/patch-manifest-io.js +10 -0
  37. package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
  38. package/dist/src/core/patch-manifest-resolve.js +29 -2
  39. package/dist/src/core/patch-manifest-validate.js +25 -1
  40. package/dist/src/core/token-coverage.js +24 -0
  41. package/dist/src/core/wire-destroy.d.ts +7 -3
  42. package/dist/src/core/wire-destroy.js +11 -6
  43. package/dist/src/core/wire-init.d.ts +9 -3
  44. package/dist/src/core/wire-init.js +18 -6
  45. package/dist/src/core/wire-subscript.d.ts +7 -3
  46. package/dist/src/core/wire-subscript.js +11 -4
  47. package/dist/src/types/commands/patches.d.ts +23 -0
  48. package/dist/src/types/furnace.d.ts +9 -0
  49. package/dist/src/utils/parse.d.ts +7 -0
  50. package/dist/src/utils/parse.js +15 -0
  51. package/package.json +1 -1
@@ -12,11 +12,16 @@ import { detectIndent, parseScript } from './ast-utils.js';
12
12
  import { withParserFallback } from './parser-fallback.js';
13
13
  import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findMethodBody, findMethodBraceIndex, validateWireName, } from './wire-utils.js';
14
14
  const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
15
+ const DEFAULT_MARKER = 'FIREFORGE:';
15
16
  /**
16
17
  * AST-based implementation: finds onUnload()/uninit() method body and
17
18
  * inserts the destroy block at the top (LIFO ordering).
19
+ *
20
+ * `marker` is prefixed to the generated comment so wire-generated
21
+ * edits carry the patch-lint `// <MARKER>:` signature
22
+ * `lintModificationComments` looks for (eval 1 Finding #9).
18
23
  */
19
- export function addDestroyAST(content, expression) {
24
+ export function addDestroyAST(content, expression, marker = DEFAULT_MARKER) {
20
25
  const name = extractNameFromExpression(expression);
21
26
  // See wire-init.ts for the rationale: the template interpolates the
22
27
  // expression verbatim, so a bare `Foo.bar` compiled to `Foo.bar;`
@@ -44,7 +49,7 @@ export function addDestroyAST(content, expression) {
44
49
  indent = ' ';
45
50
  }
46
51
  const block = [
47
- `${indent}// ${name} destroy`,
52
+ `${indent}// ${marker} wire-destroy ${name}`,
48
53
  `${indent}try {`,
49
54
  `${indent} if (typeof ${name} !== "undefined") {`,
50
55
  `${indent} ${callExpression};`,
@@ -59,7 +64,7 @@ export function addDestroyAST(content, expression) {
59
64
  /**
60
65
  * Legacy regex/line-based implementation preserved as fallback.
61
66
  */
62
- export function legacyAddDestroy(content, expression) {
67
+ export function legacyAddDestroy(content, expression, marker = DEFAULT_MARKER) {
63
68
  const name = extractNameFromExpression(expression);
64
69
  // Match the AST path on the call-coercion contract so fallback vs AST
65
70
  // emits identical blocks (see wire-init.ts).
@@ -73,7 +78,7 @@ export function legacyAddDestroy(content, expression) {
73
78
  }
74
79
  const insertIndex = found.braceIndex + 1;
75
80
  const block = [
76
- ` // ${name} destroy`,
81
+ ` // ${marker} wire-destroy ${name}`,
77
82
  ` try {`,
78
83
  ` if (typeof ${name} !== "undefined") {`,
79
84
  ` ${callExpression};`,
@@ -93,7 +98,7 @@ export function legacyAddDestroy(content, expression) {
93
98
  * @param expression - The destroy expression (e.g., "MyComponent.destroy()")
94
99
  * @returns true if added, false if already present
95
100
  */
96
- export async function addDestroyToBrowserInit(engineDir, expression) {
101
+ export async function addDestroyToBrowserInit(engineDir, expression, marker = DEFAULT_MARKER) {
97
102
  validateWireName(expression, 'destroy expression');
98
103
  const filePath = join(engineDir, BROWSER_INIT_JS);
99
104
  if (!(await pathExists(filePath))) {
@@ -109,7 +114,7 @@ export async function addDestroyToBrowserInit(engineDir, expression) {
109
114
  if (destroyPattern.test(content)) {
110
115
  return false;
111
116
  }
112
- const { value, usedFallback } = withParserFallback(() => addDestroyAST(content, expression), () => legacyAddDestroy(content, expression), BROWSER_INIT_JS);
117
+ const { value, usedFallback } = withParserFallback(() => addDestroyAST(content, expression, marker), () => legacyAddDestroy(content, expression, marker), BROWSER_INIT_JS);
113
118
  if (usedFallback) {
114
119
  assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
115
120
  }
@@ -5,12 +5,18 @@
5
5
  * AST-based implementation: finds onLoad() method body, locates existing
6
6
  * fireforge init blocks (TryStatements containing typeof guards), and inserts
7
7
  * after the correct position.
8
+ *
9
+ * `marker` is prepended (uppercased) to the generated comment line so the
10
+ * emitted block carries the patch-lint `// <MARKER>:` signature that
11
+ * `lintModificationComments` looks for. Otherwise the first export after
12
+ * `wire` trips `missing-modification-comment` on wire-generated edits —
13
+ * exactly the eval 1 Finding #9 regression.
8
14
  */
9
- export declare function addInitAST(content: string, expression: string, after?: string): string;
15
+ export declare function addInitAST(content: string, expression: string, after?: string, marker?: string): string;
10
16
  /**
11
17
  * Legacy regex/line-based implementation preserved as fallback.
12
18
  */
13
- export declare function legacyAddInit(content: string, expression: string, after?: string): string;
19
+ export declare function legacyAddInit(content: string, expression: string, after?: string, marker?: string): string;
14
20
  /**
15
21
  * Adds an init expression as the first statement(s) in gBrowserInit.onLoad()
16
22
  * in browser-init.js, after any previously-wired fireforge init blocks.
@@ -20,4 +26,4 @@ export declare function legacyAddInit(content: string, expression: string, after
20
26
  * @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
21
27
  * @returns true if added, false if already present
22
28
  */
23
- export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string): Promise<boolean>;
29
+ export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string, marker?: string): Promise<boolean>;
@@ -12,12 +12,24 @@ import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
12
12
  import { withParserFallback } from './parser-fallback.js';
13
13
  import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
14
14
  const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
15
+ /**
16
+ * Default patch-lint marker used when a caller does not supply a
17
+ * project-specific one. Kept as a constant so test fixtures and
18
+ * fallback code paths agree on the shape.
19
+ */
20
+ const DEFAULT_MARKER = 'FIREFORGE:';
15
21
  /**
16
22
  * AST-based implementation: finds onLoad() method body, locates existing
17
23
  * fireforge init blocks (TryStatements containing typeof guards), and inserts
18
24
  * after the correct position.
25
+ *
26
+ * `marker` is prepended (uppercased) to the generated comment line so the
27
+ * emitted block carries the patch-lint `// <MARKER>:` signature that
28
+ * `lintModificationComments` looks for. Otherwise the first export after
29
+ * `wire` trips `missing-modification-comment` on wire-generated edits —
30
+ * exactly the eval 1 Finding #9 regression.
19
31
  */
20
- export function addInitAST(content, expression, after) {
32
+ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER) {
21
33
  const name = extractNameFromExpression(expression);
22
34
  // `validateWireName` accepts both `Foo.bar` and `Foo.bar()` shapes. The
23
35
  // template below interpolates the value verbatim, so a bare property
@@ -99,7 +111,7 @@ export function addInitAST(content, expression, after) {
99
111
  }
100
112
  }
101
113
  const block = [
102
- `${indent}// ${name} init — must be first, before Firefox subsystem`,
114
+ `${indent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
103
115
  `${indent}// inits that reference native UI elements we hide.`,
104
116
  `${indent}try {`,
105
117
  `${indent} if (typeof ${name} !== "undefined") {`,
@@ -115,7 +127,7 @@ export function addInitAST(content, expression, after) {
115
127
  /**
116
128
  * Legacy regex/line-based implementation preserved as fallback.
117
129
  */
118
- export function legacyAddInit(content, expression, after) {
130
+ export function legacyAddInit(content, expression, after, marker = DEFAULT_MARKER) {
119
131
  const name = extractNameFromExpression(expression);
120
132
  // See `addInitAST` for the rationale — the AST and fallback paths must
121
133
  // agree on whether the emitted block is a function call, otherwise
@@ -174,7 +186,7 @@ export function legacyAddInit(content, expression, after) {
174
186
  const inner = baseIndent + ' ';
175
187
  const inner2 = inner + ' ';
176
188
  const block = [
177
- `${baseIndent}// ${name} init — must be first, before Firefox subsystem`,
189
+ `${baseIndent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
178
190
  `${baseIndent}// inits that reference native UI elements we hide.`,
179
191
  `${baseIndent}try {`,
180
192
  `${inner}if (typeof ${name} !== "undefined") {`,
@@ -196,7 +208,7 @@ export function legacyAddInit(content, expression, after) {
196
208
  * @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
197
209
  * @returns true if added, false if already present
198
210
  */
199
- export async function addInitToBrowserInit(engineDir, expression, after) {
211
+ export async function addInitToBrowserInit(engineDir, expression, after, marker = DEFAULT_MARKER) {
200
212
  validateWireName(expression, 'init expression');
201
213
  const filePath = join(engineDir, BROWSER_INIT_JS);
202
214
  if (!(await pathExists(filePath))) {
@@ -212,7 +224,7 @@ export async function addInitToBrowserInit(engineDir, expression, after) {
212
224
  if (initPattern.test(content)) {
213
225
  return false;
214
226
  }
215
- const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after), () => legacyAddInit(content, expression, after), BROWSER_INIT_JS);
227
+ const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS);
216
228
  if (usedFallback) {
217
229
  assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
218
230
  }
@@ -4,12 +4,16 @@
4
4
  /**
5
5
  * AST-based implementation: finds the last try/catch containing
6
6
  * `loadSubScript` and inserts a new try/catch block after it.
7
+ *
8
+ * The inserted block carries a `// <MARKER>: wire-subscript ...` comment
9
+ * so the emitted edit satisfies `lintModificationComments` (eval 1
10
+ * Finding #9).
7
11
  */
8
- export declare function addSubscriptAST(content: string, name: string): string;
12
+ export declare function addSubscriptAST(content: string, name: string, marker?: string): string;
9
13
  /**
10
14
  * Legacy regex/line-based implementation preserved as fallback.
11
15
  */
12
- export declare function legacyAddSubscript(content: string, name: string): string;
16
+ export declare function legacyAddSubscript(content: string, name: string, marker?: string): string;
13
17
  /**
14
18
  * Adds a loadSubScript entry to browser-main.js with try/catch error handling.
15
19
  *
@@ -17,4 +21,4 @@ export declare function legacyAddSubscript(content: string, name: string): strin
17
21
  * @param name - Subscript name (without .js extension)
18
22
  * @returns true if added, false if already present
19
23
  */
20
- export declare function addSubscriptToBrowserMain(engineDir: string, name: string): Promise<boolean>;
24
+ export declare function addSubscriptToBrowserMain(engineDir: string, name: string, marker?: string): Promise<boolean>;
@@ -11,11 +11,16 @@ import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.
11
11
  import { withParserFallback } from './parser-fallback.js';
12
12
  import { assertBraceBalancePreserved, findNearestTryLine, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
13
13
  const BROWSER_MAIN_JS = 'browser/base/content/browser-main.js';
14
+ const DEFAULT_MARKER = 'FIREFORGE:';
14
15
  /**
15
16
  * AST-based implementation: finds the last try/catch containing
16
17
  * `loadSubScript` and inserts a new try/catch block after it.
18
+ *
19
+ * The inserted block carries a `// <MARKER>: wire-subscript ...` comment
20
+ * so the emitted edit satisfies `lintModificationComments` (eval 1
21
+ * Finding #9).
17
22
  */
18
- export function addSubscriptAST(content, name) {
23
+ export function addSubscriptAST(content, name, marker = DEFAULT_MARKER) {
19
24
  const ast = parseScript(content);
20
25
  const ms = new MagicString(content);
21
26
  // Collect all TryStatements containing loadSubScript
@@ -58,6 +63,7 @@ export function addSubscriptAST(content, name) {
58
63
  indent = detectIndent(content, lastBrace);
59
64
  }
60
65
  const block = [
66
+ `${indent}// ${marker} wire-subscript ${name}`,
61
67
  `${indent}try {`,
62
68
  `${indent} Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
63
69
  `${indent}} catch (e) {`,
@@ -70,7 +76,7 @@ export function addSubscriptAST(content, name) {
70
76
  /**
71
77
  * Legacy regex/line-based implementation preserved as fallback.
72
78
  */
73
- export function legacyAddSubscript(content, name) {
79
+ export function legacyAddSubscript(content, name, marker = DEFAULT_MARKER) {
74
80
  const lines = content.split('\n');
75
81
  let lastSubScriptLine = -1;
76
82
  for (let i = 0; i < lines.length; i++) {
@@ -103,6 +109,7 @@ export function legacyAddSubscript(content, name) {
103
109
  const ind = refLine?.match(/^(\s*)/)?.[1] ?? ' ';
104
110
  const inner = ind + ' ';
105
111
  const block = [
112
+ `${ind}// ${marker} wire-subscript ${name}`,
106
113
  `${ind}try {`,
107
114
  `${inner}Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
108
115
  `${ind}} catch (e) {`,
@@ -119,7 +126,7 @@ export function legacyAddSubscript(content, name) {
119
126
  * @param name - Subscript name (without .js extension)
120
127
  * @returns true if added, false if already present
121
128
  */
122
- export async function addSubscriptToBrowserMain(engineDir, name) {
129
+ export async function addSubscriptToBrowserMain(engineDir, name, marker = DEFAULT_MARKER) {
123
130
  validateWireName(name, 'subscript name');
124
131
  const filePath = join(engineDir, BROWSER_MAIN_JS);
125
132
  if (!(await pathExists(filePath))) {
@@ -130,7 +137,7 @@ export async function addSubscriptToBrowserMain(engineDir, name) {
130
137
  if (content.includes(`content/${name}.js"`)) {
131
138
  return false;
132
139
  }
133
- const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name), () => legacyAddSubscript(content, name), BROWSER_MAIN_JS);
140
+ const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS);
134
141
  if (usedFallback) {
135
142
  assertBraceBalancePreserved(content, value, BROWSER_MAIN_JS);
136
143
  }
@@ -73,6 +73,29 @@ export interface PatchMetadata {
73
73
  * renamed or removed.
74
74
  */
75
75
  lintIgnore?: string[];
76
+ /**
77
+ * Optional per-patch threshold-tier override for the `large-patch-lines`
78
+ * rule. Exists for branding patches that must touch a small number of
79
+ * cross-cutting registration files alongside `browser/branding/<name>/`
80
+ * (notably `browser/moz.configure` to register the new branding flavor
81
+ * with the top-level configure). The narrow auto-detect allowlist in
82
+ * `isBrandingOnlyPatch` covers the canonical shape, but a fork whose
83
+ * branding patch also touches an unlisted sibling (for example a
84
+ * `browser/themes/<name>/` override or a vendor-specific icon
85
+ * resource) falls through to the general tier and trips the hard
86
+ * limit on what is legitimately one branding diff.
87
+ *
88
+ * Declaring `tier: "branding"` here forces the branding thresholds
89
+ * (notice 3000 / warning 8000 / error 20000) regardless of
90
+ * `filesAffected`. The tier is the weaker claim than test — a patch
91
+ * of all-tests still lands in the test tier even if this field is
92
+ * set, because the test-tier thresholds are already more permissive
93
+ * and a test that is also branding-shaped is vanishingly rare.
94
+ *
95
+ * Only `"branding"` is currently recognised. Unknown values are
96
+ * rejected by the manifest validator, not silently stripped.
97
+ */
98
+ tier?: 'branding';
76
99
  }
77
100
  /**
78
101
  * Schema for patches/patches.json file.
@@ -102,6 +102,15 @@ export interface FurnaceConfig {
102
102
  tokenPrefix?: string;
103
103
  /** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
104
104
  tokenAllowlist?: string[];
105
+ /**
106
+ * CSS custom-property prefixes that identify upstream / platform
107
+ * variables the fork does not own. `token coverage` counts matches
108
+ * as `allowlisted` rather than `unknown` so a copied upstream
109
+ * baseline doesn't drag fork-owned coverage percentages down.
110
+ * Defaults to `['--moz-']` when unset. Pass an explicit empty array
111
+ * to restore the pre-0.18.0 strict contract.
112
+ */
113
+ platformPrefixes?: string[];
105
114
  /**
106
115
  * Custom properties used as runtime state channels — written and read by the
107
116
  * component itself (e.g. per-frame camera/tile positions) rather than
@@ -76,6 +76,13 @@ export declare class ParsedRecord {
76
76
  * @throws Error if the field is missing or not an array of strings
77
77
  */
78
78
  stringArray(key: string): string[];
79
+ /**
80
+ * Extracts an optional array-of-strings field.
81
+ * @param key - Field name
82
+ * @returns The string array (fresh copy) or undefined when absent
83
+ * @throws Error if the field is present but not an array of strings
84
+ */
85
+ optionalStringArray(key: string): string[] | undefined;
79
86
  /**
80
87
  * Extracts a required nested object field.
81
88
  * @param key - Field name
@@ -142,6 +142,21 @@ export class ParsedRecord {
142
142
  }
143
143
  return [...value];
144
144
  }
145
+ /**
146
+ * Extracts an optional array-of-strings field.
147
+ * @param key - Field name
148
+ * @returns The string array (fresh copy) or undefined when absent
149
+ * @throws Error if the field is present but not an array of strings
150
+ */
151
+ optionalStringArray(key) {
152
+ const value = this.#data[key];
153
+ if (value === undefined)
154
+ return undefined;
155
+ if (!isArray(value) || !value.every(isString)) {
156
+ throw new Error(`${this.#label}.${key} must be an array of strings`);
157
+ }
158
+ return [...value];
159
+ }
145
160
  /**
146
161
  * Extracts a required nested object field.
147
162
  * @param key - Field name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",