@hominis/fireforge 0.30.1 → 0.32.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 (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +9 -16
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +46 -1
  10. package/dist/src/commands/export.js +52 -113
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +3 -1
  20. package/dist/src/commands/lint-per-patch.js +265 -74
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +193 -88
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-files.js +4 -1
  42. package/dist/src/commands/re-export-scan.js +8 -1
  43. package/dist/src/commands/re-export.js +8 -1
  44. package/dist/src/commands/rebase/summary.d.ts +1 -5
  45. package/dist/src/commands/rebase/summary.js +1 -1
  46. package/dist/src/commands/status-output.js +77 -68
  47. package/dist/src/commands/test-diagnose.d.ts +23 -0
  48. package/dist/src/commands/test-diagnose.js +210 -0
  49. package/dist/src/commands/test-run.d.ts +68 -0
  50. package/dist/src/commands/test-run.js +97 -0
  51. package/dist/src/commands/test.js +214 -263
  52. package/dist/src/commands/token.js +15 -1
  53. package/dist/src/commands/wire.js +109 -78
  54. package/dist/src/core/build-audit.d.ts +1 -1
  55. package/dist/src/core/build-audit.js +2 -46
  56. package/dist/src/core/build-baseline-types.d.ts +38 -0
  57. package/dist/src/core/build-baseline-types.js +10 -0
  58. package/dist/src/core/build-baseline.d.ts +1 -31
  59. package/dist/src/core/build-prepare.d.ts +1 -1
  60. package/dist/src/core/build-prepare.js +2 -45
  61. package/dist/src/core/config-paths.d.ts +0 -8
  62. package/dist/src/core/config-paths.js +4 -4
  63. package/dist/src/core/config-state.d.ts +0 -6
  64. package/dist/src/core/config-state.js +1 -1
  65. package/dist/src/core/config-validate-patch-policy.js +12 -13
  66. package/dist/src/core/config-validate.js +74 -28
  67. package/dist/src/core/engine-changes.d.ts +24 -0
  68. package/dist/src/core/engine-changes.js +64 -0
  69. package/dist/src/core/firefox-cache.d.ts +0 -5
  70. package/dist/src/core/firefox-cache.js +1 -1
  71. package/dist/src/core/firefox-download.d.ts +0 -6
  72. package/dist/src/core/firefox-download.js +1 -1
  73. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  74. package/dist/src/core/furnace-apply-helpers.js +11 -20
  75. package/dist/src/core/furnace-apply.d.ts +1 -1
  76. package/dist/src/core/furnace-apply.js +1 -1
  77. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  78. package/dist/src/core/furnace-checksum-utils.js +15 -0
  79. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  80. package/dist/src/core/furnace-config-validate.js +133 -0
  81. package/dist/src/core/furnace-config.d.ts +4 -32
  82. package/dist/src/core/furnace-config.js +15 -111
  83. package/dist/src/core/furnace-constants.d.ts +0 -10
  84. package/dist/src/core/furnace-constants.js +2 -2
  85. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  86. package/dist/src/core/furnace-css-fragments.js +243 -0
  87. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  88. package/dist/src/core/furnace-jsconfig.js +191 -0
  89. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  90. package/dist/src/core/furnace-validate-helpers.js +40 -1
  91. package/dist/src/core/furnace-validate-registration.js +16 -1
  92. package/dist/src/core/furnace-validate.js +54 -2
  93. package/dist/src/core/git-base.d.ts +15 -0
  94. package/dist/src/core/git-base.js +32 -0
  95. package/dist/src/core/git-diff.d.ts +8 -0
  96. package/dist/src/core/git-diff.js +224 -59
  97. package/dist/src/core/git-file-ops.d.ts +39 -12
  98. package/dist/src/core/git-file-ops.js +84 -3
  99. package/dist/src/core/lint-cache.d.ts +0 -13
  100. package/dist/src/core/lint-cache.js +5 -5
  101. package/dist/src/core/mach.d.ts +22 -1
  102. package/dist/src/core/mach.js +27 -2
  103. package/dist/src/core/manifest-register.d.ts +5 -16
  104. package/dist/src/core/manifest-register.js +3 -1
  105. package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
  106. package/dist/src/core/patch-lint-checkjs.js +263 -71
  107. package/dist/src/core/patch-lint-css.d.ts +23 -0
  108. package/dist/src/core/patch-lint-css.js +172 -0
  109. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  110. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  111. package/dist/src/core/patch-lint-observer.js +168 -0
  112. package/dist/src/core/patch-lint.d.ts +34 -11
  113. package/dist/src/core/patch-lint.js +24 -161
  114. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  115. package/dist/src/core/patch-manifest-io.js +44 -2
  116. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  117. package/dist/src/core/patch-manifest-validate.js +1 -1
  118. package/dist/src/core/patch-manifest.d.ts +1 -1
  119. package/dist/src/core/patch-manifest.js +1 -1
  120. package/dist/src/core/patch-policy.d.ts +0 -4
  121. package/dist/src/core/patch-policy.js +10 -4
  122. package/dist/src/core/register-browser-content.d.ts +1 -1
  123. package/dist/src/core/register-module.d.ts +1 -1
  124. package/dist/src/core/register-result.d.ts +21 -0
  125. package/dist/src/core/register-result.js +9 -0
  126. package/dist/src/core/register-shared-css.d.ts +1 -1
  127. package/dist/src/core/register-test-manifest.d.ts +1 -1
  128. package/dist/src/core/test-harness-crash.d.ts +61 -0
  129. package/dist/src/core/test-harness-crash.js +140 -0
  130. package/dist/src/core/test-stale-check.d.ts +1 -1
  131. package/dist/src/core/test-stale-check.js +2 -46
  132. package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
  133. package/dist/src/core/test-xpcshell-retry.js +10 -3
  134. package/dist/src/core/token-dark-mode.js +14 -26
  135. package/dist/src/core/token-manager.d.ts +4 -0
  136. package/dist/src/core/token-manager.js +70 -16
  137. package/dist/src/core/typecheck-shim.d.ts +3 -22
  138. package/dist/src/core/typecheck-shim.js +69 -7
  139. package/dist/src/core/wire-utils.js +37 -44
  140. package/dist/src/types/commands/index.d.ts +1 -1
  141. package/dist/src/types/commands/options.d.ts +122 -0
  142. package/dist/src/types/config.d.ts +11 -2
  143. package/dist/src/types/furnace.d.ts +12 -1
  144. package/dist/src/utils/elapsed.d.ts +0 -2
  145. package/dist/src/utils/elapsed.js +1 -1
  146. package/dist/src/utils/fs.d.ts +0 -5
  147. package/dist/src/utils/fs.js +1 -1
  148. package/dist/src/utils/regex.d.ts +0 -6
  149. package/dist/src/utils/regex.js +3 -3
  150. package/dist/src/utils/validation.d.ts +0 -8
  151. package/dist/src/utils/validation.js +2 -2
  152. package/package.json +6 -4
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CSS patch-lint rules: introduced raw color values and non-tokenized
3
+ * custom-property references.
4
+ *
5
+ * Split out of `patch-lint.ts` so the per-patch and CSS rule bodies each
6
+ * stay within the project's per-file line budget — the same separation
7
+ * already applied to the JSDoc, observer, import, ownership, checkJs, and
8
+ * cross-patch rule families. `patch-lint.ts` re-exports `lintPatchedCss`
9
+ * so existing callers keep importing from the single module.
10
+ */
11
+ import type { PatchLintIssue } from '../types/commands/index.js';
12
+ import type { FireForgeConfig } from '../types/config.js';
13
+ /**
14
+ * Lints patched CSS files for introduced raw color values and non-tokenized
15
+ * custom properties.
16
+ *
17
+ * @param repoDir - Absolute path to the engine (repository) directory
18
+ * @param affectedFiles - File paths (relative to repoDir) affected by the patch
19
+ * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
20
+ * @param config - Project configuration
21
+ * @returns Array of lint issues found
22
+ */
23
+ export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string, config?: FireForgeConfig): Promise<PatchLintIssue[]>;
@@ -0,0 +1,172 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * CSS patch-lint rules: introduced raw color values and non-tokenized
4
+ * custom-property references.
5
+ *
6
+ * Split out of `patch-lint.ts` so the per-patch and CSS rule bodies each
7
+ * stay within the project's per-file line budget — the same separation
8
+ * already applied to the JSDoc, observer, import, ownership, checkJs, and
9
+ * cross-patch rule families. `patch-lint.ts` re-exports `lintPatchedCss`
10
+ * so existing callers keep importing from the single module.
11
+ */
12
+ import { join } from 'node:path';
13
+ import { toError } from '../utils/errors.js';
14
+ import { pathExists, readText } from '../utils/fs.js';
15
+ import { verbose } from '../utils/logger.js';
16
+ import { hasRawCssColors } from '../utils/regex.js';
17
+ import { loadFurnaceConfig } from './furnace-config.js';
18
+ import { extractAddedLinesPerFile } from './patch-lint-diff.js';
19
+ /**
20
+ * Loads the furnace token-prefix lint inputs gracefully — returns
21
+ * undefined (skipping the token-prefix check) when furnace.json cannot
22
+ * be loaded or no tokenPrefix is configured.
23
+ */
24
+ async function loadCssTokenContext(repoDir) {
25
+ try {
26
+ const root = join(repoDir, '..');
27
+ const furnaceConfig = await loadFurnaceConfig(root);
28
+ if (furnaceConfig.tokenPrefix) {
29
+ return {
30
+ tokenPrefix: furnaceConfig.tokenPrefix,
31
+ tokenAllowlist: new Set(furnaceConfig.tokenAllowlist ?? []),
32
+ runtimeVariables: new Set(furnaceConfig.runtimeVariables ?? []),
33
+ };
34
+ }
35
+ }
36
+ catch (error) {
37
+ verbose(`Skipping furnace token-prefix lint hints because furnace.json could not be loaded: ${toError(error).message}`);
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Raw-color check for one patched CSS file, scoped to introduced lines
43
+ * when diff context is available. Pushes onto `issues`.
44
+ */
45
+ function checkRawColorValues(file, rawCss, addedLinesByFile, config, issues) {
46
+ // Check only introduced raw color values when diff context is available.
47
+ // Skip files on the raw-color allowlist (exact path or basename match) and
48
+ // auto-exempt files under `browser/branding/` — those are the fork's
49
+ // visual identity assets (app-about dialogs, installer pages, branded
50
+ // CSS copied from Firefox's `unofficial` template) and belong to the
51
+ // design-decision layer the design-token system does not govern.
52
+ // Without this auto-exemption, every first-time setup's copied CSS
53
+ // failed `raw-color-value` with no actionable fix other than manually
54
+ // listing each path in `rawColorAllowlist`.
55
+ const allowlist = config?.patchLint?.rawColorAllowlist;
56
+ const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
57
+ const isBranding = file.startsWith('browser/branding/');
58
+ if (!isAllowlisted && !isBranding) {
59
+ // Strip lines with inline fireforge-ignore: raw-color-value suppression.
60
+ // Check against rawCss (before comment stripping) so the CSS comment marker is still present.
61
+ const sourceForSuppression = addedLinesByFile
62
+ ? (addedLinesByFile.get(file) ?? []).join('\n')
63
+ : rawCss;
64
+ const suppressedContent = sourceForSuppression
65
+ .split('\n')
66
+ .filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
67
+ .join('\n')
68
+ .replace(/\/\*[\s\S]*?\*\//g, '');
69
+ if (hasRawCssColors(suppressedContent)) {
70
+ issues.push({
71
+ file,
72
+ check: 'raw-color-value',
73
+ message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
74
+ severity: 'error',
75
+ });
76
+ }
77
+ }
78
+ }
79
+ /**
80
+ * Token-prefix check for one patched CSS file: flags `var(--x)` references
81
+ * that match neither the configured prefix, the allowlist, the runtime
82
+ * variables, nor a same-file declaration. Pushes onto `issues`.
83
+ */
84
+ function checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues) {
85
+ // Check for non-tokenized custom properties. A variable that is both
86
+ // declared and consumed inside the same file is auto-exempted as a
87
+ // runtime state channel (see furnace.json → runtimeVariables).
88
+ //
89
+ // When diff context is available, scope the `var(...)` scan to
90
+ // added/modified lines only. `cssContent` (full-file) is still the
91
+ // source of `localDeclarations` so vars declared anywhere in the file
92
+ // are recognised as same-file refs regardless of where the consuming
93
+ // `var(...)` appears. Before this scoping change, a small edit to a
94
+ // Furnace override of a stock component (e.g. moz-card) produced a
95
+ // `token-prefix-violation` for every stock `var(--moz-card-*)` the
96
+ // upstream file already carried, because the scanner saw the full
97
+ // applied file and flagged each inherited reference as if the fork
98
+ // had introduced it.
99
+ if (tokenContext) {
100
+ const declarationPattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
101
+ const localDeclarations = new Set();
102
+ let declMatch;
103
+ while ((declMatch = declarationPattern.exec(cssContent)) !== null) {
104
+ const name = declMatch[1];
105
+ if (name)
106
+ localDeclarations.add(name);
107
+ }
108
+ const prefixScanSource = addedLinesByFile
109
+ ? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
110
+ : cssContent;
111
+ if (prefixScanSource.length > 0) {
112
+ const varPattern = /var\(\s*(--[\w-]+)/g;
113
+ const flaggedProps = new Set();
114
+ let match;
115
+ while ((match = varPattern.exec(prefixScanSource)) !== null) {
116
+ const prop = match[1];
117
+ if (!prop)
118
+ continue;
119
+ if (prop.startsWith(tokenContext.tokenPrefix))
120
+ continue;
121
+ if (tokenContext.tokenAllowlist.has(prop))
122
+ continue;
123
+ if (tokenContext.runtimeVariables.has(prop))
124
+ continue;
125
+ if (localDeclarations.has(prop))
126
+ continue;
127
+ // De-duplicate per (file, prop) pair so the same introduced var
128
+ // used five times in the added hunk doesn't produce five
129
+ // identical issue entries.
130
+ if (flaggedProps.has(prop))
131
+ continue;
132
+ flaggedProps.add(prop);
133
+ issues.push({
134
+ file,
135
+ check: 'token-prefix-violation',
136
+ message: `CSS references var(${prop}) which does not match the required token prefix "${tokenContext.tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`,
137
+ severity: 'error',
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Lints patched CSS files for introduced raw color values and non-tokenized
145
+ * custom properties.
146
+ *
147
+ * @param repoDir - Absolute path to the engine (repository) directory
148
+ * @param affectedFiles - File paths (relative to repoDir) affected by the patch
149
+ * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
150
+ * @param config - Project configuration
151
+ * @returns Array of lint issues found
152
+ */
153
+ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config) {
154
+ const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
155
+ if (cssFiles.length === 0)
156
+ return [];
157
+ const tokenContext = await loadCssTokenContext(repoDir);
158
+ const issues = [];
159
+ const addedLinesByFile = diffContent ? extractAddedLinesPerFile(diffContent) : undefined;
160
+ for (const file of cssFiles) {
161
+ const filePath = join(repoDir, file);
162
+ if (!(await pathExists(filePath)))
163
+ continue;
164
+ const rawCss = await readText(filePath);
165
+ // Strip block comments before scanning
166
+ const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
167
+ checkRawColorValues(file, rawCss, addedLinesByFile, config, issues);
168
+ checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues);
169
+ }
170
+ return issues;
171
+ }
172
+ //# sourceMappingURL=patch-lint-css.js.map
@@ -37,13 +37,72 @@ function findAttachedJsDoc(comments, declStart, source) {
37
37
  // ---------------------------------------------------------------------------
38
38
  // JSDoc tag parsing
39
39
  // ---------------------------------------------------------------------------
40
+ /**
41
+ * Skips a balanced `{ … }` JSDoc type expression starting at `start`
42
+ * (which must point at the opening brace). Braces nest in inline object
43
+ * types (`{{ id: string, args?: Record<string, boolean> }}`), so a flat
44
+ * "anything but `}`" regex truncates at the first inner close brace and
45
+ * loses the param name. String literal types may contain braces too, so
46
+ * quoted runs are skipped verbatim.
47
+ *
48
+ * @returns Index just past the matching close brace, or -1 when the type
49
+ * expression never closes (malformed doc — caller skips the tag).
50
+ */
51
+ function skipBalancedTypeBraces(jsDoc, start) {
52
+ let depth = 0;
53
+ let quote = null;
54
+ for (let i = start; i < jsDoc.length; i++) {
55
+ const ch = jsDoc[i];
56
+ if (quote !== null) {
57
+ if (ch === '\\')
58
+ i++;
59
+ else if (ch === quote)
60
+ quote = null;
61
+ continue;
62
+ }
63
+ if (ch === "'" || ch === '"' || ch === '`') {
64
+ quote = ch;
65
+ continue;
66
+ }
67
+ if (ch === '{')
68
+ depth++;
69
+ else if (ch === '}') {
70
+ depth--;
71
+ if (depth === 0)
72
+ return i + 1;
73
+ }
74
+ }
75
+ return -1;
76
+ }
40
77
  function extractParamNames(jsDoc) {
41
78
  const names = [];
42
- const paramPattern = /@param\s+(?:\{[^}]*\}\s+)?(\w+)/g;
79
+ const tagPattern = /@param\b/g;
43
80
  let m;
44
- while ((m = paramPattern.exec(jsDoc)) !== null) {
45
- if (m[1])
46
- names.push(m[1]);
81
+ while ((m = tagPattern.exec(jsDoc)) !== null) {
82
+ let i = m.index + m[0].length;
83
+ while (i < jsDoc.length && /\s/.test(jsDoc[i] ?? ''))
84
+ i++;
85
+ // Optional `{type}` expression — depth-aware so nested braces inside
86
+ // inline object types or Record<…> generics don't truncate the scan.
87
+ if (jsDoc[i] === '{') {
88
+ const afterType = skipBalancedTypeBraces(jsDoc, i);
89
+ if (afterType === -1)
90
+ continue;
91
+ i = afterType;
92
+ while (i < jsDoc.length && /\s/.test(jsDoc[i] ?? ''))
93
+ i++;
94
+ }
95
+ // Name token: bare `name`, optional `[name]`, or defaulted `[name=x]`.
96
+ // Dotted property docs (`opts.id`) record the base object name once.
97
+ const optional = jsDoc[i] === '[';
98
+ if (optional)
99
+ i++;
100
+ const nameMatch = /^[A-Za-z_$][\w$]*/.exec(jsDoc.slice(i));
101
+ if (!nameMatch)
102
+ continue;
103
+ const name = nameMatch[0];
104
+ if (!names.includes(name))
105
+ names.push(name);
47
106
  }
48
107
  return names;
49
108
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `observer-topic-naming` rule body, extracted from `patch-lint.ts`.
3
+ *
4
+ * The historical implementation captured the first string literal after
5
+ * the call's opening paren on a single line. That mis-attributed string
6
+ * literals in complex subject arguments and silently skipped multi-line
7
+ * call sites. This version scans the balanced argument list (across
8
+ * newlines, string-aware), takes the *topic* argument by position, and
9
+ * allowlists well-known Firefox-owned topics so tests that simulate
10
+ * upstream notifications are not pushed toward renaming real topics.
11
+ */
12
+ import type { PatchLintIssue } from '../types/commands/index.js';
13
+ /**
14
+ * Firefox-owned observer topics a fork legitimately observes or simulates.
15
+ * These must never be flagged for fork-prefix naming, even when a fork's
16
+ * binaryName happens to be a substring of one. `quit-application*` is
17
+ * handled as a prefix family in {@link isKnownFirefoxTopic}.
18
+ *
19
+ * The list is deliberately conservative: it only needs to cover topics
20
+ * whose text could plausibly contain a fork's binaryName, plus the
21
+ * high-traffic lifecycle topics seen in downstream test simulations.
22
+ */
23
+ export declare const KNOWN_FIREFOX_OBSERVER_TOPICS: ReadonlySet<string>;
24
+ /** True for allowlisted Firefox topics, including the quit-application family. */
25
+ export declare function isKnownFirefoxTopic(topic: string): boolean;
26
+ /**
27
+ * Lints observer-service call sites in `strippedContent` (comments already
28
+ * removed) for fork topic naming. Only topics that embed `binaryName` and
29
+ * do not follow the `<binary>-<noun>-<verb>` convention are flagged;
30
+ * allowlisted Firefox topics and constant-named topics are skipped.
31
+ *
32
+ * @param strippedContent - Source with comments stripped
33
+ * @param file - File path for issue attribution
34
+ * @param binaryName - Lowercased fork binary name
35
+ * @returns Observer-topic naming issues
36
+ */
37
+ export declare function lintObserverTopics(strippedContent: string, file: string, binaryName: string): PatchLintIssue[];
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `observer-topic-naming` rule body, extracted from `patch-lint.ts`.
4
+ *
5
+ * The historical implementation captured the first string literal after
6
+ * the call's opening paren on a single line. That mis-attributed string
7
+ * literals in complex subject arguments and silently skipped multi-line
8
+ * call sites. This version scans the balanced argument list (across
9
+ * newlines, string-aware), takes the *topic* argument by position, and
10
+ * allowlists well-known Firefox-owned topics so tests that simulate
11
+ * upstream notifications are not pushed toward renaming real topics.
12
+ */
13
+ /**
14
+ * Firefox-owned observer topics a fork legitimately observes or simulates.
15
+ * These must never be flagged for fork-prefix naming, even when a fork's
16
+ * binaryName happens to be a substring of one. `quit-application*` is
17
+ * handled as a prefix family in {@link isKnownFirefoxTopic}.
18
+ *
19
+ * The list is deliberately conservative: it only needs to cover topics
20
+ * whose text could plausibly contain a fork's binaryName, plus the
21
+ * high-traffic lifecycle topics seen in downstream test simulations.
22
+ */
23
+ export const KNOWN_FIREFOX_OBSERVER_TOPICS = new Set([
24
+ 'idle-daily',
25
+ 'profile-after-change',
26
+ 'profile-before-change',
27
+ 'xpcom-shutdown',
28
+ 'xpcom-will-shutdown',
29
+ 'final-ui-startup',
30
+ 'browser-delayed-startup-finished',
31
+ 'sessionstore-windows-restored',
32
+ 'document-element-inserted',
33
+ 'content-document-global-created',
34
+ 'http-on-modify-request',
35
+ 'http-on-examine-response',
36
+ 'nsPref:changed',
37
+ 'browser-window-before-show',
38
+ 'domwindowopened',
39
+ 'domwindowclosed',
40
+ ]);
41
+ /** True for allowlisted Firefox topics, including the quit-application family. */
42
+ export function isKnownFirefoxTopic(topic) {
43
+ return KNOWN_FIREFOX_OBSERVER_TOPICS.has(topic) || topic.startsWith('quit-application');
44
+ }
45
+ /**
46
+ * Scans a balanced `( … )` argument span starting at `openParen` (which
47
+ * must point at the opening paren) and splits it into top-level argument
48
+ * strings. Tracks paren/brace/bracket depth and skips quoted runs, so
49
+ * commas inside nested calls, object literals, or strings do not split.
50
+ *
51
+ * @returns The argument texts, or null when the span never closes within
52
+ * `maxLength` characters (malformed or truncated source — caller skips).
53
+ */
54
+ function extractCallArguments(content, openParen, maxLength = 2000) {
55
+ const args = [];
56
+ let current = '';
57
+ let depth = 0;
58
+ let quote = null;
59
+ const end = Math.min(content.length, openParen + maxLength);
60
+ for (let i = openParen; i < end; i++) {
61
+ const ch = content[i] ?? '';
62
+ if (quote !== null) {
63
+ current += ch;
64
+ if (ch === '\\') {
65
+ current += content[i + 1] ?? '';
66
+ i++;
67
+ }
68
+ else if (ch === quote) {
69
+ quote = null;
70
+ }
71
+ continue;
72
+ }
73
+ if (ch === "'" || ch === '"' || ch === '`') {
74
+ quote = ch;
75
+ current += ch;
76
+ continue;
77
+ }
78
+ if (ch === '(' || ch === '{' || ch === '[') {
79
+ depth++;
80
+ if (depth === 1 && ch === '(')
81
+ continue; // the call's own paren
82
+ current += ch;
83
+ continue;
84
+ }
85
+ if (ch === ')' || ch === '}' || ch === ']') {
86
+ depth--;
87
+ if (depth === 0 && ch === ')') {
88
+ args.push(current.trim());
89
+ return args;
90
+ }
91
+ current += ch;
92
+ continue;
93
+ }
94
+ if (ch === ',' && depth === 1) {
95
+ args.push(current.trim());
96
+ current = '';
97
+ continue;
98
+ }
99
+ current += ch;
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Returns the literal string value when `arg` is exactly one plain string
105
+ * literal (no concatenation, no `${}` interpolation), otherwise null.
106
+ * Constant-named topics (identifiers, member expressions) intentionally
107
+ * return null — hoisting a literal into a named constant is a supported
108
+ * way to mark a topic as deliberate.
109
+ */
110
+ function asStringLiteral(arg) {
111
+ const trimmed = arg.trim();
112
+ if (trimmed.length < 2)
113
+ return null;
114
+ const quoteChar = trimmed[0];
115
+ if (quoteChar !== "'" && quoteChar !== '"' && quoteChar !== '`')
116
+ return null;
117
+ if (trimmed[trimmed.length - 1] !== quoteChar)
118
+ return null;
119
+ const inner = trimmed.slice(1, -1);
120
+ if (inner.includes(quoteChar))
121
+ return null; // concatenation like "a" + "b"
122
+ if (quoteChar === '`' && inner.includes('${'))
123
+ return null;
124
+ return inner;
125
+ }
126
+ /**
127
+ * Lints observer-service call sites in `strippedContent` (comments already
128
+ * removed) for fork topic naming. Only topics that embed `binaryName` and
129
+ * do not follow the `<binary>-<noun>-<verb>` convention are flagged;
130
+ * allowlisted Firefox topics and constant-named topics are skipped.
131
+ *
132
+ * @param strippedContent - Source with comments stripped
133
+ * @param file - File path for issue attribution
134
+ * @param binaryName - Lowercased fork binary name
135
+ * @returns Observer-topic naming issues
136
+ */
137
+ export function lintObserverTopics(strippedContent, file, binaryName) {
138
+ const issues = [];
139
+ const callPattern = /\b(?:addObserver|removeObserver|notifyObservers)\s*\(/g;
140
+ let callMatch;
141
+ while ((callMatch = callPattern.exec(strippedContent)) !== null) {
142
+ const openParen = callMatch.index + callMatch[0].length - 1;
143
+ const args = extractCallArguments(strippedContent, openParen);
144
+ if (!args)
145
+ continue;
146
+ // Topic is the second argument for all three observer-service methods:
147
+ // addObserver(observer, topic[, weak]), removeObserver(observer, topic),
148
+ // notifyObservers(subject, topic[, data]).
149
+ const topicArg = args[1];
150
+ if (topicArg === undefined)
151
+ continue;
152
+ const topic = asStringLiteral(topicArg);
153
+ if (topic === null)
154
+ continue;
155
+ if (isKnownFirefoxTopic(topic))
156
+ continue;
157
+ if (topic.toLowerCase().includes(binaryName) && !/^[\w]+-[a-z]+-[a-z]+/.test(topic)) {
158
+ issues.push({
159
+ file,
160
+ check: 'observer-topic-naming',
161
+ message: `Observer topic "${topic}" should follow "${binaryName}-<noun>-<verb>" naming convention.`,
162
+ severity: 'warning',
163
+ });
164
+ }
165
+ }
166
+ return issues;
167
+ }
168
+ //# sourceMappingURL=patch-lint-observer.js.map
@@ -1,7 +1,9 @@
1
1
  import type { PatchLintIssue } from '../types/commands/index.js';
2
2
  import type { FireForgeConfig } from '../types/config.js';
3
3
  import { type CommentStyle } from './license-headers.js';
4
+ import { lintPatchedCss } from './patch-lint-css.js';
4
5
  export * from './patch-lint-reexports.js';
6
+ export { lintPatchedCss };
5
7
  /**
6
8
  * Counts the total lines in a unified diff and the number of non-binary
7
9
  * text lines, so binary hunks do not inflate patch size checks.
@@ -23,16 +25,6 @@ export declare function isTestFile(file: string): boolean;
23
25
  * Detects comment style from file extension for license header checks.
24
26
  */
25
27
  export declare function commentStyleForFile(file: string): CommentStyle | null;
26
- /**
27
- * Lints patched CSS files for introduced raw color values and non-tokenized
28
- * custom properties.
29
- *
30
- * @param repoDir - Absolute path to the engine (repository) directory
31
- * @param affectedFiles - File paths (relative to repoDir) affected by the patch
32
- * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
33
- * @returns Array of lint issues found
34
- */
35
- export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string, config?: FireForgeConfig): Promise<PatchLintIssue[]>;
36
28
  /**
37
29
  * Checks new files for required license headers.
38
30
  *
@@ -121,6 +113,35 @@ export declare function lintPatchSize(filesAffected: string[], lineCount: number
121
113
  * @returns Warning-level lint issues for files missing any recognized header
122
114
  */
123
115
  export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles: string[], newFiles: Set<string>): Promise<PatchLintIssue[]>;
116
+ /**
117
+ * Optional behaviour switches for {@link lintExportedPatch}.
118
+ */
119
+ export interface LintExportedPatchOptions {
120
+ /**
121
+ * Skip the patch-size rules (`large-patch-files` / `large-patch-lines`).
122
+ * The ad-hoc `fireforge lint <files>` path passes a cross-patch file
123
+ * list that does not correspond to a single patch, so it suppresses the
124
+ * synthetic combined-list size check here and re-evaluates the size
125
+ * rules per owning patch instead — never synthesising a phantom
126
+ * oversized patch from the operator's file selection.
127
+ */
128
+ skipPatchSize?: boolean;
129
+ /**
130
+ * Restrict checkJs diagnostics to these repo-relative files; module
131
+ * resolution still spans every owned file in `patchQueueCtx`. Export and
132
+ * re-export pass the patch under export so cross-patch `resource:///`
133
+ * imports resolve against the whole queue while only that patch's
134
+ * findings surface.
135
+ */
136
+ checkJsReportScope?: ReadonlySet<string>;
137
+ /**
138
+ * Pre-computed checkJs issues for this patch. When provided, the internal
139
+ * checkJs invocation is skipped and these are appended verbatim — the
140
+ * per-patch lint path builds one queue-wide checkJs program and
141
+ * attributes findings per patch instead of rebuilding per patch.
142
+ */
143
+ precomputedCheckJs?: readonly PatchLintIssue[];
144
+ }
124
145
  /**
125
146
  * Runs all patch lint checks and returns combined issues.
126
147
  *
@@ -140,6 +161,8 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
140
161
  * per-patch manifest context (re-export, per-patch lint) should
141
162
  * pass this; aggregate-mode callers without a specific patch
142
163
  * context skip it and fall through to auto-detection.
164
+ * @param options - Optional behaviour switches; see
165
+ * {@link LintExportedPatchOptions}.
143
166
  * @returns Array of all lint issues found
144
167
  */
145
- export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<PatchLintIssue[]>;
168
+ export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding', options?: LintExportedPatchOptions): Promise<PatchLintIssue[]>;