@hominis/fireforge 0.11.2 → 0.12.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 (34) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +54 -1
  3. package/dist/src/commands/export-shared.d.ts +2 -1
  4. package/dist/src/commands/export-shared.js +3 -2
  5. package/dist/src/commands/furnace/create.js +1 -1
  6. package/dist/src/commands/furnace/deploy.js +1 -1
  7. package/dist/src/commands/furnace/override.js +1 -1
  8. package/dist/src/commands/furnace/refresh.js +1 -1
  9. package/dist/src/commands/furnace/remove.js +1 -1
  10. package/dist/src/commands/furnace/rename.js +1 -1
  11. package/dist/src/commands/furnace/scan.js +1 -1
  12. package/dist/src/commands/lint.js +8 -3
  13. package/dist/src/core/ast-utils.d.ts +10 -0
  14. package/dist/src/core/ast-utils.js +18 -0
  15. package/dist/src/core/config-paths.d.ts +2 -2
  16. package/dist/src/core/config-paths.js +3 -0
  17. package/dist/src/core/config-validate.js +21 -3
  18. package/dist/src/core/file-lock.js +39 -2
  19. package/dist/src/core/furnace-apply.js +2 -1
  20. package/dist/src/core/furnace-config.js +6 -2
  21. package/dist/src/core/patch-apply.js +26 -4
  22. package/dist/src/core/patch-lint-checkjs.d.ts +21 -0
  23. package/dist/src/core/patch-lint-checkjs.js +225 -0
  24. package/dist/src/core/patch-lint-cross.d.ts +1 -0
  25. package/dist/src/core/patch-lint-cross.js +7 -0
  26. package/dist/src/core/patch-lint-jsdoc.d.ts +21 -0
  27. package/dist/src/core/patch-lint-jsdoc.js +259 -0
  28. package/dist/src/core/patch-lint-ownership.d.ts +25 -0
  29. package/dist/src/core/patch-lint-ownership.js +43 -0
  30. package/dist/src/core/patch-lint.d.ts +7 -2
  31. package/dist/src/core/patch-lint.js +30 -30
  32. package/dist/src/types/config.d.ts +9 -0
  33. package/dist/src/utils/paths.js +3 -1
  34. package/package.json +1 -1
@@ -0,0 +1,225 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Optional TypeScript `checkJs` pass for patch-owned `.sys.mjs` files.
4
+ *
5
+ * Loads the TypeScript compiler API via dynamic import so it is only
6
+ * required when `patchLint.checkJs` is enabled in `fireforge.json`.
7
+ * TypeScript remains a dev-dependency — if a user enables checkJs
8
+ * without installing it, the pass emits a clear error explaining
9
+ * how to fix it.
10
+ *
11
+ * Separated from `patch-lint.ts` to keep both files within the
12
+ * project's per-file line budget.
13
+ */
14
+ import { resolve } from 'node:path';
15
+ import { pathExists } from '../utils/fs.js';
16
+ import { verbose } from '../utils/logger.js';
17
+ // ---------------------------------------------------------------------------
18
+ // Firefox globals shim
19
+ // ---------------------------------------------------------------------------
20
+ const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
21
+ /**
22
+ * Minimal `.d.ts` shim for Firefox privileged-scope globals.
23
+ *
24
+ * Firefox source is plain JS — no TypeScript allowed. The shim lets
25
+ * `checkJs` run without reporting "cannot find name" for the most
26
+ * common Mozilla APIs. Types are intentionally loose (`any`) because
27
+ * full Firefox type coverage is out of scope.
28
+ *
29
+ * Notable patterns that require shimming:
30
+ * - `const lazy = {};` + `ChromeUtils.defineESModuleGetters(lazy, { ... })`
31
+ * populates `lazy` at runtime; we declare it as `Record<string, any>`.
32
+ * - `Services.obs`, `Services.prefs`, etc. are XPCOM service accessors.
33
+ * - `Ci`, `Cc`, `Cr`, `Cu` are XPCOM component shortcuts.
34
+ * - Browser chrome globals like `gBrowser`, `gURLBar` are common in
35
+ * content scripts wired via `browser.js`.
36
+ */
37
+ const FIREFOX_GLOBALS_SHIM = `
38
+ declare var Services: any;
39
+ declare var ChromeUtils: {
40
+ defineESModuleGetters(target: any, modules: Record<string, string>): void;
41
+ importESModule(specifier: string): any;
42
+ import(specifier: string): any;
43
+ defineModuleGetter(target: any, name: string, specifier: string): void;
44
+ generateQI(interfaces: any[]): Function;
45
+ isClassInfo(obj: any): boolean;
46
+ };
47
+ declare var Cu: any;
48
+ declare var Ci: any;
49
+ declare var Cc: any;
50
+ declare var Cr: any;
51
+ declare var Components: any;
52
+ declare var XPCOMUtils: any;
53
+ declare var lazy: Record<string, any>;
54
+ declare var PathUtils: any;
55
+ declare var IOUtils: any;
56
+ declare var FileUtils: any;
57
+ declare var gBrowser: any;
58
+ declare var gURLBar: any;
59
+ declare var gNavigatorBundle: any;
60
+ declare var AppConstants: any;
61
+ `;
62
+ // ---------------------------------------------------------------------------
63
+ // Diagnostic filtering
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * TS diagnostic codes to suppress because they are inherent to
67
+ * checking Firefox JS files outside of Mozilla's own build system.
68
+ *
69
+ * Firefox uses `resource://` and `chrome://` URL schemes for module
70
+ * imports. TypeScript's module resolver cannot follow these, so every
71
+ * import from an upstream Firefox module produces a spurious
72
+ * "Cannot find module" error. Filtering these out is essential to
73
+ * keep the checkJs pass usable — otherwise every file with an import
74
+ * would be buried in false positives.
75
+ */
76
+ const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
77
+ 2307, // Cannot find module '{0}' or its corresponding type declarations.
78
+ 2306, // File '{0}' is not a module.
79
+ 2305, // Module '{0}' has no exported member '{1}'.
80
+ 2792, // Cannot find module '{0}'. Did you mean to set the 'moduleResolution' option...
81
+ 2304, // Cannot find name '{0}'. (for globals we missed in the shim)
82
+ 2552, // Cannot find name '{0}'. Did you mean '{1}'?
83
+ 2580, // Cannot find name '{0}'. Do you need to install type definitions...
84
+ 7016, // Could not find a declaration file for module '{0}'.
85
+ ]);
86
+ // ---------------------------------------------------------------------------
87
+ // Public API
88
+ // ---------------------------------------------------------------------------
89
+ /**
90
+ * Runs TypeScript's checkJs pass on patch-owned `.sys.mjs` files.
91
+ *
92
+ * @param repoDir - Absolute path to the engine (repository) directory
93
+ * @param patchOwnedFiles - Set of patch-owned `.sys.mjs` file paths (relative to repoDir)
94
+ * @returns Array of lint issues from TS diagnostics
95
+ */
96
+ export async function runCheckJs(repoDir, patchOwnedFiles) {
97
+ if (patchOwnedFiles.size === 0)
98
+ return [];
99
+ // Dynamic import — typescript stays as a dev dependency
100
+ let ts;
101
+ try {
102
+ ts = await import('typescript');
103
+ }
104
+ catch {
105
+ return [
106
+ {
107
+ file: '(checkJs)',
108
+ check: 'checkjs-type-error',
109
+ message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
110
+ 'Run "npm install typescript" to enable type checking.',
111
+ severity: 'error',
112
+ },
113
+ ];
114
+ }
115
+ // Resolve absolute paths for root files, filtering to files that exist
116
+ const rootFiles = [];
117
+ const ownedAbsolute = new Set();
118
+ for (const rel of patchOwnedFiles) {
119
+ const abs = resolve(repoDir, rel);
120
+ if (await pathExists(abs)) {
121
+ rootFiles.push(abs);
122
+ ownedAbsolute.add(abs);
123
+ }
124
+ }
125
+ if (rootFiles.length === 0)
126
+ return [];
127
+ const shimPath = resolve(repoDir, SHIM_FILENAME);
128
+ rootFiles.push(shimPath);
129
+ const options = {
130
+ allowJs: true,
131
+ checkJs: true,
132
+ noEmit: true,
133
+ strict: false,
134
+ target: ts.ScriptTarget.ESNext,
135
+ module: ts.ModuleKind.ESNext,
136
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
137
+ skipLibCheck: true,
138
+ // Do not follow import/reference directives into the Firefox tree.
139
+ // We only want to check the patch-owned files themselves.
140
+ // Without this, TS would try (and fail) to resolve every
141
+ // resource:// and chrome:// import, flooding the output with
142
+ // "Cannot find module" errors for upstream Firefox modules.
143
+ noResolve: true,
144
+ // Suppress implicit-any noise — Firefox code rarely has full type
145
+ // annotations and drowning users in thousands of implicit-any
146
+ // errors defeats the purpose of a focused check.
147
+ noImplicitAny: false,
148
+ };
149
+ // Custom compiler host: reads patch-owned files from disk, returns
150
+ // the shim for the shim path, and returns empty content for
151
+ // anything else to avoid reading the full Firefox tree.
152
+ const defaultHost = ts.createCompilerHost(options);
153
+ const host = {
154
+ ...defaultHost,
155
+ getSourceFile(fileName, languageVersion, onError) {
156
+ if (fileName === shimPath) {
157
+ return ts.createSourceFile(fileName, FIREFOX_GLOBALS_SHIM, languageVersion, true);
158
+ }
159
+ if (ownedAbsolute.has(fileName)) {
160
+ return defaultHost.getSourceFile(fileName, languageVersion, onError);
161
+ }
162
+ // For lib files (lib.es*.d.ts) delegate to the default host
163
+ // so built-in types like Promise, Array, etc. are available.
164
+ if (fileName.includes('lib.') && fileName.endsWith('.d.ts')) {
165
+ return defaultHost.getSourceFile(fileName, languageVersion, onError);
166
+ }
167
+ // Return an empty source file for anything else to avoid
168
+ // reading unrelated Firefox source files.
169
+ return ts.createSourceFile(fileName, '', languageVersion, true);
170
+ },
171
+ fileExists(fileName) {
172
+ if (fileName === shimPath)
173
+ return true;
174
+ if (ownedAbsolute.has(fileName))
175
+ return true;
176
+ return defaultHost.fileExists(fileName);
177
+ },
178
+ readFile(fileName) {
179
+ if (fileName === shimPath)
180
+ return FIREFOX_GLOBALS_SHIM;
181
+ return defaultHost.readFile(fileName);
182
+ },
183
+ };
184
+ const program = ts.createProgram(rootFiles, options, host);
185
+ const allDiagnostics = [
186
+ ...program.getSemanticDiagnostics(),
187
+ ...program.getSyntacticDiagnostics(),
188
+ ];
189
+ // Filter to diagnostics originating in patch-owned files only,
190
+ // and suppress module-resolution / unknown-name noise that is
191
+ // inherent to checking Firefox JS outside Mozilla's build system.
192
+ const issues = [];
193
+ for (const diag of allDiagnostics) {
194
+ if (SUPPRESSED_DIAGNOSTIC_CODES.has(diag.code))
195
+ continue;
196
+ const sourceFile = diag.file;
197
+ if (!sourceFile)
198
+ continue;
199
+ if (!ownedAbsolute.has(sourceFile.fileName))
200
+ continue;
201
+ const lineInfo = sourceFile.getLineAndCharacterOfPosition(diag.start ?? 0);
202
+ const line = lineInfo.line + 1;
203
+ const messageText = typeof diag.messageText === 'string'
204
+ ? diag.messageText
205
+ : ts.flattenDiagnosticMessageText(diag.messageText, '\n');
206
+ // Find the relative path for the issue
207
+ let relPath = sourceFile.fileName;
208
+ for (const [rel, abs] of [...patchOwnedFiles].map((r) => [r, resolve(repoDir, r)])) {
209
+ if (abs === sourceFile.fileName) {
210
+ relPath = rel;
211
+ break;
212
+ }
213
+ }
214
+ const severity = diag.category === ts.DiagnosticCategory.Error ? 'error' : 'warning';
215
+ issues.push({
216
+ file: relPath,
217
+ check: 'checkjs-type-error',
218
+ message: `Line ${line}: ${messageText}`,
219
+ severity,
220
+ });
221
+ }
222
+ verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s), found ${issues.length} issue(s)`);
223
+ return issues;
224
+ }
225
+ //# sourceMappingURL=patch-lint-checkjs.js.map
@@ -113,6 +113,7 @@ export declare function isForwardImportableFile(path: string): boolean;
113
113
  * - `import "specifier"` (side-effect imports — the `from`
114
114
  * clause is optional in the regex)
115
115
  * - `import("specifier")` (dynamic imports)
116
+ * - ChromeUtils.importESModule("specifier")
116
117
  * - ChromeUtils.defineESModuleGetters(obj, { Name: "specifier", ... })
117
118
  *
118
119
  * Returns the raw specifier strings — callers should take the leaf basename
@@ -156,6 +156,7 @@ export function isForwardImportableFile(path) {
156
156
  * - `import "specifier"` (side-effect imports — the `from`
157
157
  * clause is optional in the regex)
158
158
  * - `import("specifier")` (dynamic imports)
159
+ * - ChromeUtils.importESModule("specifier")
159
160
  * - ChromeUtils.defineESModuleGetters(obj, { Name: "specifier", ... })
160
161
  *
161
162
  * Returns the raw specifier strings — callers should take the leaf basename
@@ -282,6 +283,12 @@ export function extractImportSpecifiersWithLines(source) {
282
283
  if (match[1])
283
284
  results.push({ specifier: match[1], line: offsetToLine(match.index) });
284
285
  }
286
+ // ChromeUtils.importESModule("resource://...") — Firefox single-module import
287
+ const chromeUtilsPattern = /ChromeUtils\.importESModule\s*\(\s*["']([^"']+)["']/g;
288
+ while ((match = chromeUtilsPattern.exec(stripped)) !== null) {
289
+ if (match[1])
290
+ results.push({ specifier: match[1], line: offsetToLine(match.index) });
291
+ }
285
292
  collectGetterSpecifiers(stripped, results, offsetToLine);
286
293
  return results;
287
294
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * AST-based JSDoc validation for exported declarations in `.sys.mjs`
3
+ * modules. Uses Acorn (already a runtime dependency) to parse the
4
+ * module and inspects JSDoc comments via the `onComment` callback.
5
+ *
6
+ * Separated from `patch-lint.ts` to keep both files within the
7
+ * project's per-file line budget.
8
+ */
9
+ export type JsDocCheck = 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns';
10
+ export interface JsDocIssue {
11
+ line: number;
12
+ check: JsDocCheck;
13
+ message: string;
14
+ }
15
+ /**
16
+ * Validates JSDoc on exported declarations in a `.sys.mjs` source file.
17
+ *
18
+ * @param source - File content
19
+ * @returns Array of JSDoc issues found
20
+ */
21
+ export declare function validateExportJsDoc(source: string): JsDocIssue[];
@@ -0,0 +1,259 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * AST-based JSDoc validation for exported declarations in `.sys.mjs`
4
+ * modules. Uses Acorn (already a runtime dependency) to parse the
5
+ * module and inspects JSDoc comments via the `onComment` callback.
6
+ *
7
+ * Separated from `patch-lint.ts` to keep both files within the
8
+ * project's per-file line budget.
9
+ */
10
+ import { parseModule } from './ast-utils.js';
11
+ // ---------------------------------------------------------------------------
12
+ // JSDoc comment helpers
13
+ // ---------------------------------------------------------------------------
14
+ function isJsDocComment(comment) {
15
+ return comment.type === 'Block' && comment.value.startsWith('*');
16
+ }
17
+ /**
18
+ * Finds the JSDoc comment immediately preceding `declStart` in the
19
+ * source. "Immediately" means only whitespace and newlines may appear
20
+ * between the comment's closing delimiter and the declaration.
21
+ */
22
+ function findAttachedJsDoc(comments, declStart, source) {
23
+ for (let i = comments.length - 1; i >= 0; i--) {
24
+ const c = comments[i];
25
+ if (!c || !isJsDocComment(c))
26
+ continue;
27
+ const commentEnd = c.end;
28
+ if (commentEnd > declStart)
29
+ continue;
30
+ const between = source.slice(commentEnd, declStart);
31
+ if (/^\s*$/.test(between))
32
+ return c;
33
+ break;
34
+ }
35
+ return undefined;
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // JSDoc tag parsing
39
+ // ---------------------------------------------------------------------------
40
+ function extractParamNames(jsDoc) {
41
+ const names = [];
42
+ const paramPattern = /@param\s+(?:\{[^}]*\}\s+)?(\w+)/g;
43
+ let m;
44
+ while ((m = paramPattern.exec(jsDoc)) !== null) {
45
+ if (m[1])
46
+ names.push(m[1]);
47
+ }
48
+ return names;
49
+ }
50
+ function hasReturnsTag(jsDoc) {
51
+ return /@returns?\b/.test(jsDoc);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Return-statement detection
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Returns true if any direct `ReturnStatement` in the function body
58
+ * has a non-null argument. Does not recurse into nested functions or
59
+ * arrow expressions which have their own return semantics.
60
+ */
61
+ function functionReturnsValue(node) {
62
+ return walkForReturn(node.body);
63
+ }
64
+ function walkForReturn(node) {
65
+ if (node.type === 'ReturnStatement') {
66
+ return node.argument != null;
67
+ }
68
+ if (node.type === 'FunctionDeclaration' ||
69
+ node.type === 'FunctionExpression' ||
70
+ node.type === 'ArrowFunctionExpression') {
71
+ return false;
72
+ }
73
+ for (const key of Object.keys(node)) {
74
+ if (key === 'type')
75
+ continue;
76
+ const val = node[key];
77
+ if (val && typeof val === 'object') {
78
+ if (Array.isArray(val)) {
79
+ for (const child of val) {
80
+ if (child && typeof child === 'object' && 'type' in child) {
81
+ if (walkForReturn(child))
82
+ return true;
83
+ }
84
+ }
85
+ }
86
+ else if ('type' in val) {
87
+ if (walkForReturn(val))
88
+ return true;
89
+ }
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+ function findLocalDeclaration(body, name) {
95
+ for (const stmt of body) {
96
+ if (stmt.type === 'FunctionDeclaration') {
97
+ const fn = stmt;
98
+ if (fn.id.name === name)
99
+ return stmt;
100
+ }
101
+ else if (stmt.type === 'ClassDeclaration') {
102
+ const cls = stmt;
103
+ if (cls.id.name === name)
104
+ return stmt;
105
+ }
106
+ else if (stmt.type === 'VariableDeclaration') {
107
+ const varDecl = stmt;
108
+ for (const d of varDecl.declarations) {
109
+ if (d.id.type === 'Identifier' && d.id.name === name) {
110
+ return varDecl;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Line number helpers
119
+ // ---------------------------------------------------------------------------
120
+ function lineAt(source, offset) {
121
+ let line = 1;
122
+ for (let i = 0; i < offset && i < source.length; i++) {
123
+ if (source[i] === '\n')
124
+ line++;
125
+ }
126
+ return line;
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Core validation
130
+ // ---------------------------------------------------------------------------
131
+ function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
132
+ const name = fn.id.name;
133
+ const start = lookupStart !== undefined ? lookupStart : fn.start;
134
+ const line = lineAt(source, start);
135
+ const jsDoc = findAttachedJsDoc(comments, start, source);
136
+ if (!jsDoc) {
137
+ issues.push({
138
+ line,
139
+ check: 'missing-jsdoc',
140
+ message: `Exported function "${name}" at line ${line} is missing a JSDoc comment.`,
141
+ });
142
+ return;
143
+ }
144
+ const docText = jsDoc.value;
145
+ const actualParams = fn.params
146
+ .map((p) => (p.type === 'Identifier' ? p.name : null))
147
+ .filter((n) => n !== null);
148
+ if (actualParams.length > 0) {
149
+ const docParams = extractParamNames(docText);
150
+ for (const param of actualParams) {
151
+ if (!docParams.includes(param)) {
152
+ issues.push({
153
+ line,
154
+ check: 'jsdoc-param-mismatch',
155
+ message: `Exported function "${name}" at line ${line}: @param "${param}" is missing or misnamed in JSDoc.`,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ if (functionReturnsValue(fn) && !hasReturnsTag(docText)) {
161
+ issues.push({
162
+ line,
163
+ check: 'jsdoc-missing-returns',
164
+ message: `Exported function "${name}" at line ${line} returns a value but JSDoc is missing @returns.`,
165
+ });
166
+ }
167
+ }
168
+ function validateClassDecl(cls, comments, source, issues, lookupStart) {
169
+ const name = cls.id.name;
170
+ const start = lookupStart !== undefined ? lookupStart : cls.start;
171
+ const line = lineAt(source, start);
172
+ const jsDoc = findAttachedJsDoc(comments, start, source);
173
+ if (!jsDoc) {
174
+ issues.push({
175
+ line,
176
+ check: 'missing-jsdoc',
177
+ message: `Exported class "${name}" at line ${line} is missing a JSDoc comment.`,
178
+ });
179
+ }
180
+ }
181
+ function validateVariableDecl(varDecl, comments, source, issues, lookupStart) {
182
+ const start = lookupStart !== undefined ? lookupStart : varDecl.start;
183
+ const jsDoc = findAttachedJsDoc(comments, start, source);
184
+ if (jsDoc)
185
+ return; // has a JSDoc block — sufficient for constants
186
+ for (const decl of varDecl.declarations) {
187
+ const name = decl.id.type === 'Identifier' ? decl.id.name : '<destructured>';
188
+ const line = lineAt(source, start);
189
+ issues.push({
190
+ line,
191
+ check: 'missing-jsdoc',
192
+ message: `Exported constant "${name}" at line ${line} is missing a JSDoc comment.`,
193
+ });
194
+ }
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Public API
198
+ // ---------------------------------------------------------------------------
199
+ /**
200
+ * Validates JSDoc on exported declarations in a `.sys.mjs` source file.
201
+ *
202
+ * @param source - File content
203
+ * @returns Array of JSDoc issues found
204
+ */
205
+ export function validateExportJsDoc(source) {
206
+ const comments = [];
207
+ let ast;
208
+ try {
209
+ ast = parseModule(source, comments);
210
+ }
211
+ catch {
212
+ return [];
213
+ }
214
+ const issues = [];
215
+ const body = ast.body;
216
+ for (const node of body) {
217
+ if (node.type !== 'ExportNamedDeclaration')
218
+ continue;
219
+ const exportNode = node;
220
+ // Case 1: inline export declaration — JSDoc attaches to `export`
221
+ if (exportNode.declaration) {
222
+ const decl = exportNode.declaration;
223
+ const exportStart = exportNode.start;
224
+ if (decl.type === 'FunctionDeclaration') {
225
+ validateFunctionDecl(decl, comments, source, issues, exportStart);
226
+ }
227
+ else if (decl.type === 'ClassDeclaration') {
228
+ validateClassDecl(decl, comments, source, issues, exportStart);
229
+ }
230
+ else if (decl.type === 'VariableDeclaration') {
231
+ validateVariableDecl(decl, comments, source, issues, exportStart);
232
+ }
233
+ continue;
234
+ }
235
+ // Case 2: `export { foo, Bar }` — resolve back to local declarations
236
+ if (exportNode.specifiers.length > 0 && !exportNode.source) {
237
+ for (const spec of exportNode.specifiers) {
238
+ const local = spec.local;
239
+ if (local.type !== 'Identifier')
240
+ continue;
241
+ const localName = local.name;
242
+ const localDecl = findLocalDeclaration(body, localName);
243
+ if (!localDecl)
244
+ continue;
245
+ if (localDecl.type === 'FunctionDeclaration') {
246
+ validateFunctionDecl(localDecl, comments, source, issues);
247
+ }
248
+ else if (localDecl.type === 'ClassDeclaration') {
249
+ validateClassDecl(localDecl, comments, source, issues);
250
+ }
251
+ else {
252
+ validateVariableDecl(localDecl, comments, source, issues);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ return issues;
258
+ }
259
+ //# sourceMappingURL=patch-lint-jsdoc.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Patch ownership resolution for `.sys.mjs` files.
3
+ *
4
+ * A file is "patch-owned" when it was created by the project's patch
5
+ * queue rather than being an upstream Firefox file that happens to be
6
+ * modified. This module computes the set of patch-owned `.sys.mjs`
7
+ * paths so lint rules can scope enforcement to project code only.
8
+ */
9
+ import type { PatchQueueContext } from './patch-lint-cross.js';
10
+ /**
11
+ * Returns the set of file paths that are patch-owned `.sys.mjs` files.
12
+ *
13
+ * A file is patch-owned if:
14
+ * 1. It is newly created in the current diff, OR
15
+ * 2. It was created by an existing patch already in the queue.
16
+ *
17
+ * When no queue context is provided the result is limited to (1),
18
+ * which matches the pre-ownership behavior and keeps callers that
19
+ * do not have access to the patches directory working correctly.
20
+ *
21
+ * @param currentNewFiles - Files newly created in the current diff
22
+ * @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
23
+ * @returns Set of patch-owned `.sys.mjs` file paths
24
+ */
25
+ export declare function resolvePatchOwnedSysMjs(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Patch ownership resolution for `.sys.mjs` files.
4
+ *
5
+ * A file is "patch-owned" when it was created by the project's patch
6
+ * queue rather than being an upstream Firefox file that happens to be
7
+ * modified. This module computes the set of patch-owned `.sys.mjs`
8
+ * paths so lint rules can scope enforcement to project code only.
9
+ */
10
+ import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
11
+ /**
12
+ * Returns the set of file paths that are patch-owned `.sys.mjs` files.
13
+ *
14
+ * A file is patch-owned if:
15
+ * 1. It is newly created in the current diff, OR
16
+ * 2. It was created by an existing patch already in the queue.
17
+ *
18
+ * When no queue context is provided the result is limited to (1),
19
+ * which matches the pre-ownership behavior and keeps callers that
20
+ * do not have access to the patches directory working correctly.
21
+ *
22
+ * @param currentNewFiles - Files newly created in the current diff
23
+ * @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
24
+ * @returns Set of patch-owned `.sys.mjs` file paths
25
+ */
26
+ export function resolvePatchOwnedSysMjs(currentNewFiles, patchQueueCtx) {
27
+ const owned = new Set();
28
+ for (const file of currentNewFiles) {
29
+ if (file.endsWith('.sys.mjs')) {
30
+ owned.add(file);
31
+ }
32
+ }
33
+ if (patchQueueCtx) {
34
+ const creators = collectNewFileCreatorsByPath(patchQueueCtx);
35
+ for (const [file, owners] of creators) {
36
+ if (file.endsWith('.sys.mjs') && owners.length > 0) {
37
+ owned.add(file);
38
+ }
39
+ }
40
+ }
41
+ return owned;
42
+ }
43
+ //# sourceMappingURL=patch-lint-ownership.js.map
@@ -1,8 +1,11 @@
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
+ export { runCheckJs } from './patch-lint-checkjs.js';
4
5
  export { buildPatchQueueContext, collectNewFileCreatorsByPath, type ExtractedSpecifier, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
5
6
  export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
7
+ export { type JsDocCheck, type JsDocIssue, validateExportJsDoc } from './patch-lint-jsdoc.js';
8
+ export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
6
9
  /**
7
10
  * Detects comment style from file extension for license header checks.
8
11
  */
@@ -34,9 +37,10 @@ export declare function lintNewFileHeaders(repoDir: string, newFiles: string[],
34
37
  * @param affectedFiles - File paths (relative to repoDir)
35
38
  * @param newFiles - Set of files that are newly created in this patch
36
39
  * @param config - Project configuration
40
+ * @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
37
41
  * @returns Array of lint issues
38
42
  */
39
- export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig): Promise<PatchLintIssue[]>;
43
+ export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>): Promise<PatchLintIssue[]>;
40
44
  /**
41
45
  * Checks that modifications to existing (non-new) JS/MJS files include at
42
46
  * least one `// BINARYNAME:` comment in the added lines.
@@ -67,6 +71,7 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
67
71
  * @param affectedFiles - File paths (relative to repoDir) affected by the patch
68
72
  * @param diffContent - Raw unified diff string
69
73
  * @param config - Project configuration
74
+ * @param patchQueueCtx - Optional cross-patch context for ownership resolution
70
75
  * @returns Array of all lint issues found
71
76
  */
72
- export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig): Promise<PatchLintIssue[]>;
77
+ export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext): Promise<PatchLintIssue[]>;