@hominis/fireforge 0.15.8 → 0.15.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -3
- package/dist/src/commands/export-shared.d.ts +6 -1
- package/dist/src/commands/export-shared.js +7 -2
- package/dist/src/commands/lint.d.ts +20 -0
- package/dist/src/commands/lint.js +157 -44
- package/dist/src/commands/re-export-files.js +6 -2
- package/dist/src/commands/re-export.js +37 -4
- package/dist/src/core/patch-lint.d.ts +6 -1
- package/dist/src/core/patch-lint.js +14 -1
- package/dist/src/types/commands/options.d.ts +10 -0
- package/dist/src/types/commands/patches.d.ts +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.15.0
|
|
4
4
|
|
|
5
|
+
### Re-export — opt-in `--stamp` and per-patch `lintIgnore`
|
|
6
|
+
|
|
7
|
+
- New `fireforge re-export --stamp` stamps `sourceEsrVersion` on every successfully re-exported patch to the current `firefox.version` from `fireforge.json`. Previously `re-export` only ever refreshed patch bodies and `filesAffected`; version stamping was exclusive to `rebase`'s `stampPatchVersions` call (plus `doctor --repair-patches-manifest`). An operator asked to "re-export targeting a new ESR" had no in-surface signal that the command could not deliver the version half of that request, and had to route through the full rebase flow (which requires a Firefox source re-download) purely to update a version string. `--stamp` closes that gap for the case where the re-export cleanly refreshes every selected patch — a partial run (any skipped or failed patch) refuses to stamp and the success line notes the refusal, so a torn "some bodies refreshed at old version, some at new" state is not representable. The command description and `--help` text now explicitly call out that `re-export` does NOT change `sourceEsrVersion` by default.
|
|
8
|
+
- New optional `lintIgnore: string[]` field on each patch entry in `patches.json` lists lint check IDs to suppress for that patch specifically. Surgical alternative to `--skip-lint` (which downgrades _every_ error to a warning) for the class of patch that is advisory-noisy by nature — cohesive branding bundles, localised-resource packs, auto-generated manifests — where a rule like `large-patch-lines` is not actionable. Threaded through `lintExportedPatch` as an optional `ignoreChecks` filter, honoured by `re-export`, `re-export --files`, and `lint --per-patch`. The file-level `fireforge-ignore:` comment markers for `raw-color-value` and `forward-import` are unchanged; `lintIgnore` fills the gap at the patch level where no per-line marker can exist (the `.patch` body is regenerated on every export). Unknown check IDs are a no-op so the metadata documents the _intent_ to suppress even if the rule is renamed later.
|
|
9
|
+
- Motivating case: re-exporting a 22-patch queue onto 140.9.0esr after `download --force` failed on `001-branding-branding-assets` with `ERROR [large-patch-lines] (patch): Patch is 15665 lines (hard limit: 3000)` — a 57-file branding bundle that genuinely cannot be split. The only escape was `--skip-lint` (downgrades 22 patches' worth of errors) or the full `rebase` flow (already-wasted Firefox download). With `lintIgnore: ["large-patch-lines", "large-patch-files"]` on that one patch, `re-export --all --scan --stamp` now completes in one call.
|
|
10
|
+
|
|
11
|
+
### Lint — `--per-patch` scope and aggregate-mode hint
|
|
12
|
+
|
|
13
|
+
- New `fireforge lint --per-patch` scopes the lint diff to each patch's own `filesAffected` in turn rather than the aggregate `git diff HEAD` across every applied patch. Motivating case: running `fireforge lint` (no args) after `fireforge import` / `fireforge rebase` has just applied a 22-patch queue produces an aggregate diff of every patch summed, which means the patch-size advisory rules (`large-patch-lines`, `large-patch-files`) fire against the sum — e.g. `Patch is 37529 lines`, `Patch affects 126 files` — with `Lint failed` and a non-zero exit code on a repo that is actually in a good state. The aggregate framing reads as a task-specific regression when it is really an artefact of aggregation. `--per-patch` restates the scope so each patch lints as its own isolated diff, honours the patch's own `lintIgnore` entries, and runs the cross-patch rules (`duplicate-new-file-creation`, `forward-import`) once over the whole queue so queue-level findings are not lost by the rescoping. Mutually exclusive with explicit file paths (the two scope contracts are different).
|
|
14
|
+
- Aggregate-mode runs that would otherwise surface a `large-patch-lines` / `large-patch-files` error against a multi-patch queue now print a one-line `NOTE: aggregate diff across all applied patches. Use 'fireforge lint --per-patch' to lint each patch individually; patch-size rules fire against the sum in aggregate mode.` ahead of the failure message, so the operator reaches the per-patch escape hatch without having to read the help text first. The note fires only when the patches directory has at least two entries AND the rule that fired is a patch-size rule — a single-patch queue or a non-size rule behaves identically to before.
|
|
15
|
+
- Per-patch output namespaces every issue with its owning patch filename (`ERROR [relative-import] 001-ui-test.patch :: browser/base/content/a.ts: …`) so triage can attribute findings without cross-referencing patches.json. The passing summary reports how many patches were actually linted (patches with no files on disk or an empty projected diff are silently skipped — they are not a finding).
|
|
16
|
+
|
|
5
17
|
### Test — xpcshell appdir auto-injection
|
|
6
18
|
|
|
7
19
|
- `fireforge test` now auto-injects `--app-path=<absolute>` into mach test invocations whose nearest `xpcshell.toml` sets `firefox-appdir = "browser"` on a rebranded fork (appname != `firefox`). Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored when `appname` is anything other than `firefox`, so `appPath` falls back to `xrePath` (one level above the real app root). The resolver lives in the new `src/core/xpcshell-appdir.ts`: it walks each test path to the nearest `xpcshell.toml`, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest (so an operator who already migrated is not overridden), and otherwise probes `<objDir>/dist/bin/<value>` and `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` for the absolute target. Operator overrides via `--mach-arg=--app-path=…` always win and the resolver is skipped silently when one is detected. Mismatches across multiple test paths (different manifests resolving to different app dirs) and unresolvable manifest values (no candidate under `dist/`) are surfaced as warnings rather than guessed at, so triage can reach the underlying cause instead of debugging a wrong path.
|
package/README.md
CHANGED
|
@@ -64,8 +64,7 @@ npx fireforge export browser/base/content/browser.js --name "custom-toolbar" --c
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
3. Your patch is now in `patches/`.
|
|
67
|
-
|
|
68
|
-
# 4. Reset and import to verify everything applies cleanly:
|
|
67
|
+
4. Reset and import to verify everything applies cleanly:
|
|
69
68
|
|
|
70
69
|
```bash
|
|
71
70
|
npx fireforge reset --yes
|
|
@@ -139,6 +138,13 @@ fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch -
|
|
|
139
138
|
|
|
140
139
|
# Restrict a re-export to a specific file subset
|
|
141
140
|
fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar
|
|
141
|
+
|
|
142
|
+
# Refresh every patch AND stamp sourceEsrVersion from fireforge.json onto each
|
|
143
|
+
# one. Only stamps when every selected patch refreshes cleanly — partial
|
|
144
|
+
# runs refuse to stamp. Use when you re-exported after a manual Firefox
|
|
145
|
+
# bump that did not go through `rebase`. By default `re-export` refreshes
|
|
146
|
+
# patch bodies and filesAffected but does NOT change sourceEsrVersion.
|
|
147
|
+
fireforge re-export --all --scan --stamp
|
|
142
148
|
```
|
|
143
149
|
|
|
144
150
|
### Rebasing on top of a new Firefox version
|
|
@@ -176,12 +182,15 @@ This re-exports the fixed patch and continues applying the remaining stack.
|
|
|
176
182
|
"description": "Replaces default Firefox branding with custom logo",
|
|
177
183
|
"createdAt": "2025-01-15T10:30:00Z",
|
|
178
184
|
"sourceEsrVersion": "140.9.0esr",
|
|
179
|
-
"filesAffected": ["browser/branding/official/logo.png"]
|
|
185
|
+
"filesAffected": ["browser/branding/official/logo.png"],
|
|
186
|
+
"lintIgnore": ["large-patch-lines", "large-patch-files"]
|
|
180
187
|
}
|
|
181
188
|
]
|
|
182
189
|
}
|
|
183
190
|
```
|
|
184
191
|
|
|
192
|
+
The optional `lintIgnore` field lists lint check IDs to suppress for that patch specifically. Useful for the class of patch that is advisory-noisy by nature — a cohesive branding bundle, a localised-resource pack, an auto-generated manifest — where `--skip-lint` is too blunt and a per-line marker cannot exist (the `.patch` body is regenerated on every export). Threaded through `export`, `re-export`, `re-export --files`, and `lint --per-patch`. Unknown check IDs are a no-op.
|
|
193
|
+
|
|
185
194
|
If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
|
|
186
195
|
|
|
187
196
|
</details>
|
|
@@ -191,6 +200,8 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
191
200
|
|
|
192
201
|
`fireforge lint` runs automatically during export, export-all and re-export. Use `--skip-lint` to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
|
|
193
202
|
|
|
203
|
+
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed. On a repo where `fireforge import` or `fireforge rebase` has just applied the full queue, the patch-size rules (`large-patch-lines`, `large-patch-files`) fire against the sum, which reads as "my queue is broken" when it is really an artefact of aggregation. Use `fireforge lint --per-patch` to rescope the diff to each patch's own `filesAffected`, honouring the patch's own `lintIgnore`. Cross-patch rules (`duplicate-new-file-creation`, `forward-import`) still run once over the whole queue either way. Pass explicit file paths to narrow the scope further; the three modes (aggregate, file-scoped, per-patch) are mutually exclusive.
|
|
204
|
+
|
|
194
205
|
| Check | Scope | Severity |
|
|
195
206
|
| ------------------------------ | ------------------------------------------------------------------------- | ------------------------ |
|
|
196
207
|
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
@@ -11,8 +11,13 @@ import type { SpinnerHandle } from '../utils/logger.js';
|
|
|
11
11
|
* @param config - Project configuration
|
|
12
12
|
* @param skipLint - If true, downgrade errors to warnings
|
|
13
13
|
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
14
|
+
* @param ignoreChecks - Optional per-patch set of `check` IDs to suppress
|
|
15
|
+
* (threaded from `PatchMetadata.lintIgnore`). Surgical alternative to
|
|
16
|
+
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
17
|
+
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
18
|
+
* bundle that genuinely cannot be split.
|
|
14
19
|
*/
|
|
15
|
-
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext): Promise<void>;
|
|
20
|
+
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>): Promise<void>;
|
|
16
21
|
/**
|
|
17
22
|
* Resolves patch metadata interactively or from flags, with shared validation.
|
|
18
23
|
* @param options - Export command options
|
|
@@ -18,9 +18,14 @@ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../ut
|
|
|
18
18
|
* @param config - Project configuration
|
|
19
19
|
* @param skipLint - If true, downgrade errors to warnings
|
|
20
20
|
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
21
|
+
* @param ignoreChecks - Optional per-patch set of `check` IDs to suppress
|
|
22
|
+
* (threaded from `PatchMetadata.lintIgnore`). Surgical alternative to
|
|
23
|
+
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
24
|
+
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
25
|
+
* bundle that genuinely cannot be split.
|
|
21
26
|
*/
|
|
22
|
-
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx) {
|
|
23
|
-
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx);
|
|
27
|
+
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks) {
|
|
28
|
+
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx, ignoreChecks);
|
|
24
29
|
if (issues.length === 0)
|
|
25
30
|
return;
|
|
26
31
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
@@ -25,6 +25,26 @@ export interface LintCommandOptions {
|
|
|
25
25
|
* rejected up-front rather than silently ignored.
|
|
26
26
|
*/
|
|
27
27
|
onlyIntroduced?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Lint each patch in the queue as its own isolated diff, rather than
|
|
30
|
+
* the aggregate `git diff HEAD` across all applied patches.
|
|
31
|
+
*
|
|
32
|
+
* Motivating case: running `fireforge lint` (no args) on a repo where
|
|
33
|
+
* `fireforge import` or `fireforge rebase` has just applied the full
|
|
34
|
+
* patch queue produces an aggregate diff (every patch's changes
|
|
35
|
+
* summed). The patch-size advisory rules (`large-patch-lines`,
|
|
36
|
+
* `large-patch-files`) then fire against the sum — e.g. "Patch is
|
|
37
|
+
* 37529 lines" on a queue of 22 individually-fine patches — which
|
|
38
|
+
* reads as a task-specific regression when it is really an artefact
|
|
39
|
+
* of the aggregation. `--per-patch` rescopes the diff to each patch's
|
|
40
|
+
* own `filesAffected`, honours the patch's own `lintIgnore`, and runs
|
|
41
|
+
* the cross-patch rules once over the whole queue so queue-level
|
|
42
|
+
* findings (duplicate creations, forward imports) still surface.
|
|
43
|
+
*
|
|
44
|
+
* Mutually exclusive with passing explicit file paths — the two
|
|
45
|
+
* scope contracts are different.
|
|
46
|
+
*/
|
|
47
|
+
perPatch?: boolean;
|
|
28
48
|
}
|
|
29
49
|
/**
|
|
30
50
|
* Runs the lint command to check engine changes against patch quality rules.
|
|
@@ -8,40 +8,27 @@ import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } fro
|
|
|
8
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
9
9
|
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
10
10
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
11
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
11
12
|
import { GeneralError } from '../errors/base.js';
|
|
12
13
|
import { pathExists } from '../utils/fs.js';
|
|
13
14
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Resolves the diff the lint command should run against. Returns `null` when
|
|
17
|
+
* there is nothing to lint (e.g. no matching files, clean tree, or empty
|
|
18
|
+
* diff content) — callers treat that as the early-exit signal and stop.
|
|
19
|
+
*
|
|
20
|
+
* Extracted from {@link lintCommand} so that function stays under the
|
|
21
|
+
* per-function LOC budget as the command grows; the two file-mode and
|
|
22
|
+
* aggregate-mode branches share no state with the post-lint reporting
|
|
23
|
+
* pipeline, so the split is a pure rename rather than a refactor.
|
|
19
24
|
*/
|
|
20
|
-
|
|
21
|
-
intro('FireForge Lint');
|
|
22
|
-
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
23
|
-
// without a revision to anchor the diff there is no "introduced" subset
|
|
24
|
-
// to scope to — reject the combination up-front so a misconfigured CI
|
|
25
|
-
// invocation fails loud instead of silently treating every error as
|
|
26
|
-
// cumulative and passing.
|
|
27
|
-
if (options.onlyIntroduced && !options.since) {
|
|
28
|
-
throw new GeneralError('--only-introduced requires --since <git-rev> so introduced-vs-cumulative can be distinguished.');
|
|
29
|
-
}
|
|
30
|
-
const paths = getProjectPaths(projectRoot);
|
|
31
|
-
if (!(await pathExists(paths.engine))) {
|
|
32
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
33
|
-
}
|
|
34
|
-
if (!(await isGitRepository(paths.engine))) {
|
|
35
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
36
|
-
}
|
|
37
|
-
let diff;
|
|
25
|
+
async function resolveLintDiff(engineDir, files) {
|
|
38
26
|
if (files.length > 0) {
|
|
39
|
-
// Collect specific files/directories
|
|
40
27
|
const collectedFiles = new Set();
|
|
41
28
|
let fileStatuses;
|
|
42
29
|
let untrackedFiles;
|
|
43
30
|
for (const inputPath of files) {
|
|
44
|
-
const fullInputPath = join(
|
|
31
|
+
const fullInputPath = join(engineDir, inputPath);
|
|
45
32
|
let isDirectory = false;
|
|
46
33
|
try {
|
|
47
34
|
const fileStat = await stat(fullInputPath);
|
|
@@ -52,47 +39,87 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
52
39
|
}
|
|
53
40
|
if (isDirectory) {
|
|
54
41
|
const dirPath = inputPath.endsWith('/') ? inputPath.slice(0, -1) : inputPath;
|
|
55
|
-
const modifiedFiles = await getModifiedFilesInDir(
|
|
56
|
-
const dirUntrackedFiles = await getUntrackedFilesInDir(
|
|
42
|
+
const modifiedFiles = await getModifiedFilesInDir(engineDir, dirPath);
|
|
43
|
+
const dirUntrackedFiles = await getUntrackedFilesInDir(engineDir, dirPath);
|
|
57
44
|
for (const f of modifiedFiles)
|
|
58
45
|
collectedFiles.add(f);
|
|
59
46
|
for (const f of dirUntrackedFiles)
|
|
60
47
|
collectedFiles.add(f);
|
|
61
48
|
}
|
|
62
49
|
else {
|
|
63
|
-
if (!fileStatuses)
|
|
64
|
-
fileStatuses = await getStatusWithCodes(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
untrackedFiles = await getUntrackedFiles(paths.engine);
|
|
68
|
-
}
|
|
50
|
+
if (!fileStatuses)
|
|
51
|
+
fileStatuses = await getStatusWithCodes(engineDir);
|
|
52
|
+
if (!untrackedFiles)
|
|
53
|
+
untrackedFiles = await getUntrackedFiles(engineDir);
|
|
69
54
|
const hasStatus = fileStatuses.some((s) => s.file === inputPath) || untrackedFiles.includes(inputPath);
|
|
70
|
-
if (hasStatus)
|
|
55
|
+
if (hasStatus)
|
|
71
56
|
collectedFiles.add(inputPath);
|
|
72
|
-
}
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
59
|
if (collectedFiles.size === 0) {
|
|
76
60
|
info('No modified files found in the specified paths.');
|
|
77
61
|
outro('Nothing to lint');
|
|
78
|
-
return;
|
|
62
|
+
return null;
|
|
79
63
|
}
|
|
80
|
-
diff = await getDiffForFilesAgainstHead(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Lint all changes
|
|
84
|
-
if (!(await hasChanges(paths.engine))) {
|
|
85
|
-
info('No changes to lint.');
|
|
64
|
+
const diff = await getDiffForFilesAgainstHead(engineDir, [...collectedFiles].sort());
|
|
65
|
+
if (!diff.trim()) {
|
|
66
|
+
info('No diff content to lint.');
|
|
86
67
|
outro('Nothing to lint');
|
|
87
|
-
return;
|
|
68
|
+
return null;
|
|
88
69
|
}
|
|
89
|
-
diff
|
|
70
|
+
return diff;
|
|
90
71
|
}
|
|
72
|
+
if (!(await hasChanges(engineDir))) {
|
|
73
|
+
info('No changes to lint.');
|
|
74
|
+
outro('Nothing to lint');
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const diff = await getAllDiff(engineDir);
|
|
91
78
|
if (!diff.trim()) {
|
|
92
79
|
info('No diff content to lint.');
|
|
93
80
|
outro('Nothing to lint');
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return diff;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Runs the lint command to check engine changes against patch quality rules.
|
|
87
|
+
* @param projectRoot - Root directory of the project
|
|
88
|
+
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
89
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
90
|
+
*/
|
|
91
|
+
export async function lintCommand(projectRoot, files, options = {}) {
|
|
92
|
+
intro('FireForge Lint');
|
|
93
|
+
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
94
|
+
// without a revision to anchor the diff there is no "introduced" subset
|
|
95
|
+
// to scope to — reject the combination up-front so a misconfigured CI
|
|
96
|
+
// invocation fails loud instead of silently treating every error as
|
|
97
|
+
// cumulative and passing.
|
|
98
|
+
if (options.onlyIntroduced && !options.since) {
|
|
99
|
+
throw new GeneralError('--only-introduced requires --since <git-rev> so introduced-vs-cumulative can be distinguished.');
|
|
100
|
+
}
|
|
101
|
+
// `--per-patch` rescopes the diff from "aggregate engine state" to "each
|
|
102
|
+
// patch's own filesAffected". Mixing in explicit file paths would produce
|
|
103
|
+
// an ambiguous set — is the file list an additional filter, or does it
|
|
104
|
+
// replace the per-patch scope? Reject up-front so the operator gets a
|
|
105
|
+
// clear error rather than a silently-narrowed result.
|
|
106
|
+
if (options.perPatch && files.length > 0) {
|
|
107
|
+
throw new GeneralError('--per-patch cannot be combined with explicit file paths. Pass either --per-patch or a file list, not both.');
|
|
108
|
+
}
|
|
109
|
+
const paths = getProjectPaths(projectRoot);
|
|
110
|
+
if (!(await pathExists(paths.engine))) {
|
|
111
|
+
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
112
|
+
}
|
|
113
|
+
if (!(await isGitRepository(paths.engine))) {
|
|
114
|
+
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
115
|
+
}
|
|
116
|
+
if (options.perPatch) {
|
|
117
|
+
await lintPerPatch(projectRoot, paths);
|
|
94
118
|
return;
|
|
95
119
|
}
|
|
120
|
+
const diff = await resolveLintDiff(paths.engine, files);
|
|
121
|
+
if (diff === null)
|
|
122
|
+
return;
|
|
96
123
|
const config = await loadConfig(projectRoot);
|
|
97
124
|
const filesAffected = extractAffectedFiles(diff);
|
|
98
125
|
// Build patch queue context once so it can be shared between the
|
|
@@ -110,6 +137,19 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
110
137
|
if (ctx) {
|
|
111
138
|
issues.push(...lintPatchQueue(ctx));
|
|
112
139
|
}
|
|
140
|
+
// When a queue manifest exists AND files were NOT scoped explicitly, the
|
|
141
|
+
// "diff" we just linted is every applied patch summed together. Patch-
|
|
142
|
+
// size rules (`large-patch-lines`, `large-patch-files`) then fire against
|
|
143
|
+
// the aggregate rather than any individual patch, producing counts like
|
|
144
|
+
// "Patch is 37529 lines" that read as a task-specific regression but are
|
|
145
|
+
// really an artefact of aggregation. Surface a one-line note pointing at
|
|
146
|
+
// `--per-patch` so the operator knows the per-patch scope exists before
|
|
147
|
+
// they read the error message as "my queue is broken".
|
|
148
|
+
const aggregateHintApplicable = files.length === 0 && ctx !== undefined && ctx.entries.length > 1;
|
|
149
|
+
if (aggregateHintApplicable &&
|
|
150
|
+
issues.some((i) => i.check === 'large-patch-lines' || i.check === 'large-patch-files')) {
|
|
151
|
+
info('NOTE: aggregate diff across all applied patches. Use `fireforge lint --per-patch` to lint each patch individually; patch-size rules fire against the sum in aggregate mode.');
|
|
152
|
+
}
|
|
113
153
|
if (issues.length === 0) {
|
|
114
154
|
success('No lint issues found.');
|
|
115
155
|
outro('Lint passed');
|
|
@@ -164,13 +204,83 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
164
204
|
}
|
|
165
205
|
outro('Lint passed with warnings');
|
|
166
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Lints each patch in the queue as its own isolated diff, honouring
|
|
209
|
+
* per-patch `lintIgnore` entries. Cross-patch rules still run once over
|
|
210
|
+
* the whole queue so queue-level findings (duplicate creations, forward
|
|
211
|
+
* imports) are not lost by the rescoping.
|
|
212
|
+
*
|
|
213
|
+
* Kept separate from {@link lintCommand}'s aggregate path because the
|
|
214
|
+
* two scopes have genuinely different contracts — the aggregate path
|
|
215
|
+
* reports what `git diff HEAD` looks like right now, the per-patch
|
|
216
|
+
* path reports what each patch's own slice of that diff looks like.
|
|
217
|
+
* Sharing a loop would hide the distinction and force the caller to
|
|
218
|
+
* decide semantics mid-function.
|
|
219
|
+
*/
|
|
220
|
+
async function lintPerPatch(projectRoot, paths) {
|
|
221
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
222
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
223
|
+
info('No patches in manifest — nothing to lint per-patch.');
|
|
224
|
+
outro('Nothing to lint');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const config = await loadConfig(projectRoot);
|
|
228
|
+
const ctx = await buildPatchQueueContext(paths.patches);
|
|
229
|
+
const issues = [];
|
|
230
|
+
let linted = 0;
|
|
231
|
+
for (const patch of manifest.patches) {
|
|
232
|
+
const existing = [];
|
|
233
|
+
for (const f of patch.filesAffected) {
|
|
234
|
+
if (await pathExists(join(paths.engine, f)))
|
|
235
|
+
existing.push(f);
|
|
236
|
+
}
|
|
237
|
+
if (existing.length === 0)
|
|
238
|
+
continue;
|
|
239
|
+
const diff = await getDiffForFilesAgainstHead(paths.engine, existing);
|
|
240
|
+
if (!diff.trim())
|
|
241
|
+
continue;
|
|
242
|
+
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
243
|
+
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore);
|
|
244
|
+
for (const issue of patchIssues) {
|
|
245
|
+
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
246
|
+
}
|
|
247
|
+
linted++;
|
|
248
|
+
}
|
|
249
|
+
// Cross-patch rules over the whole queue — rescoping per-patch would
|
|
250
|
+
// lose these findings, so they run exactly once against the full
|
|
251
|
+
// context.
|
|
252
|
+
issues.push(...lintPatchQueue(ctx));
|
|
253
|
+
if (issues.length === 0) {
|
|
254
|
+
success(`No lint issues found across ${linted} patch(es).`);
|
|
255
|
+
outro('Lint passed');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const errors = issues.filter((i) => i.severity === 'error');
|
|
259
|
+
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
260
|
+
const notices = issues.filter((i) => i.severity === 'notice');
|
|
261
|
+
for (const issue of notices)
|
|
262
|
+
info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
263
|
+
for (const issue of warnings)
|
|
264
|
+
warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
265
|
+
for (const issue of errors)
|
|
266
|
+
warn(`ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
267
|
+
info(`\nLint (per-patch over ${linted} patch(es)): ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
268
|
+
if (errors.length > 0) {
|
|
269
|
+
outro('Lint failed');
|
|
270
|
+
throw new GeneralError(`Patch lint found ${errors.length} error(s) across ${linted} patch(es). Fix these before exporting.`);
|
|
271
|
+
}
|
|
272
|
+
outro('Lint passed with warnings');
|
|
273
|
+
}
|
|
167
274
|
/** Registers the lint command on the CLI program. */
|
|
168
275
|
export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
169
276
|
program
|
|
170
277
|
.command('lint [paths...]')
|
|
171
|
-
.description('Lint engine changes against patch quality rules'
|
|
278
|
+
.description('Lint engine changes against patch quality rules. Default: aggregate diff against HEAD ' +
|
|
279
|
+
'(every applied patch summed). Use --per-patch for per-patch scope, or pass explicit ' +
|
|
280
|
+
'file paths to narrow to those.')
|
|
172
281
|
.option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
|
|
173
282
|
.option('--only-introduced', 'Fail only on issues tagged [introduced] (requires --since). Cumulative errors still print but do not set a non-zero exit.')
|
|
283
|
+
.option('--per-patch', "Lint each patch in the queue as its own isolated diff. Rescopes patch-size rules so they fire against individual patches rather than the aggregate. Honours each patch's `lintIgnore` entries.")
|
|
174
284
|
.action(withErrorHandling(async (paths, options) => {
|
|
175
285
|
const lintOptions = {};
|
|
176
286
|
if (options.since !== undefined) {
|
|
@@ -179,6 +289,9 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
179
289
|
if (options.onlyIntroduced !== undefined) {
|
|
180
290
|
lintOptions.onlyIntroduced = options.onlyIntroduced;
|
|
181
291
|
}
|
|
292
|
+
if (options.perPatch !== undefined) {
|
|
293
|
+
lintOptions.perPatch = options.perPatch;
|
|
294
|
+
}
|
|
182
295
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
183
296
|
}));
|
|
184
297
|
}
|
|
@@ -71,8 +71,12 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
71
71
|
'Remove those paths from --files or modify them before retrying.', '--files');
|
|
72
72
|
}
|
|
73
73
|
// Run the per-patch lint against the projected diff. This mirrors what
|
|
74
|
-
// runPatchLint does in the standard re-export path.
|
|
75
|
-
|
|
74
|
+
// runPatchLint does in the standard re-export path. The target patch's
|
|
75
|
+
// `lintIgnore` threads through so a shrink of an advisory-noisy-but-
|
|
76
|
+
// intentional patch (branding bundle, localised-resource pack) does not
|
|
77
|
+
// have to choose between `--skip-lint` (blunt) and the full rebase path.
|
|
78
|
+
const ignoreChecks = target.lintIgnore?.length ? new Set(target.lintIgnore) : undefined;
|
|
79
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks);
|
|
76
80
|
// Project the cross-patch context: replace the target entry with its
|
|
77
81
|
// would-be shrunken self (new diff + new newFiles + new
|
|
78
82
|
// modifiedFileAdditions). The projected entry must repopulate both
|
|
@@ -6,7 +6,7 @@ import { isGitRepository } from '../core/git.js';
|
|
|
6
6
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
7
7
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
8
8
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
9
|
-
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, } from '../core/patch-manifest.js';
|
|
9
|
+
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
10
10
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -97,7 +97,14 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
97
97
|
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// Thread the patch's own `lintIgnore` list through so the per-patch
|
|
101
|
+
// suppression honoured by export/export-all is also honoured here.
|
|
102
|
+
// Without this, `re-export` could not refresh an advisory-noisy but
|
|
103
|
+
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
104
|
+
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
105
|
+
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
106
|
+
const ignoreChecks = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
107
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks);
|
|
101
108
|
if (isDryRun) {
|
|
102
109
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
103
110
|
}
|
|
@@ -219,13 +226,16 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
219
226
|
}
|
|
220
227
|
const config = await loadConfig(projectRoot);
|
|
221
228
|
let reExported = 0;
|
|
229
|
+
const reExportedFilenames = [];
|
|
222
230
|
const progress = spinner('Preparing re-export...');
|
|
223
231
|
for (const patch of selectedPatches) {
|
|
224
232
|
progress.message(`Re-exporting ${patch.filename}...`);
|
|
225
233
|
try {
|
|
226
234
|
const exported = await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config);
|
|
227
|
-
if (exported)
|
|
235
|
+
if (exported) {
|
|
228
236
|
reExported++;
|
|
237
|
+
reExportedFilenames.push(patch.filename);
|
|
238
|
+
}
|
|
229
239
|
}
|
|
230
240
|
catch (error) {
|
|
231
241
|
warn(`Failed to re-export ${patch.filename}`);
|
|
@@ -236,14 +246,34 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
236
246
|
progress.error('Re-export failed');
|
|
237
247
|
throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
|
|
238
248
|
}
|
|
249
|
+
// `--stamp` only fires on a run where every selected patch refreshed
|
|
250
|
+
// cleanly. A partial success would leave some patches with a stale body
|
|
251
|
+
// but a new version — the opposite of the "what I tested, what the
|
|
252
|
+
// manifest says" invariant `sourceEsrVersion` exists to record. A
|
|
253
|
+
// non-empty `reExportedFilenames` with fewer entries than `selectedPatches`
|
|
254
|
+
// means a lint failure or missing-file skip landed somewhere in the loop,
|
|
255
|
+
// which we refuse to version-stamp through.
|
|
256
|
+
const shouldStamp = options.stamp === true && !isDryRun && reExported > 0 && reExported === selectedPatches.length;
|
|
257
|
+
if (shouldStamp) {
|
|
258
|
+
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version);
|
|
259
|
+
}
|
|
239
260
|
if (isDryRun) {
|
|
240
261
|
progress.stop('Dry run complete');
|
|
241
262
|
success(`[dry-run] Would re-export ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
263
|
+
if (options.stamp === true) {
|
|
264
|
+
info(`[dry-run] Would stamp sourceEsrVersion=${config.firefox.version} on ${reExported} patch(es)`);
|
|
265
|
+
}
|
|
242
266
|
outro('Dry run complete');
|
|
243
267
|
}
|
|
244
268
|
else {
|
|
245
269
|
progress.stop('Re-export complete');
|
|
246
270
|
success(`Re-exported ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
271
|
+
if (shouldStamp) {
|
|
272
|
+
success(`Stamped sourceEsrVersion=${config.firefox.version} on ${reExportedFilenames.length} patch(es)`);
|
|
273
|
+
}
|
|
274
|
+
else if (options.stamp === true && reExported !== selectedPatches.length) {
|
|
275
|
+
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
276
|
+
}
|
|
247
277
|
outro('Re-export complete');
|
|
248
278
|
}
|
|
249
279
|
}
|
|
@@ -251,7 +281,9 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
251
281
|
export function registerReExport(program, { getProjectRoot, withErrorHandling }) {
|
|
252
282
|
program
|
|
253
283
|
.command('re-export [patches...]')
|
|
254
|
-
.description('
|
|
284
|
+
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
285
|
+
'state. Does NOT change sourceEsrVersion by default — use --stamp or run rebase for ' +
|
|
286
|
+
'version stamping.')
|
|
255
287
|
.option('-a, --all', 'Re-export all patches')
|
|
256
288
|
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
257
289
|
.option('--files <paths>', 'Restrict the re-exported filesAffected to this comma-separated list (single target patch only)', (value) => value
|
|
@@ -262,6 +294,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
262
294
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
263
295
|
.option('-y, --yes', 'Skip confirmation when --files shrinks a patch (required for non-TTY)')
|
|
264
296
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
297
|
+
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceEsrVersion in patches.json to firefox.version from fireforge.json. No effect on a partial run.")
|
|
265
298
|
.action(withErrorHandling(async (patches, options) => {
|
|
266
299
|
await reExportCommand(getProjectRoot(), patches, pickDefined(options));
|
|
267
300
|
}));
|
|
@@ -89,6 +89,11 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
|
|
|
89
89
|
* @param diffContent - Raw unified diff string
|
|
90
90
|
* @param config - Project configuration
|
|
91
91
|
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
92
|
+
* @param ignoreChecks - Optional set of per-patch `check` IDs to drop from the
|
|
93
|
+
* returned issues. Threaded from `PatchMetadata.lintIgnore` so a patch that
|
|
94
|
+
* is advisory-noisy by nature (a cohesive branding bundle, auto-generated
|
|
95
|
+
* manifest, etc.) can opt out of a specific rule without reaching for the
|
|
96
|
+
* blunt `--skip-lint` hammer. Not mutated by this function.
|
|
92
97
|
* @returns Array of all lint issues found
|
|
93
98
|
*/
|
|
94
|
-
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext): Promise<PatchLintIssue[]>;
|
|
99
|
+
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
|
|
@@ -455,9 +455,14 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
|
|
|
455
455
|
* @param diffContent - Raw unified diff string
|
|
456
456
|
* @param config - Project configuration
|
|
457
457
|
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
458
|
+
* @param ignoreChecks - Optional set of per-patch `check` IDs to drop from the
|
|
459
|
+
* returned issues. Threaded from `PatchMetadata.lintIgnore` so a patch that
|
|
460
|
+
* is advisory-noisy by nature (a cohesive branding bundle, auto-generated
|
|
461
|
+
* manifest, etc.) can opt out of a specific rule without reaching for the
|
|
462
|
+
* blunt `--skip-lint` hammer. Not mutated by this function.
|
|
458
463
|
* @returns Array of all lint issues found
|
|
459
464
|
*/
|
|
460
|
-
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx) {
|
|
465
|
+
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx, ignoreChecks) {
|
|
461
466
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
462
467
|
const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
|
|
463
468
|
const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
|
|
@@ -482,6 +487,14 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
482
487
|
const checkJsIssues = await runCheckJs(repoDir, patchOwnedFiles);
|
|
483
488
|
issues.push(...checkJsIssues);
|
|
484
489
|
}
|
|
490
|
+
// Filter out ignored checks last so every rule still runs (keeps the
|
|
491
|
+
// implementation uniform) but suppressed rules do not surface. We do not
|
|
492
|
+
// reclassify severities — an ignored error simply drops, mirroring how
|
|
493
|
+
// inline `fireforge-ignore: <check>` markers work in the CSS and
|
|
494
|
+
// forward-import rules.
|
|
495
|
+
if (ignoreChecks && ignoreChecks.size > 0) {
|
|
496
|
+
return issues.filter((issue) => !ignoreChecks.has(issue.check));
|
|
497
|
+
}
|
|
485
498
|
return issues;
|
|
486
499
|
}
|
|
487
500
|
//# sourceMappingURL=patch-lint.js.map
|
|
@@ -155,6 +155,16 @@ export interface ReExportOptions {
|
|
|
155
155
|
yes?: boolean;
|
|
156
156
|
/** Bypass cross-patch lint refusal on projected shrink state */
|
|
157
157
|
forceUnsafe?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* After every selected patch re-exports cleanly, stamp each re-exported
|
|
160
|
+
* patch's `sourceEsrVersion` in `patches.json` to the current
|
|
161
|
+
* `firefox.version` from `fireforge.json`. Opt-in because the default
|
|
162
|
+
* contract of `re-export` is "refresh the patch body and filesAffected";
|
|
163
|
+
* version stamping is normally a `rebase` responsibility. Use this when
|
|
164
|
+
* re-exporting after a manual Firefox bump that did not go through
|
|
165
|
+
* `rebase`.
|
|
166
|
+
*/
|
|
167
|
+
stamp?: boolean;
|
|
158
168
|
}
|
|
159
169
|
/**
|
|
160
170
|
* Options for the rebase command.
|
|
@@ -51,6 +51,28 @@ export interface PatchMetadata {
|
|
|
51
51
|
sourceEsrVersion: string;
|
|
52
52
|
/** Array of file paths affected by this patch */
|
|
53
53
|
filesAffected: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Optional per-patch list of lint check IDs to suppress when this patch
|
|
56
|
+
* is the target of `export`, `export-all`, or `re-export`. Exists for
|
|
57
|
+
* the class of patch that is advisory-noisy by nature — a cohesive
|
|
58
|
+
* branding bundle, a localised-resource pack, an auto-generated
|
|
59
|
+
* manifest — where the generic `large-patch-lines` / `large-patch-files`
|
|
60
|
+
* thresholds do not apply but `--skip-lint` (which silences *all*
|
|
61
|
+
* errors, not just the one that does not apply) is too coarse a hammer.
|
|
62
|
+
*
|
|
63
|
+
* Previously the only escape hatches were `--skip-lint` (blunt) or the
|
|
64
|
+
* full `rebase` flow (refreshes the same patch through a code path that
|
|
65
|
+
* silently skips `runPatchLint` — an asymmetry that forced operators
|
|
66
|
+
* through a multi-minute Firefox source re-download just to refresh
|
|
67
|
+
* one patch body).
|
|
68
|
+
*
|
|
69
|
+
* Values are free-form check IDs (e.g. `"large-patch-lines"`,
|
|
70
|
+
* `"large-patch-files"`). Checks not listed here still run normally.
|
|
71
|
+
* An entry for an unknown check ID is a no-op — the patch metadata
|
|
72
|
+
* documents the *intent* to suppress even if the check is later
|
|
73
|
+
* renamed or removed.
|
|
74
|
+
*/
|
|
75
|
+
lintIgnore?: string[];
|
|
54
76
|
}
|
|
55
77
|
/**
|
|
56
78
|
* Schema for patches/patches.json file.
|