@hominis/fireforge 0.38.0 → 0.39.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 (60) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/src/commands/build.js +8 -3
  3. package/dist/src/commands/discard.js +75 -50
  4. package/dist/src/commands/export-flow.d.ts +1 -1
  5. package/dist/src/commands/export-flow.js +7 -4
  6. package/dist/src/commands/export-placement-gate.js +1 -1
  7. package/dist/src/commands/export-shared.js +17 -2
  8. package/dist/src/commands/import.js +5 -0
  9. package/dist/src/commands/lint-per-patch.js +15 -5
  10. package/dist/src/commands/lint.js +1 -1
  11. package/dist/src/commands/patch/lint-ignore.js +20 -1
  12. package/dist/src/commands/patch/move-files-into.d.ts +47 -0
  13. package/dist/src/commands/patch/move-files-into.js +245 -0
  14. package/dist/src/commands/patch/move-files.js +27 -13
  15. package/dist/src/commands/patch/rename.js +4 -1
  16. package/dist/src/commands/reset.js +5 -0
  17. package/dist/src/commands/status-output.d.ts +9 -0
  18. package/dist/src/commands/status-output.js +33 -1
  19. package/dist/src/commands/status.js +17 -3
  20. package/dist/src/commands/test-diagnose.d.ts +1 -1
  21. package/dist/src/commands/test-diagnose.js +9 -3
  22. package/dist/src/commands/test-modes.d.ts +21 -0
  23. package/dist/src/commands/test-modes.js +42 -0
  24. package/dist/src/commands/test-run.d.ts +2 -0
  25. package/dist/src/commands/test-run.js +8 -2
  26. package/dist/src/commands/test.js +38 -32
  27. package/dist/src/core/build-baseline-types.d.ts +7 -0
  28. package/dist/src/core/build-baseline.d.ts +1 -1
  29. package/dist/src/core/build-baseline.js +2 -1
  30. package/dist/src/core/build-prepare.d.ts +9 -0
  31. package/dist/src/core/build-prepare.js +24 -0
  32. package/dist/src/core/config-validate.js +8 -0
  33. package/dist/src/core/discard-baseline.d.ts +60 -0
  34. package/dist/src/core/discard-baseline.js +232 -0
  35. package/dist/src/core/license-headers.d.ts +12 -0
  36. package/dist/src/core/license-headers.js +27 -0
  37. package/dist/src/core/patch-export.d.ts +10 -1
  38. package/dist/src/core/patch-export.js +12 -2
  39. package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
  40. package/dist/src/core/patch-lint-checkjs.js +30 -14
  41. package/dist/src/core/patch-lint-cross.d.ts +9 -1
  42. package/dist/src/core/patch-lint-cross.js +69 -9
  43. package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
  44. package/dist/src/core/patch-lint-mozbuild.js +104 -0
  45. package/dist/src/core/patch-lint.js +4 -1
  46. package/dist/src/core/patch-policy.d.ts +14 -1
  47. package/dist/src/core/patch-policy.js +13 -2
  48. package/dist/src/core/test-harness-crash.d.ts +10 -0
  49. package/dist/src/core/test-harness-crash.js +16 -0
  50. package/dist/src/core/test-stale-check.d.ts +15 -0
  51. package/dist/src/core/test-stale-check.js +37 -1
  52. package/dist/src/core/token-manager.js +39 -2
  53. package/dist/src/core/token-variant.d.ts +7 -0
  54. package/dist/src/core/token-variant.js +9 -0
  55. package/dist/src/core/typecheck-shim.d.ts +13 -0
  56. package/dist/src/core/typecheck-shim.js +17 -2
  57. package/dist/src/core/typecheck.js +14 -3
  58. package/dist/src/types/commands/options.d.ts +12 -0
  59. package/dist/src/types/config.d.ts +15 -0
  60. package/package.json +1 -1
@@ -4,14 +4,14 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
4
4
  import { FurnaceError } from '../errors/furnace.js';
5
5
  import { toError } from '../utils/errors.js';
6
6
  import { pathExists, readText, writeText } from '../utils/fs.js';
7
- import { warn } from '../utils/logger.js';
7
+ import { info, warn } from '../utils/logger.js';
8
8
  import { escapeRegex } from '../utils/regex.js';
9
9
  import { validateTokenName } from '../utils/validation.js';
10
10
  import { getProjectPaths, loadConfig } from './config.js';
11
11
  import { loadFurnaceConfig } from './furnace-config.js';
12
12
  import { findDarkMediaCloseIndex, findDarkRootInsertionIndex } from './token-dark-mode.js';
13
13
  import { addTokenToDocs } from './token-docs.js';
14
- import { insertVariantDeclaration, validateVariantSelector, variantBlockHasToken, } from './token-variant.js';
14
+ import { insertVariantDeclaration, validateVariantSelector, variantBlockExists, variantBlockHasToken, } from './token-variant.js';
15
15
  /** Returns the token CSS path relative to engine root for a given binary name. */
16
16
  export function getTokensCssPath(binaryName) {
17
17
  return `browser/themes/shared/${binaryName}-tokens.css`;
@@ -389,6 +389,39 @@ function findCategorySection(lines, category, tokensCssPath) {
389
389
  }
390
390
  return { categoryLine, sectionEnd };
391
391
  }
392
+ /**
393
+ * Explicit theme-attribute selectors the override path keeps in sync with
394
+ * the `@media (prefers-color-scheme: dark)` block. Consumer scaffolds that
395
+ * pair the media query with `:root[data-theme="dark"]` /
396
+ * `:root[data-theme="light"]` blocks (viewer-toggle theming) require every
397
+ * themed list to declare identical token sets, so an override add that only
398
+ * wrote the media block was a guaranteed half-finished edit (FORGE F8).
399
+ */
400
+ const THEME_ATTRIBUTE_VARIANTS = [
401
+ { variant: '[data-theme="dark"]', pick: 'dark' },
402
+ { variant: '[data-theme="light"]', pick: 'light' },
403
+ ];
404
+ /**
405
+ * Mirrors an override's light/dark values into existing top-level
406
+ * `:root[data-theme="dark"]` / `:root[data-theme="light"]` blocks. Blocks
407
+ * that do not exist are left alone (scaffolded files have none; behavior is
408
+ * unchanged there). Idempotent per block. Returns the selectors written.
409
+ */
410
+ function insertThemeAttributeOverrides(lines, options) {
411
+ if (options.mode !== 'override' || !options.darkValue)
412
+ return [];
413
+ const written = [];
414
+ for (const { variant, pick } of THEME_ATTRIBUTE_VARIANTS) {
415
+ if (!variantBlockExists(lines, variant))
416
+ continue;
417
+ if (variantBlockHasToken(lines, variant, options.tokenName))
418
+ continue;
419
+ const value = pick === 'dark' ? options.darkValue : options.value;
420
+ insertVariantDeclaration(lines, variant, ` ${options.tokenName}: ${value};`);
421
+ written.push(`:root${variant}`);
422
+ }
423
+ return written;
424
+ }
392
425
  function insertDarkModeOverride(lines, options) {
393
426
  if (options.mode !== 'override' || !options.darkValue)
394
427
  return;
@@ -451,6 +484,10 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
451
484
  }
452
485
  lines.splice(insertIndex, 0, ...insertLines);
453
486
  insertDarkModeOverride(lines, options);
487
+ const themeBlocksWritten = insertThemeAttributeOverrides(lines, options);
488
+ if (themeBlocksWritten.length > 0) {
489
+ info(`Override also written to ${themeBlocksWritten.join(' and ')}.`);
490
+ }
454
491
  content = lines.join('\n');
455
492
  await writeText(filePath, content);
456
493
  return { added: true, categoryCreated };
@@ -29,6 +29,13 @@ export type VariantValidation = {
29
29
  * @param raw - Raw `--variant` value from the CLI / programmatic caller.
30
30
  */
31
31
  export declare function validateVariantSelector(raw: unknown): VariantValidation;
32
+ /**
33
+ * True when a top-level `:root<variant>` block exists in `lines`. Used by
34
+ * the override path to write theme-attribute blocks only when the scaffold
35
+ * already declares them — `insertVariantDeclaration` would otherwise create
36
+ * the block, which is only wanted for the explicit `--variant` flow.
37
+ */
38
+ export declare function variantBlockExists(lines: string[], variant: string): boolean;
32
39
  /** True when the `:root<variant>` block already declares `tokenName`. */
33
40
  export declare function variantBlockHasToken(lines: string[], variant: string, tokenName: string): boolean;
34
41
  /**
@@ -107,6 +107,15 @@ function findVariantBlockInsertionPoint(lines) {
107
107
  }
108
108
  return lines.length;
109
109
  }
110
+ /**
111
+ * True when a top-level `:root<variant>` block exists in `lines`. Used by
112
+ * the override path to write theme-attribute blocks only when the scaffold
113
+ * already declares them — `insertVariantDeclaration` would otherwise create
114
+ * the block, which is only wanted for the explicit `--variant` flow.
115
+ */
116
+ export function variantBlockExists(lines, variant) {
117
+ return findVariantBlock(lines, variant) !== null;
118
+ }
110
119
  /** True when the `:root<variant>` block already declares `tokenName`. */
111
120
  export function variantBlockHasToken(lines, variant, tokenName) {
112
121
  const block = findVariantBlock(lines, variant);
@@ -24,6 +24,19 @@ export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
24
24
  * reason the operator couldn't infer from the docs.
25
25
  */
26
26
  export declare const SUPPRESSED_DIAGNOSTIC_CODES: ReadonlySet<number>;
27
+ /**
28
+ * Undefined-free-identifier codes, split out of
29
+ * {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
30
+ * suppression let a module reference a name with no import or declaration
31
+ * anywhere and still typecheck with 0 errors — the failure then surfaced
32
+ * as a runtime `ReferenceError`. Both flows now report these at a
33
+ * configurable severity (default `'warning'`: visible without breaking
34
+ * gates; genuine shim gaps are silenced by adding the global to
35
+ * `extraShim`, or per-run via the `'off'` setting).
36
+ */
37
+ export declare const UNDEFINED_IDENTIFIER_CODES: ReadonlySet<number>;
38
+ /** Hint appended to reported undefined-identifier diagnostics. */
39
+ export declare const UNDEFINED_IDENTIFIER_HINT: string;
27
40
  /**
28
41
  * Result of {@link composeShimSource}: the source body to feed into
29
42
  * the TS host plus a flag indicating whether the user-supplied extra
@@ -146,11 +146,26 @@ export const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
146
146
  2306, // File '{0}' is not a module.
147
147
  2305, // Module '{0}' has no exported member '{1}'.
148
148
  2792, // Cannot find module '{0}'. Did you mean to set the 'moduleResolution' option...
149
- 2304, // Cannot find name '{0}'. (for globals we missed in the shim)
150
- 2552, // Cannot find name '{0}'. Did you mean '{1}'?
151
149
  2580, // Cannot find name '{0}'. Do you need to install type definitions...
152
150
  7016, // Could not find a declaration file for module '{0}'.
153
151
  ]);
152
+ /**
153
+ * Undefined-free-identifier codes, split out of
154
+ * {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
155
+ * suppression let a module reference a name with no import or declaration
156
+ * anywhere and still typecheck with 0 errors — the failure then surfaced
157
+ * as a runtime `ReferenceError`. Both flows now report these at a
158
+ * configurable severity (default `'warning'`: visible without breaking
159
+ * gates; genuine shim gaps are silenced by adding the global to
160
+ * `extraShim`, or per-run via the `'off'` setting).
161
+ */
162
+ export const UNDEFINED_IDENTIFIER_CODES = new Set([
163
+ 2304, // Cannot find name '{0}'.
164
+ 2552, // Cannot find name '{0}'. Did you mean '{1}'?
165
+ ]);
166
+ /** Hint appended to reported undefined-identifier diagnostics. */
167
+ export const UNDEFINED_IDENTIFIER_HINT = '(undefined identifier — import or declare it, add the global to the extra shim, ' +
168
+ 'or tune the "undefinedIdentifiers" setting)';
154
169
  /** Matches a lone triple-slash `/// <reference path="…" />` directive line. */
155
170
  const TRIPLE_SLASH_REFERENCE = /^\s*\/\/\/\s*<reference\s+path\s*=\s*["']([^"']+)["']\s*\/?>\s*$/;
156
171
  /**
@@ -21,7 +21,7 @@
21
21
  import { dirname, isAbsolute, relative, resolve } from 'node:path';
22
22
  import { pathExists } from '../utils/fs.js';
23
23
  import { verbose } from '../utils/logger.js';
24
- import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES } from './typecheck-shim.js';
24
+ import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
25
25
  /**
26
26
  * Sentinel string surfaced as a `TypecheckIssue.message` when the
27
27
  * project's jsconfig sets `checkJs: false`. Exported so tests can
@@ -131,7 +131,7 @@ export async function runTypecheck(projectRoot, cfg) {
131
131
  });
132
132
  continue;
133
133
  }
134
- results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource));
134
+ results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource, cfg.undefinedIdentifiers ?? 'warning'));
135
135
  }
136
136
  return results;
137
137
  }
@@ -149,7 +149,7 @@ function resolveProjectExtraShim(cfg, projectPath) {
149
149
  return cfg.extraShim;
150
150
  }
151
151
  /** Runs typecheck for a single jsconfig path, isolating its failures. */
152
- async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource) {
152
+ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource, undefinedIdentifiers = 'warning') {
153
153
  const absConfig = resolve(projectRoot, projectPath);
154
154
  if (!(await pathExists(absConfig))) {
155
155
  return {
@@ -268,6 +268,17 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource)
268
268
  // can't act on them and they would clutter CI logs.
269
269
  if (diag.file?.fileName === shimPath)
270
270
  continue;
271
+ if (UNDEFINED_IDENTIFIER_CODES.has(diag.code)) {
272
+ if (undefinedIdentifiers === 'off')
273
+ continue;
274
+ const issue = diagnosticToIssue(ts, diag, absConfig, projectPath);
275
+ issues.push({
276
+ ...issue,
277
+ category: undefinedIdentifiers,
278
+ message: `${issue.message} ${UNDEFINED_IDENTIFIER_HINT}`,
279
+ });
280
+ continue;
281
+ }
271
282
  issues.push(diagnosticToIssue(ts, diag, absConfig, projectPath));
272
283
  }
273
284
  verbose(`typecheck: ${projectPath} — analyzed ${String(parsed.fileNames.length)} file(s), found ${String(issues.length)} issue(s)`);
@@ -160,6 +160,12 @@ export interface ResetOptions {
160
160
  * Options for the discard command.
161
161
  */
162
162
  export interface DiscardOptions {
163
+ /**
164
+ * Restore to pristine upstream (HEAD) instead of the patch-applied
165
+ * baseline; deletes patch-created files (the pre-0.39.0 semantics —
166
+ * FORGE F1).
167
+ */
168
+ toUpstream?: boolean;
163
169
  /** Show what would be discarded without doing it */
164
170
  dryRun?: boolean;
165
171
  /** Skip confirmation prompt */
@@ -795,6 +801,12 @@ export interface StatusOptions {
795
801
  * patch as an ownership conflict.
796
802
  */
797
803
  ownership?: boolean;
804
+ /**
805
+ * Print the recorded test-packaging coverage of the last build baseline
806
+ * (scope, timestamp, recording invocation) and exit. Read-only — the
807
+ * counterpart to the out-of-coverage test refusal (FORGE F11).
808
+ */
809
+ testCoverage?: boolean;
798
810
  /** Output machine-readable JSON instead of human-readable text. */
799
811
  json?: boolean;
800
812
  }
@@ -181,6 +181,14 @@ export interface TypecheckConfig {
181
181
  * mismatch). A narrowed project sets `null` here to stay clean.
182
182
  */
183
183
  projectOverrides?: Record<string, string | null>;
184
+ /**
185
+ * How to report undefined free identifiers (TS2304/TS2552). These were
186
+ * historically suppressed as shim-gap noise, which also hid genuine
187
+ * missing imports until they crashed at runtime (FORGE F12). Default
188
+ * `'warning'`: visible without failing the gate. `'error'` makes them
189
+ * blocking; `'off'` restores the historical suppression.
190
+ */
191
+ undefinedIdentifiers?: PatchLintSeverityGate;
184
192
  }
185
193
  /**
186
194
  * Wire command configuration.
@@ -262,6 +270,13 @@ export interface PatchLintConfig {
262
270
  * 'off'.
263
271
  */
264
272
  chromeScriptJsDoc?: PatchLintSeverityGate;
273
+ /**
274
+ * How the checkJs pass reports undefined free identifiers
275
+ * (TS2304/TS2552). Same semantics as `typecheck.undefinedIdentifiers`
276
+ * (FORGE F12); the two flows share the suppression policy so a patch
277
+ * cannot pass one and fail the other. Default: 'warning'.
278
+ */
279
+ undefinedIdentifiers?: PatchLintSeverityGate;
265
280
  }
266
281
  /**
267
282
  * Build mode for mach.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",