@hominis/fireforge 0.40.0 → 0.41.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.
- package/CHANGELOG.md +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -10,7 +10,7 @@ import { toError } from '../utils/errors.js';
|
|
|
10
10
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
11
11
|
import { verbose, warn } from '../utils/logger.js';
|
|
12
12
|
import { escapeRegex } from '../utils/regex.js';
|
|
13
|
-
import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
|
|
13
|
+
import { asEstree, detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
|
|
14
14
|
import { CUSTOM_ELEMENTS_JS } from './furnace-constants.js';
|
|
15
15
|
import { validateRegistrationPlacement, validateTagName } from './furnace-registration-validate.js';
|
|
16
16
|
/**
|
|
@@ -62,7 +62,7 @@ function isInsideDOMContentLoaded(ancestors, content) {
|
|
|
62
62
|
const ancestor = ancestors[i];
|
|
63
63
|
if (!ancestor || ancestor.type !== 'CallExpression')
|
|
64
64
|
continue;
|
|
65
|
-
const call = ancestor;
|
|
65
|
+
const call = asEstree(ancestor);
|
|
66
66
|
if (call.callee.type === 'MemberExpression' &&
|
|
67
67
|
call.callee.object.type === 'Identifier' &&
|
|
68
68
|
call.callee.object.name === 'document' &&
|
|
@@ -87,18 +87,18 @@ function collectArrayDeclarations(ast) {
|
|
|
87
87
|
enter(node) {
|
|
88
88
|
if (node.type !== 'VariableDeclarator')
|
|
89
89
|
return;
|
|
90
|
-
const declarator = node;
|
|
90
|
+
const declarator = asEstree(node);
|
|
91
91
|
if (declarator.id.type !== 'Identifier' || declarator.init?.type !== 'ArrayExpression') {
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
-
arrays.set(declarator.id.name, declarator.init);
|
|
94
|
+
arrays.set(declarator.id.name, asEstree(declarator.init));
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
return arrays;
|
|
98
98
|
}
|
|
99
99
|
function resolveForOfArray(right, declaredArrays) {
|
|
100
100
|
if (right.type === 'ArrayExpression') {
|
|
101
|
-
return right;
|
|
101
|
+
return asEstree(right);
|
|
102
102
|
}
|
|
103
103
|
if (right.type === 'Identifier') {
|
|
104
104
|
return declaredArrays.get(right.name);
|
|
@@ -148,7 +148,7 @@ function addRegistrationAST(content, tagName, modulePath, isESModule, markerComm
|
|
|
148
148
|
enter(node) {
|
|
149
149
|
ancestors.push(node);
|
|
150
150
|
if (node.type === 'ForOfStatement') {
|
|
151
|
-
const forOf = node;
|
|
151
|
+
const forOf = asEstree(node);
|
|
152
152
|
const array = resolveForOfArray(forOf.right, declaredArrays);
|
|
153
153
|
if (array) {
|
|
154
154
|
forOfs.push({
|
|
@@ -170,7 +170,7 @@ function addRegistrationAST(content, tagName, modulePath, isESModule, markerComm
|
|
|
170
170
|
for (const el of array.elements) {
|
|
171
171
|
if (!el || el.type !== 'ArrayExpression')
|
|
172
172
|
continue;
|
|
173
|
-
const entryArr = el;
|
|
173
|
+
const entryArr = asEstree(el);
|
|
174
174
|
const firstEl = entryArr.elements[0];
|
|
175
175
|
if (!firstEl || firstEl.type !== 'Literal')
|
|
176
176
|
continue;
|
|
@@ -181,7 +181,7 @@ function addRegistrationAST(content, tagName, modulePath, isESModule, markerComm
|
|
|
181
181
|
const indent = detectIndent(content, entryArr.start);
|
|
182
182
|
let innerIndent;
|
|
183
183
|
if (isMultiLine) {
|
|
184
|
-
const firstElNode = firstEl;
|
|
184
|
+
const firstElNode = asEstree(firstEl);
|
|
185
185
|
innerIndent = detectIndent(content, firstElNode.start);
|
|
186
186
|
}
|
|
187
187
|
entries.push({ tag, node: entryArr, isMultiLine, indent, innerIndent });
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import MagicString from 'magic-string';
|
|
27
27
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
28
|
-
import { parseScript, walkAST } from './ast-utils.js';
|
|
28
|
+
import { asEstree, parseScript, walkAST } from './ast-utils.js';
|
|
29
29
|
import { CUSTOM_ELEMENTS_JS } from './furnace-constants.js';
|
|
30
30
|
/**
|
|
31
31
|
* Expands `[start, end)` to consume a trailing comma and any whitespace up
|
|
@@ -147,7 +147,7 @@ function collectRemovalRanges(ast, content, tagName) {
|
|
|
147
147
|
}
|
|
148
148
|
// Standalone call: customElements.setElementCreationCallback(tag, …)
|
|
149
149
|
if (node.type === 'CallExpression') {
|
|
150
|
-
const call = node;
|
|
150
|
+
const call = asEstree(node);
|
|
151
151
|
if (isStandaloneCallbackCallFor(call, tagName)) {
|
|
152
152
|
// Find the enclosing statement so we can delete `call(...);` as
|
|
153
153
|
// a unit, not just the call expression body.
|
|
@@ -158,7 +158,7 @@ function collectRemovalRanges(ast, content, tagName) {
|
|
|
158
158
|
continue;
|
|
159
159
|
if (ancestor.type === 'ExpressionStatement' ||
|
|
160
160
|
ancestor.type === 'VariableDeclaration') {
|
|
161
|
-
enclosing = ancestor;
|
|
161
|
+
enclosing = asEstree(ancestor);
|
|
162
162
|
break;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -203,6 +203,9 @@ export async function removeCustomElementRegistration(engineDir, tagName) {
|
|
|
203
203
|
ast = parseScript(content);
|
|
204
204
|
}
|
|
205
205
|
catch {
|
|
206
|
+
// An unparsable customElements.js is left untouched on purpose: this
|
|
207
|
+
// function only removes registrations, and editing a file it cannot parse
|
|
208
|
+
// risks corrupting it. The caller's regex fallback handles the retry.
|
|
206
209
|
return;
|
|
207
210
|
}
|
|
208
211
|
const ranges = collectRemovalRanges(ast, content, tagName);
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { FurnaceError } from '../errors/furnace.js';
|
|
4
4
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
5
|
-
/** Escapes special regex characters in a literal string. */
|
|
6
|
-
function escapeForRegex(str) {
|
|
7
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
-
}
|
|
9
5
|
/**
|
|
10
6
|
* Detects the indentation used by existing `content/global/elements/` lines
|
|
11
7
|
* in jar.mn. Falls back to 3 spaces (the historical Firefox convention) when
|
|
@@ -22,6 +18,7 @@ function detectJarMnIndent(lines) {
|
|
|
22
18
|
}
|
|
23
19
|
// Re-export everything from the AST module so existing imports keep working
|
|
24
20
|
export { addCustomElementRegistration, removeCustomElementRegistration, validateCustomElementRegistration, } from './furnace-registration-ast.js';
|
|
21
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
25
22
|
import { JAR_MN } from './furnace-constants.js';
|
|
26
23
|
/**
|
|
27
24
|
* Adds jar.mn entries that map chrome:// URIs to on-disk paths for a
|
|
@@ -66,7 +63,7 @@ export async function addJarMnEntries(engineDir, tagName, files) {
|
|
|
66
63
|
const lines = content.split('\n');
|
|
67
64
|
// Filter to files not already registered. Use a word-boundary-aware
|
|
68
65
|
// check so that "moz-card.css" does not match "moz-card-group.css".
|
|
69
|
-
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${
|
|
66
|
+
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${escapeRegex(f)}(?:\\s|$)`, 'm').test(content));
|
|
70
67
|
if (newFiles.length === 0)
|
|
71
68
|
return 0;
|
|
72
69
|
// Build new entry lines using the indent detected from existing entries.
|
|
@@ -141,8 +138,8 @@ export async function addLocaleFtlJarMnEntry(engineDir, jarMnRelPath, tagName, c
|
|
|
141
138
|
const content = await readText(filePath);
|
|
142
139
|
const lines = content.split('\n');
|
|
143
140
|
const ftlFile = `${tagName}.ftl`;
|
|
144
|
-
const escapedTag =
|
|
145
|
-
const escapedChrome =
|
|
141
|
+
const escapedTag = escapeRegex(tagName);
|
|
142
|
+
const escapedChrome = escapeRegex(chromeSubPath);
|
|
146
143
|
const presencePattern = new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapedChrome}\\/${escapedTag}\\.ftl`, 'm');
|
|
147
144
|
if (presencePattern.test(content)) {
|
|
148
145
|
return 0;
|
|
@@ -176,7 +173,7 @@ export async function addLocaleFtlJarMnEntry(engineDir, jarMnRelPath, tagName, c
|
|
|
176
173
|
}
|
|
177
174
|
/** Detects locale jar.mn indentation by sampling an existing matching entry. */
|
|
178
175
|
function detectLocaleJarMnIndent(lines, chromeSubPath) {
|
|
179
|
-
const escapedChrome =
|
|
176
|
+
const escapedChrome = escapeRegex(chromeSubPath);
|
|
180
177
|
const pattern = new RegExp(`^(\\s+)locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapedChrome}\\/`);
|
|
181
178
|
for (const line of lines) {
|
|
182
179
|
const match = pattern.exec(line);
|
|
@@ -202,7 +199,7 @@ export async function removeLocaleFtlJarMnEntry(engineDir, jarMnRelPath, tagName
|
|
|
202
199
|
}
|
|
203
200
|
const content = await readText(filePath);
|
|
204
201
|
const lines = content.split('\n');
|
|
205
|
-
const pattern = new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${
|
|
202
|
+
const pattern = new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapeRegex(chromeSubPath)}\\/${escapeRegex(tagName)}\\.ftl`);
|
|
206
203
|
const filtered = lines.filter((line) => !pattern.test(line));
|
|
207
204
|
if (filtered.length === lines.length)
|
|
208
205
|
return;
|
|
@@ -232,8 +229,8 @@ export async function removeJarMnEntries(engineDir, tagName) {
|
|
|
232
229
|
// report). The legacy target-path match is kept as an OR for lines
|
|
233
230
|
// written by older FireForge versions without a source mapping. Word
|
|
234
231
|
// boundaries keep "moz-card" from matching "moz-card-group".
|
|
235
|
-
const sourcePattern = new RegExp(`\\(widgets/${
|
|
236
|
-
const legacyTargetPattern = new RegExp(`content/global/elements/${
|
|
232
|
+
const sourcePattern = new RegExp(`\\(widgets/${escapeRegex(tagName)}/`);
|
|
233
|
+
const legacyTargetPattern = new RegExp(`content/global/elements/${escapeRegex(tagName)}\\.`);
|
|
237
234
|
const filtered = lines.filter((line) => !sourcePattern.test(line) && !legacyTargetPattern.test(line));
|
|
238
235
|
if (filtered.length === lines.length)
|
|
239
236
|
return;
|
|
@@ -296,7 +293,7 @@ export async function validateJarMnInsertionForFiles(engineDir, tagName, files)
|
|
|
296
293
|
}
|
|
297
294
|
const content = await readText(filePath);
|
|
298
295
|
const lines = content.split('\n');
|
|
299
|
-
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${
|
|
296
|
+
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${escapeRegex(f)}(?:\\s|$)`, 'm').test(content));
|
|
300
297
|
if (newFiles.length === 0)
|
|
301
298
|
return;
|
|
302
299
|
const elementLinePattern = /^\s+content\/global\/elements\/([^.]+)\./;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { toError } from '../utils/errors.js';
|
|
5
6
|
import { pathExists, readText } from '../utils/fs.js';
|
|
6
7
|
import { verbose, warn } from '../utils/logger.js';
|
|
7
8
|
import { asEstree, getNodeSource, parseScript, walkAST } from './ast-utils.js';
|
|
@@ -116,7 +117,7 @@ export async function scanCustomElementsRegistrations(engineDir) {
|
|
|
116
117
|
// Best-effort scanner: if upstream syntax changes or the file is damaged,
|
|
117
118
|
// fall back to the old literal callback heuristic instead of failing the
|
|
118
119
|
// whole scan command.
|
|
119
|
-
const reason = parseError
|
|
120
|
+
const reason = toError(parseError).message;
|
|
120
121
|
warn(`AST parsing of customElements.js failed (${reason}). Falling back to regex-based heuristic. ` +
|
|
121
122
|
'Results may be incomplete — run "fireforge furnace validate" to verify registration consistency.');
|
|
122
123
|
verbose(`Scanner regex fallback activated due to parse error: ${reason}`);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* block non-furnace patch work.
|
|
21
21
|
*/
|
|
22
22
|
import { GeneralError } from '../errors/base.js';
|
|
23
|
+
import { toError } from '../utils/errors.js';
|
|
23
24
|
import { pathExists } from '../utils/fs.js';
|
|
24
25
|
import { verbose, warn } from '../utils/logger.js';
|
|
25
26
|
import { extractComponentChecksums, hasComponentChanged } from './furnace-apply-helpers.js';
|
|
@@ -89,7 +90,7 @@ export async function findStaleFurnaceComponentsForFiles(projectRoot, files) {
|
|
|
89
90
|
return stale;
|
|
90
91
|
}
|
|
91
92
|
catch (error) {
|
|
92
|
-
verbose(`Stale-furnace export gate skipped due to an error: ${error
|
|
93
|
+
verbose(`Stale-furnace export gate skipped due to an error: ${toError(error).message}`);
|
|
93
94
|
return [];
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -21,14 +21,13 @@
|
|
|
21
21
|
* <name>` had exactly this bug while `furnace deploy <name>` documented
|
|
22
22
|
* and avoided it).
|
|
23
23
|
*/
|
|
24
|
-
import { type
|
|
24
|
+
import { type ApplyAllComponentsResult } from './furnace-apply.js';
|
|
25
25
|
import { type getFurnacePaths } from './furnace-config.js';
|
|
26
|
-
type ApplyAllResult = Awaited<ReturnType<typeof applyAllComponents>>;
|
|
27
26
|
/**
|
|
28
27
|
* Counts applied entries that carry at least one BLOCKING step error
|
|
29
28
|
* (advisory step errors are warnings and never fail a run).
|
|
30
29
|
*/
|
|
31
|
-
export declare function getStepFailureCount(result:
|
|
30
|
+
export declare function getStepFailureCount(result: ApplyAllComponentsResult): number;
|
|
32
31
|
/**
|
|
33
32
|
* Decides whether a single-component apply/deploy completed cleanly enough
|
|
34
33
|
* to persist its checksums into furnace-state.json.
|
|
@@ -40,7 +39,7 @@ export declare function getStepFailureCount(result: ApplyAllResult): number;
|
|
|
40
39
|
* future refactor cannot drift them apart and accidentally persist partial
|
|
41
40
|
* state.
|
|
42
41
|
*/
|
|
43
|
-
export declare function shouldPersistSingleComponentState(result:
|
|
42
|
+
export declare function shouldPersistSingleComponentState(result: ApplyAllComponentsResult, isDryRun: boolean): boolean;
|
|
44
43
|
/**
|
|
45
44
|
* Validates and narrows the applied entry a named run should persist.
|
|
46
45
|
*
|
|
@@ -52,7 +51,7 @@ export declare function shouldPersistSingleComponentState(result: ApplyAllResult
|
|
|
52
51
|
*
|
|
53
52
|
* @param commandLabel - Human label for error messages ("Deploy", "Apply")
|
|
54
53
|
*/
|
|
55
|
-
export declare function getPersistableAppliedEntry(commandLabel: string, name: string, appliedEntry:
|
|
54
|
+
export declare function getPersistableAppliedEntry(commandLabel: string, name: string, appliedEntry: ApplyAllComponentsResult['applied'][number] | undefined): {
|
|
56
55
|
name: string;
|
|
57
56
|
type: 'override' | 'custom';
|
|
58
57
|
};
|
|
@@ -69,4 +68,3 @@ export declare function persistSingleComponentState(projectRoot: string, applied
|
|
|
69
68
|
name: string;
|
|
70
69
|
type: 'override' | 'custom';
|
|
71
70
|
}, furnacePaths: ReturnType<typeof getFurnacePaths>): Promise<void>;
|
|
72
|
-
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
|
-
import { hasRawCssColors } from '../utils/regex.js';
|
|
4
|
+
import { escapeRegex, hasRawCssColors } from '../utils/regex.js';
|
|
5
5
|
import { classExtendsMozLitElement, collectCssVariableDeclarations, collectCssVariableReferences, createIssue, getTokenPrefixContext, hasCustomElementDefineCall, hasRelativeModuleImport, stripCssBlockComments, } from './furnace-validate-helpers.js';
|
|
6
6
|
async function validateMjsCompatibility(mjsPath, tagName, isLibrary) {
|
|
7
7
|
if (!(await pathExists(mjsPath)))
|
|
@@ -78,22 +78,19 @@ async function validateCssCompatibility(cssPath, tagName, type, config, root) {
|
|
|
78
78
|
*/
|
|
79
79
|
function isReferencedAsElement(source, tagName) {
|
|
80
80
|
// Match as an HTML element: <tagName or </tagName
|
|
81
|
-
const htmlPattern = new RegExp(`</?${
|
|
81
|
+
const htmlPattern = new RegExp(`</?${escapeRegex(tagName)}[\\s/>]`);
|
|
82
82
|
if (htmlPattern.test(source))
|
|
83
83
|
return true;
|
|
84
84
|
// Match as a CSS tag selector: standalone word at start of line or after combinators
|
|
85
|
-
const cssPattern = new RegExp(`(?:^|[\\s,>+~])${
|
|
85
|
+
const cssPattern = new RegExp(`(?:^|[\\s,>+~])${escapeRegex(tagName)}(?:[\\s,{:>+~.[#]|$)`, 'm');
|
|
86
86
|
if (cssPattern.test(source))
|
|
87
87
|
return true;
|
|
88
88
|
// Also accept querySelector/querySelectorAll('tagName') as intentional usage
|
|
89
|
-
const querySelectorPattern = new RegExp(`querySelector(?:All)?\\(\\s*["'\`]${
|
|
89
|
+
const querySelectorPattern = new RegExp(`querySelector(?:All)?\\(\\s*["'\`]${escapeRegex(tagName)}(?:[\\s"'\`.,>+~:[#])`);
|
|
90
90
|
if (querySelectorPattern.test(source))
|
|
91
91
|
return true;
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
|
-
function escapeForValidation(str) {
|
|
95
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
96
|
-
}
|
|
97
94
|
/**
|
|
98
95
|
* Validates that composed tags declared in furnace.json are actually referenced
|
|
99
96
|
* in the component's source (.mjs and .css) in an HTML template or CSS context.
|
|
@@ -180,10 +180,42 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
|
180
180
|
}
|
|
181
181
|
return !NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName);
|
|
182
182
|
}
|
|
183
|
+
/** ASCII punctuation that reads as decoration rather than localizable text. */
|
|
184
|
+
const SYMBOL_ONLY_ASCII = '+-*=<>|/\\^~@#&!?%';
|
|
185
|
+
/**
|
|
186
|
+
* Reports whether `text` is decoration (arrows, maths, emoji, bare
|
|
187
|
+
* punctuation) rather than user-facing prose that needs localizing.
|
|
188
|
+
*
|
|
189
|
+
* The letter/number/mark test comes first and is script-agnostic. Until
|
|
190
|
+
* 0.41.0 the only non-ASCII rule was `code > 0xff`, which classified *every*
|
|
191
|
+
* code point above U+00FF as a symbol — so all CJK, Cyrillic, Greek, Arabic,
|
|
192
|
+
* Hebrew, Devanagari and Thai text was exempted from a **localization**
|
|
193
|
+
* validator, i.e. the rule was blind to precisely the strings most likely to
|
|
194
|
+
* need translating.
|
|
195
|
+
*
|
|
196
|
+
* Combining marks are deliberately NOT counted as text: the check is an
|
|
197
|
+
* `every()`, so a script's base letters already disqualify the string, and
|
|
198
|
+
* treating marks as text would flag emoji carrying a variation selector
|
|
199
|
+
* (U+FE0F is category Mn — `⚙️` is U+2699 + U+FE0F).
|
|
200
|
+
*
|
|
201
|
+
* High code points that are not letters — emoji, arrows, CJK punctuation —
|
|
202
|
+
* stay exempt, which is the rule's original intent.
|
|
203
|
+
*
|
|
204
|
+
* A handful of emoji are ALSO Unicode letters, and the letter test alone gets
|
|
205
|
+
* them wrong. U+2139 (`ℹ`, the base of the very common `ℹ️`) is category Ll, so
|
|
206
|
+
* the `\p{L}` branch classified an info badge as prose needing localization.
|
|
207
|
+
* Non-ASCII code points carrying `Emoji` are therefore read as symbols before
|
|
208
|
+
* the letter test. The ASCII guard matters: `\p{Emoji}` also covers the digits
|
|
209
|
+
* `0`-`9`, `#` and `*`, which must keep their existing classification.
|
|
210
|
+
*/
|
|
183
211
|
function isSymbolOnlyText(text) {
|
|
184
212
|
return Array.from(text).every((character) => {
|
|
185
213
|
const code = character.codePointAt(0) ?? 0;
|
|
186
|
-
|
|
214
|
+
if (code > 0x7f && /\p{Emoji}/u.test(character))
|
|
215
|
+
return true;
|
|
216
|
+
if (/[\p{L}\p{N}]/u.test(character))
|
|
217
|
+
return false;
|
|
218
|
+
return code > 0xff || SYMBOL_ONLY_ASCII.includes(character);
|
|
187
219
|
});
|
|
188
220
|
}
|
|
189
221
|
function isWithinLocalizedElement(content, matchIndex) {
|
|
@@ -35,6 +35,18 @@ export async function validateRegistrationPatterns(root, config) {
|
|
|
35
35
|
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
36
36
|
if (!customConfig.register)
|
|
37
37
|
continue;
|
|
38
|
+
// A component that is not mentioned at all is a distinct defect from one
|
|
39
|
+
// registered in the wrong block: `--fix` can repair the former (idempotent
|
|
40
|
+
// insert) but deliberately refuses the latter (moving code between blocks).
|
|
41
|
+
if (!isTagAlreadyRegistered(content, name)) {
|
|
42
|
+
issues.push({
|
|
43
|
+
component: name,
|
|
44
|
+
severity: 'error',
|
|
45
|
+
check: 'missing-custom-element-registration',
|
|
46
|
+
message: `${name} has register: true but no registration in ${CUSTOM_ELEMENTS_JS}. Run "fireforge furnace validate ${name} --fix" to add it.`,
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
38
50
|
const stripped = stripJsComments(content);
|
|
39
51
|
const tagPattern = new RegExp(`["']${escapeRegex(name)}["']`);
|
|
40
52
|
if (tagPattern.test(stripped) && !isTagInCorrectCustomElementsPlacement(content, name, true)) {
|
|
@@ -268,6 +280,8 @@ async function autoDetectTokenHostDocuments(engineDir, tagName, already) {
|
|
|
268
280
|
entries = await readdir(contentDir);
|
|
269
281
|
}
|
|
270
282
|
catch {
|
|
283
|
+
// No readable chrome-document directory means no documents reference the
|
|
284
|
+
// tag, which is the same verdict as an empty directory.
|
|
271
285
|
return [];
|
|
272
286
|
}
|
|
273
287
|
const alreadySet = new Set(already);
|
|
@@ -284,6 +298,8 @@ async function autoDetectTokenHostDocuments(engineDir, tagName, already) {
|
|
|
284
298
|
content = await readText(absPath);
|
|
285
299
|
}
|
|
286
300
|
catch {
|
|
301
|
+
// An unreadable chrome document cannot be searched for the tag; skip it and
|
|
302
|
+
// keep scanning the rest.
|
|
287
303
|
continue;
|
|
288
304
|
}
|
|
289
305
|
if (content.includes(tagName)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { toError } from '../utils/errors.js';
|
|
4
5
|
import { pathExists } from '../utils/fs.js';
|
|
5
6
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
6
7
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState } from './furnace-config.js';
|
|
@@ -109,7 +110,7 @@ export async function validateAllComponents(root) {
|
|
|
109
110
|
validateComposesReferences(config.stock, config.overrides, config.custom);
|
|
110
111
|
}
|
|
111
112
|
catch (err) {
|
|
112
|
-
const message = err
|
|
113
|
+
const message = toError(err).message;
|
|
113
114
|
// Attribute the issue to the first custom component with a bad composes reference
|
|
114
115
|
for (const [name, cfg] of Object.entries(config.custom)) {
|
|
115
116
|
if (cfg.composes) {
|
|
@@ -129,7 +130,7 @@ export async function validateAllComponents(root) {
|
|
|
129
130
|
detectComposesCycles(config.custom);
|
|
130
131
|
}
|
|
131
132
|
catch (err) {
|
|
132
|
-
const message = err
|
|
133
|
+
const message = toError(err).message;
|
|
133
134
|
// Attribute the cycle issue to the first custom component in the cycle
|
|
134
135
|
for (const name of Object.keys(config.custom)) {
|
|
135
136
|
if (config.custom[name]?.composes) {
|
|
@@ -274,6 +275,7 @@ async function findOrphanXpcshellScaffolds(root, config) {
|
|
|
274
275
|
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
275
276
|
}
|
|
276
277
|
catch {
|
|
278
|
+
// An unreadable parent directory yields no sibling component dirs.
|
|
277
279
|
return [];
|
|
278
280
|
}
|
|
279
281
|
const known = new Set([
|
|
@@ -460,6 +460,15 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
|
|
|
460
460
|
// stage/unstage pair mutates the index, so it must not interleave with
|
|
461
461
|
// another concurrent binary patch (see runWithBinaryStagingLock).
|
|
462
462
|
return runWithBinaryStagingLock(async () => {
|
|
463
|
+
// Snapshot the path's exact index entry before touching it. The cleanup
|
|
464
|
+
// used to be a blanket `git reset HEAD -- <file>`, which restores the
|
|
465
|
+
// path to its HEAD state — for a path absent from HEAD that means
|
|
466
|
+
// *removing* whatever entry the index carried, so any staged state that
|
|
467
|
+
// arrived between the read-only diff above and this staging block (a
|
|
468
|
+
// concurrent writer, or a staged-add whose worktree file was deleted)
|
|
469
|
+
// was silently discarded (FORGE H1). Restoring the captured entry
|
|
470
|
+
// instead makes the cleanup exact regardless of how the entry got there.
|
|
471
|
+
const priorEntry = (await execGitWithAllowedExitCodes(repoDir, ['ls-files', '--stage', '--', filePath])).stdout;
|
|
463
472
|
try {
|
|
464
473
|
await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath]);
|
|
465
474
|
const diffResult = await execGitWithAllowedExitCodes(repoDir, [
|
|
@@ -471,8 +480,34 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
|
|
|
471
480
|
return diffResult.stdout;
|
|
472
481
|
}
|
|
473
482
|
finally {
|
|
474
|
-
|
|
475
|
-
await execGitWithAllowedExitCodes(repoDir, ['reset', 'HEAD', '--', filePath]);
|
|
483
|
+
await restoreIndexEntry(repoDir, filePath, priorEntry);
|
|
476
484
|
}
|
|
477
485
|
});
|
|
478
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Restores a path's index entry to a previously captured `ls-files --stage`
|
|
489
|
+
* snapshot after a temporary `--intent-to-add` staging. No prior entry means
|
|
490
|
+
* the temporary entry is dropped via `reset HEAD` (the path's HEAD state IS
|
|
491
|
+
* "absent" — this branch only runs for files untracked in HEAD). A prior
|
|
492
|
+
* stage-0 entry is re-pointed at its recorded mode and blob. Unmerged entries
|
|
493
|
+
* (stages 1–3) cannot be rebuilt through `--cacheinfo`; that degenerate state
|
|
494
|
+
* keeps the legacy reset-to-HEAD behavior rather than corrupting the conflict.
|
|
495
|
+
* @param repoDir - Repository directory
|
|
496
|
+
* @param filePath - File path (relative to repo root)
|
|
497
|
+
* @param priorEntry - Raw `git ls-files --stage -- <file>` output from before staging
|
|
498
|
+
*/
|
|
499
|
+
async function restoreIndexEntry(repoDir, filePath, priorEntry) {
|
|
500
|
+
const entryMatch = /^(\d{6}) ([0-9a-f]{4,64}) 0\t/.exec(priorEntry.trim());
|
|
501
|
+
const mode = entryMatch?.[1];
|
|
502
|
+
const oid = entryMatch?.[2];
|
|
503
|
+
if (mode === undefined || oid === undefined) {
|
|
504
|
+
await execGitWithAllowedExitCodes(repoDir, ['reset', 'HEAD', '--', filePath]);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await execGitWithAllowedExitCodes(repoDir, [
|
|
508
|
+
'update-index',
|
|
509
|
+
'--add',
|
|
510
|
+
'--cacheinfo',
|
|
511
|
+
`${mode},${oid},${filePath}`,
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
2
2
|
import type { FireForgeConfig } from '../types/config.js';
|
|
3
3
|
import { type PatchQueueContext } from './patch-lint.js';
|
|
4
|
-
export declare const LINT_CACHE_SCHEMA_VERSION =
|
|
4
|
+
export declare const LINT_CACHE_SCHEMA_VERSION = 3;
|
|
5
5
|
export interface PerPatchLintCacheEntry {
|
|
6
6
|
key: string;
|
|
7
7
|
patchFilename: string;
|
|
@@ -11,7 +11,11 @@ import { collectNewFileCreatorsByPath } from './patch-lint.js';
|
|
|
11
11
|
// issues and the measured non-binary line count, so a warm run can report
|
|
12
12
|
// waived size measurements identically to a cold one (the F5-class hazard:
|
|
13
13
|
// a cache hit must never surface LESS than a fresh lint).
|
|
14
|
-
|
|
14
|
+
// Schema 3 (FORGE H2): the key additionally content-hashes the
|
|
15
|
+
// `patchLint.checkJsTestShim` file exactly as `checkJsExtraShim`'s already
|
|
16
|
+
// was — before this, editing the test shim in place replayed every cached
|
|
17
|
+
// verdict (the same warm-run-reports-less-than-cold hazard class).
|
|
18
|
+
export const LINT_CACHE_SCHEMA_VERSION = 3;
|
|
15
19
|
const LINT_IMPLEMENTATION_VERSION = 1;
|
|
16
20
|
const LINT_CACHE_DIRNAME = 'lint-cache';
|
|
17
21
|
const PER_PATCH_CACHE_FILENAME = 'per-patch-v1.json';
|
|
@@ -85,28 +89,44 @@ function buildOwnershipFingerprint(files, ctx) {
|
|
|
85
89
|
* The key includes source, metadata, config, engine state, and ownership inputs.
|
|
86
90
|
*/
|
|
87
91
|
export async function buildPerPatchLintCacheKey(input) {
|
|
92
|
+
// Hash in parallel, then insert in sorted order. The sort is load-bearing:
|
|
93
|
+
// `stableHash` below serialises this record, so insertion order is part of
|
|
94
|
+
// the cache key — assigning as results land would make the key
|
|
95
|
+
// nondeterministic. Hashing was strictly sequential until 0.41.0, in the hot
|
|
96
|
+
// path of the cache whose entire purpose is making lint faster.
|
|
97
|
+
const sortedFiles = [...input.existingFiles].sort((a, b) => a.localeCompare(b));
|
|
98
|
+
const hashes = await Promise.all(sortedFiles.map((file) => fileHash(join(input.engineDir, file))));
|
|
88
99
|
const engineFiles = {};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
hashes.forEach((hash, index) => {
|
|
101
|
+
const file = sortedFiles[index];
|
|
102
|
+
if (file !== undefined)
|
|
103
|
+
engineFiles[file] = hash;
|
|
104
|
+
});
|
|
92
105
|
const furnaceConfigPath = getFurnacePaths(input.projectRoot).furnaceConfig;
|
|
93
|
-
const
|
|
94
|
-
const extraShim = extraShimPath === undefined
|
|
106
|
+
const shimHashOf = async (shimPath) => shimPath === undefined
|
|
95
107
|
? null
|
|
96
|
-
: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
: { path: shimPath, hash: await fileHash(resolve(input.projectRoot, shimPath)) };
|
|
109
|
+
// Both shims feed compiled checkJs programs, so both are content-hashed —
|
|
110
|
+
// the config block alone only carries their PATHS, and an in-place edit
|
|
111
|
+
// must invalidate the programs it feeds (FORGE H2).
|
|
112
|
+
const [extraShim, testShim] = await Promise.all([
|
|
113
|
+
shimHashOf(input.config.patchLint?.checkJsExtraShim),
|
|
114
|
+
shimHashOf(input.config.patchLint?.checkJsTestShim),
|
|
115
|
+
]);
|
|
100
116
|
return stableHash({
|
|
101
117
|
cacheSchemaVersion: LINT_CACHE_SCHEMA_VERSION,
|
|
102
118
|
engineHeadSha: input.engineHeadSha ?? null,
|
|
103
119
|
lintImplementationVersion: LINT_IMPLEMENTATION_VERSION,
|
|
120
|
+
// Deliberately the PLAIN semver, not the +g<sha> build identity
|
|
121
|
+
// (FORGE K2): identity churns every commit at the same version and
|
|
122
|
+
// would invalidate the cache for no correctness gain.
|
|
104
123
|
packageVersion: input.packageVersion ?? getPackageVersion(),
|
|
105
124
|
patchFile: await fileHash(join(input.patchesDir, input.patch.filename)),
|
|
106
125
|
patchMetadata: normalizePatchMetadata(input.patch),
|
|
107
126
|
lintConfig: normalizeLintConfig(input.config),
|
|
108
127
|
furnaceConfig: await fileHash(furnaceConfigPath),
|
|
109
128
|
checkJsExtraShim: extraShim,
|
|
129
|
+
checkJsTestShim: testShim,
|
|
110
130
|
engineFiles,
|
|
111
131
|
queueOwnership: buildOwnershipFingerprint(input.existingFiles, input.queueContext),
|
|
112
132
|
});
|
|
@@ -150,6 +170,9 @@ export async function loadPerPatchLintCache(projectRoot) {
|
|
|
150
170
|
return { schemaVersion: LINT_CACHE_SCHEMA_VERSION, entries };
|
|
151
171
|
}
|
|
152
172
|
catch {
|
|
173
|
+
// Any unreadable or malformed cache file is treated as a cold cache. The
|
|
174
|
+
// cache is a pure optimisation, so discarding it is always safe — the
|
|
175
|
+
// alternative is failing lint over a corrupt sidecar.
|
|
153
176
|
return createEmptyPerPatchLintCache();
|
|
154
177
|
}
|
|
155
178
|
}
|
|
@@ -20,7 +20,11 @@ export interface BuildArtifactCheck {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Checks if build artifacts exist in the engine directory.
|
|
23
|
-
* Looks for obj-* directories with a dist subdirectory.
|
|
23
|
+
* Looks for obj-* directories with a dist subdirectory. Detection is
|
|
24
|
+
* deliberately symlink-agnostic — building/running against a symlinked
|
|
25
|
+
* objdir in place is the user's own arrangement; the clone path, where a
|
|
26
|
+
* symlink would route writes into the original build, refuses separately
|
|
27
|
+
* (`assertCloneSafeObjdir` in tree-store.ts).
|
|
24
28
|
* @param engineDir - Path to the engine directory
|
|
25
29
|
* @returns Build artifact check result
|
|
26
30
|
*/
|
|
@@ -112,3 +116,61 @@ export interface MozinfoRewriteResult {
|
|
|
112
116
|
* @returns Result object; callers inspect `rewritten` and surface `reason`.
|
|
113
117
|
*/
|
|
114
118
|
export declare function attemptMozinfoRewrite(engineDir: string, objDir: string): Promise<MozinfoRewriteResult>;
|
|
119
|
+
/** Per-command wording for {@link assertBuildArtifacts}. */
|
|
120
|
+
export interface BuildArtifactPreflightOptions {
|
|
121
|
+
/** Label passed to {@link buildArtifactMismatchMessage} (e.g. `'Tests'`). */
|
|
122
|
+
label: string;
|
|
123
|
+
/** Sentence naming what needs a build, e.g. `'Tests require a completed build.'` */
|
|
124
|
+
requirement: string;
|
|
125
|
+
/** Follow-up telling the operator what to run, shown after `requirement`. */
|
|
126
|
+
remediation: string;
|
|
127
|
+
/**
|
|
128
|
+
* Whether a missing/incomplete build is fatal. `fireforge build` runs
|
|
129
|
+
* legitimately with no artifacts and only enforces this under `--ui`, so it
|
|
130
|
+
* opts in rather than out.
|
|
131
|
+
*/
|
|
132
|
+
requireExisting?: boolean;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Shared build-artifact preflight: rejects ambiguous multi-objdir checkouts,
|
|
136
|
+
* artifacts whose metadata points at another tree, and (when
|
|
137
|
+
* `requireExisting`) missing or incomplete builds — each with the actionable
|
|
138
|
+
* message for that case.
|
|
139
|
+
*
|
|
140
|
+
* Takes an already-probed {@link BuildArtifactCheck} rather than probing
|
|
141
|
+
* itself, so it stays a pure validator: callers keep their own
|
|
142
|
+
* `hasBuildArtifacts` call (which their suites already stub) and this function
|
|
143
|
+
* needs no test double of its own.
|
|
144
|
+
*
|
|
145
|
+
* Five commands carried a character-identical copy of this ladder, including
|
|
146
|
+
* the same `ambiguous && objDirs && objDirs.length > 0` expression that
|
|
147
|
+
* `BuildArtifactCheck`'s boolean-plus-optionals shape forces at every reader.
|
|
148
|
+
* The shape itself is unchanged — it has several producers and a documented
|
|
149
|
+
* refusal contract (see the containment predicates below) — but it is now
|
|
150
|
+
* destructured in exactly one place.
|
|
151
|
+
*/
|
|
152
|
+
export declare function assertBuildArtifacts(engineDir: string, buildCheck: BuildArtifactCheck, options: BuildArtifactPreflightOptions): void;
|
|
153
|
+
/**
|
|
154
|
+
* Post-`mach configure` relocation check for a cloned objdir: confirms
|
|
155
|
+
* configure actually regenerated `<engineDir>/<objDir>` and that none of
|
|
156
|
+
* the four configure-generated root files (`config.status`, `backend.mk`,
|
|
157
|
+
* `Makefile`, `config/autoconf.mk`) still names `forbiddenDir` (the
|
|
158
|
+
* primary engine a relocated clone must never consult). Exit code 0 alone
|
|
159
|
+
* proves neither — a stray MOZCONFIG/MOZ_OBJDIR can steer configure at a
|
|
160
|
+
* different objdir entirely. Nested Makefiles are products of the verified
|
|
161
|
+
* `config.status` and are not scanned; `.deps` files are build products a
|
|
162
|
+
* configure cannot regenerate and are explicitly out of scope — any
|
|
163
|
+
* primary-path strings they retain are read-only staleness corrected by
|
|
164
|
+
* the first in-tree rebuild. Pure checker: returns a human-readable
|
|
165
|
+
* violation, or `undefined` when clean; callers own the error type and
|
|
166
|
+
* remediation copy. Unreadable metadata is itself a violation (fail
|
|
167
|
+
* closed).
|
|
168
|
+
*/
|
|
169
|
+
export declare function findObjdirRelocationViolation(args: {
|
|
170
|
+
/** The relocated (tree) engine directory configure ran in. */
|
|
171
|
+
engineDir: string;
|
|
172
|
+
/** The obj-* directory name configure was expected to target. */
|
|
173
|
+
objDir: string;
|
|
174
|
+
/** The primary engine dir whose absolute path must not appear. */
|
|
175
|
+
forbiddenDir: string;
|
|
176
|
+
}): Promise<string | undefined>;
|