@hominis/fireforge 0.39.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 +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -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 +27 -9
- 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 +30 -10
- 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 +73 -51
- 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-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -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 +152 -51
- 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 +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- 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 +11 -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 +123 -289
- 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 +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- 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 +190 -170
- 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 +59 -7
- 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/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- 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 +45 -30
- 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 +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- 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.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- 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-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- 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-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- 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-manifest.d.ts +1 -1
- 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 +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- 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 +22 -1
- package/dist/src/core/status-classify.js +112 -70
- 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/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- 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/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -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 +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem-based lock for serializing patch directory mutations.
|
|
3
3
|
*/
|
|
4
|
+
export interface PatchDirectoryLockOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Wait up to this many seconds for the patch directory lock instead of the
|
|
7
|
+
* default ~30 s budget. Enables exponential poll backoff (100 ms → 2 s)
|
|
8
|
+
* and a holder-identified progress line roughly every 5 s. `undefined`
|
|
9
|
+
* preserves the historical behavior exactly.
|
|
10
|
+
*/
|
|
11
|
+
waitLockSeconds?: number | undefined;
|
|
12
|
+
/** Command name recorded in the lock's owner metadata for holder diagnostics. */
|
|
13
|
+
command?: string | undefined;
|
|
14
|
+
}
|
|
4
15
|
/**
|
|
5
16
|
* Runs a patch directory mutation while holding an exclusive filesystem lock.
|
|
6
17
|
* This serializes filename allocation and manifest writes across parallel exports.
|
|
7
18
|
*/
|
|
8
|
-
export declare function withPatchDirectoryLock<T>(patchesDir: string, operation: () => Promise<T
|
|
19
|
+
export declare function withPatchDirectoryLock<T>(patchesDir: string, operation: () => Promise<T>, options?: PatchDirectoryLockOptions): Promise<T>;
|
|
@@ -6,18 +6,54 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { FireForgeError } from '../errors/base.js';
|
|
7
7
|
import { PatchError } from '../errors/patch.js';
|
|
8
8
|
import { toError } from '../utils/errors.js';
|
|
9
|
+
import { info } from '../utils/logger.js';
|
|
9
10
|
import { withFileLock } from './file-lock.js';
|
|
10
11
|
const PATCH_DIRECTORY_LOCK = '.fireforge-patches.lock';
|
|
12
|
+
/**
|
|
13
|
+
* Formats the periodic operator-facing waiting line printed while
|
|
14
|
+
* `--wait-lock` polls a contended patch directory lock. Mirrors the engine
|
|
15
|
+
* session lock's progress line so operators read one shape for both locks.
|
|
16
|
+
*/
|
|
17
|
+
function formatWaitProgressLine(waitedMs, timeoutMs, holder) {
|
|
18
|
+
const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
|
|
19
|
+
if (holder === undefined) {
|
|
20
|
+
return `Waiting for the FireForge patch directory lock — ${progress}.`;
|
|
21
|
+
}
|
|
22
|
+
const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
|
|
23
|
+
return `Waiting for the FireForge patch directory lock held by PID ${String(holder.pid)}${details} — ${progress}.`;
|
|
24
|
+
}
|
|
11
25
|
/**
|
|
12
26
|
* Runs a patch directory mutation while holding an exclusive filesystem lock.
|
|
13
27
|
* This serializes filename allocation and manifest writes across parallel exports.
|
|
14
28
|
*/
|
|
15
|
-
export async function withPatchDirectoryLock(patchesDir, operation) {
|
|
29
|
+
export async function withPatchDirectoryLock(patchesDir, operation, options = {}) {
|
|
16
30
|
const lockDir = join(patchesDir, PATCH_DIRECTORY_LOCK);
|
|
31
|
+
const { waitLockSeconds, command } = options;
|
|
17
32
|
return withFileLock(lockDir, operation, {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
...(waitLockSeconds !== undefined
|
|
34
|
+
? {
|
|
35
|
+
timeoutMs: waitLockSeconds * 1000,
|
|
36
|
+
pollMs: 100,
|
|
37
|
+
pollMaxMs: 2000,
|
|
38
|
+
waitProgressMs: 5000,
|
|
39
|
+
onWaitProgress: ({ waitedMs, timeoutMs, holder }) => {
|
|
40
|
+
info(formatWaitProgressLine(waitedMs, timeoutMs, holder));
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
...(command !== undefined
|
|
45
|
+
? { ownerMetadata: [`command=${command}`, `started=${new Date().toISOString()}`] }
|
|
46
|
+
: {}),
|
|
47
|
+
// Reason first, remedy second (FORGE H5). The remedy is waiting, not
|
|
48
|
+
// deleting: bulk exports/re-exports legitimately hold this lock for
|
|
49
|
+
// minutes, FireForge reaps genuinely stale locks on its own, and the
|
|
50
|
+
// old advice ("rm -rf" while a holder was alive) destroyed a live
|
|
51
|
+
// lock. The leading sentence is a message contract; extend, don't
|
|
52
|
+
// reword. withFileLock appends the holder identification.
|
|
53
|
+
onTimeoutMessage: `Timed out waiting for another FireForge command mutating ${patchesDir}.\n` +
|
|
54
|
+
`Pass --wait-lock [seconds] to wait longer (bare --wait-lock waits up to 60 seconds). ` +
|
|
55
|
+
`FireForge removes genuinely stale locks automatically; only if no other fireforge ` +
|
|
56
|
+
`process is running and the timeout persists, remove the lock manually:\n rm -rf "${lockDir}"`,
|
|
21
57
|
onStaleLockMessage: (ageMs) => `Removing stale patch lock (age: ${Math.round(ageMs / 1000)}s). ` +
|
|
22
58
|
'A previous fireforge process may have crashed.',
|
|
23
59
|
}).catch((error) => {
|
|
@@ -112,10 +112,22 @@ async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVers
|
|
|
112
112
|
return Number.isFinite(patch.order) ? Math.max(highest, patch.order) : highest;
|
|
113
113
|
}, 0);
|
|
114
114
|
let nextRecoveredOrder = highestFiniteOrder + 1;
|
|
115
|
-
|
|
115
|
+
// Prefetch the two independent per-patch reads concurrently. This runs
|
|
116
|
+
// while the patch-directory lock is held, and it was two serialised awaits
|
|
117
|
+
// per patch across the whole queue — 58 round-trips on a 29-patch queue.
|
|
118
|
+
// The fold below stays sequential because `nextRecoveredOrder` is
|
|
119
|
+
// loop-carried: parallelising the assignment would scramble the recovered
|
|
120
|
+
// ordinals.
|
|
121
|
+
const prefetched = await Promise.all(patches.map(async (patch) => ({
|
|
122
|
+
filesAffected: normalizeFiles(await getAllTargetFilesFromPatch(patch.path)),
|
|
123
|
+
patchStats: await stat(patch.path),
|
|
124
|
+
})));
|
|
125
|
+
for (const [index, patch] of patches.entries()) {
|
|
116
126
|
const existing = existingEntries.get(patch.filename);
|
|
117
|
-
const
|
|
118
|
-
|
|
127
|
+
const prefetch = prefetched[index];
|
|
128
|
+
if (!prefetch)
|
|
129
|
+
continue;
|
|
130
|
+
const { filesAffected, patchStats } = prefetch;
|
|
119
131
|
const inferred = inferPatchMetadataFromFilename(patch.filename);
|
|
120
132
|
const recoveredOrder = Number.isFinite(patch.order) ? patch.order : nextRecoveredOrder++;
|
|
121
133
|
if (!existing) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* is an implementation detail.
|
|
6
6
|
*/
|
|
7
7
|
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
8
|
-
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, type
|
|
8
|
+
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
|
|
9
9
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
10
10
|
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|
|
11
11
|
export { validatePatchesManifest } from './patch-manifest-validate.js';
|
|
@@ -60,6 +60,14 @@ export interface DiffSection {
|
|
|
60
60
|
* refuse them rather than treat base85 lines starting with `+` as content.
|
|
61
61
|
*/
|
|
62
62
|
isBinary: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Old-side blob hash from the section's `index <old>..<new>` line
|
|
65
|
+
* (possibly abbreviated). Undefined when the metadata zone carries no
|
|
66
|
+
* parseable index line (hand-written diffs, `Binary files … differ`).
|
|
67
|
+
*/
|
|
68
|
+
indexOldHash?: string;
|
|
69
|
+
/** New-side blob hash from the `index` line; see {@link indexOldHash}. */
|
|
70
|
+
indexNewHash?: string;
|
|
63
71
|
/** Parsed hunks, in file order. Empty for binary or metadata-only sections. */
|
|
64
72
|
hunks: ParsedHunk[];
|
|
65
73
|
}
|
|
@@ -169,6 +169,16 @@ export function parseDiffSections(diffContent) {
|
|
|
169
169
|
current.isBinary = true;
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
|
+
// `git diff --binary` always records full blob hashes here (git
|
|
173
|
+
// apply requires them); text sections carry abbreviated ones. The
|
|
174
|
+
// hashes let binary sections be compared by identity when their
|
|
175
|
+
// payload cannot be applied as text (FORGE J3).
|
|
176
|
+
const indexMatch = /^index ([0-9a-f]{7,64})\.\.([0-9a-f]{7,64})(?: \d{6})?$/.exec(line);
|
|
177
|
+
if (indexMatch?.[1] !== undefined && indexMatch[2] !== undefined) {
|
|
178
|
+
current.indexOldHash = indexMatch[1];
|
|
179
|
+
current.indexNewHash = indexMatch[2];
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
if (current.isBinary)
|
|
174
184
|
continue;
|
|
@@ -23,6 +23,13 @@ export interface PatchPolicyEnforcementInput {
|
|
|
23
23
|
manifest: PatchesManifest;
|
|
24
24
|
command: string;
|
|
25
25
|
forceUnsafe?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Per-issue-code remediation lines appended to matching details, so a
|
|
28
|
+
* refusal can name the exact flag or command that fixes it on the
|
|
29
|
+
* command it was raised from (e.g. `--description` on
|
|
30
|
+
* `patch move-files --create`).
|
|
31
|
+
*/
|
|
32
|
+
hints?: Partial<Record<PatchPolicyIssueCode, string>>;
|
|
26
33
|
}
|
|
27
34
|
/** Returns valid categories for prompts and CLI validation under the config. */
|
|
28
35
|
export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
|
|
@@ -341,7 +341,10 @@ export function enforcePatchPolicy(input) {
|
|
|
341
341
|
if (issues.length === 0)
|
|
342
342
|
return;
|
|
343
343
|
const mode = mutationMode(input.config);
|
|
344
|
-
const details = issues.map((issue) =>
|
|
344
|
+
const details = issues.map((issue) => {
|
|
345
|
+
const hint = input.hints?.[issue.code];
|
|
346
|
+
return ` [${issue.code}] ${issue.message}${hint ? `\n → ${hint}` : ''}`;
|
|
347
|
+
});
|
|
345
348
|
if (mode === 'warn') {
|
|
346
349
|
warn(`${input.command}: patch policy warning(s):`);
|
|
347
350
|
for (const detail of details)
|
|
@@ -35,3 +35,13 @@ export declare function extractNewFileContent(patchPath: string, targetFile?: st
|
|
|
35
35
|
* @returns Modified content
|
|
36
36
|
*/
|
|
37
37
|
export declare function applyPatchToContent(content: string | null, patchPath: string, targetFile: string): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Applies an already-read patch body to content. Batched callers that hold
|
|
40
|
+
* many (patch, file) pairs use this to read each patch body once instead of
|
|
41
|
+
* once per pair.
|
|
42
|
+
* @param content - Original content (null for new files)
|
|
43
|
+
* @param patchContent - Full text of the patch body
|
|
44
|
+
* @param targetFile - The file path within the patch
|
|
45
|
+
* @returns Modified content
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyPatchTextToContent(content: string | null, patchContent: string, targetFile: string): string;
|
|
@@ -71,11 +71,22 @@ export async function extractNewFileContent(patchPath, targetFile) {
|
|
|
71
71
|
* @returns Modified content
|
|
72
72
|
*/
|
|
73
73
|
export async function applyPatchToContent(content, patchPath, targetFile) {
|
|
74
|
-
|
|
74
|
+
return applyPatchTextToContent(content, await readText(patchPath), targetFile);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Applies an already-read patch body to content. Batched callers that hold
|
|
78
|
+
* many (patch, file) pairs use this to read each patch body once instead of
|
|
79
|
+
* once per pair.
|
|
80
|
+
* @param content - Original content (null for new files)
|
|
81
|
+
* @param patchContent - Full text of the patch body
|
|
82
|
+
* @param targetFile - The file path within the patch
|
|
83
|
+
* @returns Modified content
|
|
84
|
+
*/
|
|
85
|
+
export function applyPatchTextToContent(content, patchContent, targetFile) {
|
|
75
86
|
// Check if this is a new file patch for the target file specifically
|
|
76
87
|
if (content === null) {
|
|
77
88
|
if (isNewFileInPatch(patchContent, targetFile)) {
|
|
78
|
-
return
|
|
89
|
+
return extractNewFileContentFromDiff(patchContent, targetFile);
|
|
79
90
|
}
|
|
80
91
|
// If not a new file patch but content is null, return empty
|
|
81
92
|
return '';
|
|
@@ -35,7 +35,47 @@ export interface RebaseSession {
|
|
|
35
35
|
currentIndex: number;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Outcome of reading the session file, following the {@link LockOwner}
|
|
39
|
+
* discriminated-union form rather than collapsing three states into `null`.
|
|
40
|
+
*
|
|
41
|
+
* Distinguishing `present: false` from `present: true, valid: false` is
|
|
42
|
+
* load-bearing. Before 0.41.0 both returned `null` while
|
|
43
|
+
* {@link hasActiveRebaseSession} reported liveness from `pathExists` alone, so
|
|
44
|
+
* an invalid session file wedged the operator in a closed cycle: `rebase` said
|
|
45
|
+
* "already in progress — use --continue or --abort", and both of those said
|
|
46
|
+
* "no rebase session in progress". No CLI path deleted the file and no message
|
|
47
|
+
* named it, so the only escape was knowing to `rm` it by hand.
|
|
48
|
+
*/
|
|
49
|
+
export type RebaseSessionRead = {
|
|
50
|
+
present: false;
|
|
51
|
+
} | {
|
|
52
|
+
present: true;
|
|
53
|
+
valid: true;
|
|
54
|
+
session: RebaseSession;
|
|
55
|
+
} | {
|
|
56
|
+
present: true;
|
|
57
|
+
valid: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
/** Absolute path of the session file, for messages that must name it. */
|
|
61
|
+
export declare function getRebaseSessionPath(projectRoot: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Reads the session file, reporting absent, valid, and corrupt as three
|
|
64
|
+
* distinct outcomes. Never throws for a malformed file: `readJson` calls
|
|
65
|
+
* `JSON.parse` with no guard of its own, so an interrupted write surfaced a
|
|
66
|
+
* raw `SyntaxError` out of `--continue`/`--abort` before 0.41.0.
|
|
67
|
+
*
|
|
68
|
+
* A single read determines liveness and validity: only ENOENT/ENOTDIR from
|
|
69
|
+
* that read mean `absent`. A pathExists pre-probe both raced deletion (a file
|
|
70
|
+
* removed between probe and read misreported an absent session as corrupt)
|
|
71
|
+
* and swallowed EACCES (an unreadable `.fireforge/` misreported a session as
|
|
72
|
+
* absent) — the same failure `readTreeMarkerState` fixed for tree markers.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readRebaseSession(projectRoot: string): Promise<RebaseSessionRead>;
|
|
75
|
+
/**
|
|
76
|
+
* Loads an existing rebase session, or returns `null` when none exists or the
|
|
77
|
+
* file on disk is unusable. Callers that must tell those two apart — every
|
|
78
|
+
* command that reports to an operator — should use {@link readRebaseSession}.
|
|
39
79
|
*/
|
|
40
80
|
export declare function loadRebaseSession(projectRoot: string): Promise<RebaseSession | null>;
|
|
41
81
|
/**
|
|
@@ -47,6 +87,8 @@ export declare function saveRebaseSession(projectRoot: string, session: RebaseSe
|
|
|
47
87
|
*/
|
|
48
88
|
export declare function clearRebaseSession(projectRoot: string): Promise<void>;
|
|
49
89
|
/**
|
|
50
|
-
* Returns `true` when
|
|
90
|
+
* Returns `true` when a rebase session file exists on disk, valid or not.
|
|
91
|
+
* Test-facing convenience — production code reads `readRebaseSession` once
|
|
92
|
+
* instead, so liveness and validity come from the same read.
|
|
51
93
|
*/
|
|
52
94
|
export declare function hasActiveRebaseSession(projectRoot: string): Promise<boolean>;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* resume with `fireforge rebase --continue`.
|
|
7
7
|
*/
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
9
10
|
import { pathExists, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
10
|
-
import { isArray, isObject, isString } from '../utils/validation.js';
|
|
11
|
+
import { isArray, isNumber, isObject, isString, isValidFirefoxProduct, isValidFirefoxVersion, } from '../utils/validation.js';
|
|
11
12
|
import { getProjectPaths } from './config-paths.js';
|
|
12
13
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
13
14
|
// ── Helpers ──
|
|
@@ -15,28 +16,122 @@ const SESSION_FILENAME = 'rebase-session.json';
|
|
|
15
16
|
function sessionPath(projectRoot) {
|
|
16
17
|
return join(getProjectPaths(projectRoot).fireforgeDir, SESSION_FILENAME);
|
|
17
18
|
}
|
|
19
|
+
const REBASE_PATCH_STATUSES = [
|
|
20
|
+
'pending',
|
|
21
|
+
'applied-clean',
|
|
22
|
+
'applied-fuzz',
|
|
23
|
+
'failed',
|
|
24
|
+
'resolved',
|
|
25
|
+
'skipped',
|
|
26
|
+
];
|
|
27
|
+
/** Validates one `patches[]` element to its declared shape. */
|
|
28
|
+
function isValidPatchEntry(value) {
|
|
29
|
+
if (!isObject(value))
|
|
30
|
+
return false;
|
|
31
|
+
if (!isString(value['filename']) || value['filename'].length === 0)
|
|
32
|
+
return false;
|
|
33
|
+
if (!REBASE_PATCH_STATUSES.includes(value['status']))
|
|
34
|
+
return false;
|
|
35
|
+
if (value['fuzzFactor'] !== undefined && !isNumber(value['fuzzFactor']))
|
|
36
|
+
return false;
|
|
37
|
+
if (value['error'] !== undefined && !isString(value['error']))
|
|
38
|
+
return false;
|
|
39
|
+
const conflicting = value['conflictingFiles'];
|
|
40
|
+
if (conflicting !== undefined && (!isArray(conflicting) || !conflicting.every(isString))) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** Optional product field: absent is legal, present must be a real product. */
|
|
46
|
+
function isValidOptionalProduct(value) {
|
|
47
|
+
return value === undefined || (isString(value) && isValidFirefoxProduct(value));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validates a session file against the shape the rebase commands actually
|
|
51
|
+
* consume — not merely against broad field types.
|
|
52
|
+
*
|
|
53
|
+
* The looser predicate this replaces checked six `typeof`s and nothing else,
|
|
54
|
+
* which admitted sessions the resume path then acted on. `patches: [null]`
|
|
55
|
+
* reached `patch-loop.ts`, which reads `.filename` off each entry and hands it
|
|
56
|
+
* to `stampPatchVersions`. A `currentIndex` of `NaN` (legal for
|
|
57
|
+
* `typeof === 'number'`) made the resume loop run zero iterations and report
|
|
58
|
+
* success; a negative one indexed out of range in `continue.ts`. `toVersion:
|
|
59
|
+
* ""` passed `isString` and was stamped verbatim onto every Furnace override's
|
|
60
|
+
* `baseVersion`. `fromProduct`/`toProduct` were not checked at all despite
|
|
61
|
+
* being typed `FirefoxProduct`.
|
|
62
|
+
*
|
|
63
|
+
* The session file is written only by `rebase/index.ts` from validated CLI and
|
|
64
|
+
* config values, so every one of these is a corrupt-file or hand-edit case —
|
|
65
|
+
* which is exactly the case this predicate exists to catch.
|
|
66
|
+
*/
|
|
18
67
|
function isValidSession(data) {
|
|
19
68
|
if (!isObject(data))
|
|
20
69
|
return false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
70
|
+
if (!isString(data['startedAt']) || Number.isNaN(Date.parse(data['startedAt'])))
|
|
71
|
+
return false;
|
|
72
|
+
// 4 is git's own minimum abbreviation length; 40/64 are full SHA-1/SHA-256.
|
|
73
|
+
if (!isString(data['preRebaseCommit']) || !/^[0-9a-f]{4,64}$/i.test(data['preRebaseCommit'])) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (!isString(data['fromVersion']) || !isValidFirefoxVersion(data['fromVersion']))
|
|
77
|
+
return false;
|
|
78
|
+
if (!isString(data['toVersion']) || !isValidFirefoxVersion(data['toVersion']))
|
|
79
|
+
return false;
|
|
80
|
+
if (!isValidOptionalProduct(data['fromProduct']))
|
|
81
|
+
return false;
|
|
82
|
+
if (!isValidOptionalProduct(data['toProduct']))
|
|
83
|
+
return false;
|
|
84
|
+
const patches = data['patches'];
|
|
85
|
+
if (!isArray(patches) || !patches.every(isValidPatchEntry))
|
|
86
|
+
return false;
|
|
87
|
+
// `<= patches.length` because the resume point legally sits one past the
|
|
88
|
+
// last entry once every patch has been processed.
|
|
89
|
+
const currentIndex = data['currentIndex'];
|
|
90
|
+
if (!isNumber(currentIndex) || !Number.isInteger(currentIndex))
|
|
91
|
+
return false;
|
|
92
|
+
return currentIndex >= 0 && currentIndex <= patches.length;
|
|
93
|
+
}
|
|
94
|
+
/** Absolute path of the session file, for messages that must name it. */
|
|
95
|
+
export function getRebaseSessionPath(projectRoot) {
|
|
96
|
+
return sessionPath(projectRoot);
|
|
27
97
|
}
|
|
28
|
-
// ── Public API ──
|
|
29
98
|
/**
|
|
30
|
-
*
|
|
99
|
+
* Reads the session file, reporting absent, valid, and corrupt as three
|
|
100
|
+
* distinct outcomes. Never throws for a malformed file: `readJson` calls
|
|
101
|
+
* `JSON.parse` with no guard of its own, so an interrupted write surfaced a
|
|
102
|
+
* raw `SyntaxError` out of `--continue`/`--abort` before 0.41.0.
|
|
103
|
+
*
|
|
104
|
+
* A single read determines liveness and validity: only ENOENT/ENOTDIR from
|
|
105
|
+
* that read mean `absent`. A pathExists pre-probe both raced deletion (a file
|
|
106
|
+
* removed between probe and read misreported an absent session as corrupt)
|
|
107
|
+
* and swallowed EACCES (an unreadable `.fireforge/` misreported a session as
|
|
108
|
+
* absent) — the same failure `readTreeMarkerState` fixed for tree markers.
|
|
31
109
|
*/
|
|
32
|
-
export async function
|
|
110
|
+
export async function readRebaseSession(projectRoot) {
|
|
33
111
|
const path = sessionPath(projectRoot);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
112
|
+
let data;
|
|
113
|
+
try {
|
|
114
|
+
data = await readJson(path);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const code = getNodeErrorCode(error);
|
|
118
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
119
|
+
return { present: false };
|
|
120
|
+
return { present: true, valid: false, reason: toError(error).message };
|
|
121
|
+
}
|
|
122
|
+
if (!isValidSession(data)) {
|
|
123
|
+
return { present: true, valid: false, reason: 'the file is not a valid rebase session' };
|
|
124
|
+
}
|
|
125
|
+
return { present: true, valid: true, session: data };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Loads an existing rebase session, or returns `null` when none exists or the
|
|
129
|
+
* file on disk is unusable. Callers that must tell those two apart — every
|
|
130
|
+
* command that reports to an operator — should use {@link readRebaseSession}.
|
|
131
|
+
*/
|
|
132
|
+
export async function loadRebaseSession(projectRoot) {
|
|
133
|
+
const result = await readRebaseSession(projectRoot);
|
|
134
|
+
return result.present && result.valid ? result.session : null;
|
|
40
135
|
}
|
|
41
136
|
/**
|
|
42
137
|
* Persists a rebase session atomically.
|
|
@@ -57,7 +152,9 @@ export async function clearRebaseSession(projectRoot) {
|
|
|
57
152
|
}
|
|
58
153
|
}
|
|
59
154
|
/**
|
|
60
|
-
* Returns `true` when
|
|
155
|
+
* Returns `true` when a rebase session file exists on disk, valid or not.
|
|
156
|
+
* Test-facing convenience — production code reads `readRebaseSession` once
|
|
157
|
+
* instead, so liveness and validity come from the same read.
|
|
61
158
|
*/
|
|
62
159
|
export async function hasActiveRebaseSession(projectRoot) {
|
|
63
160
|
return pathExists(sessionPath(projectRoot));
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { GeneralError } from '../errors/base.js';
|
|
7
7
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
8
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
8
9
|
import { findAlphabeticalPosition, findAlphabeticalTokenPosition } from './manifest-helpers.js';
|
|
9
10
|
import { tokenizeJarMn } from './manifest-tokenizers.js';
|
|
10
11
|
import { withParserFallback } from './parser-fallback.js';
|
|
@@ -18,7 +19,7 @@ function registerBrowserContentTokenized(content, fileName, entry, after) {
|
|
|
18
19
|
let insertIndex;
|
|
19
20
|
let previousEntry;
|
|
20
21
|
if (after) {
|
|
21
|
-
const afterPattern = new RegExp(`(?:^|/)${after
|
|
22
|
+
const afterPattern = new RegExp(`(?:^|/)${escapeRegex(after)}(?:\\s|\\)|$)`);
|
|
22
23
|
const afterToken = tokens.find((t) => afterPattern.test(t.raw));
|
|
23
24
|
if (afterToken) {
|
|
24
25
|
insertIndex = afterToken.lineIndex + 1;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { GeneralError } from '../errors/base.js';
|
|
7
7
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
8
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
8
9
|
import { findAlphabeticalPosition, findAlphabeticalTokenPosition } from './manifest-helpers.js';
|
|
9
10
|
import { tokenizeJarMn } from './manifest-tokenizers.js';
|
|
10
11
|
import { withParserFallback } from './parser-fallback.js';
|
|
@@ -18,7 +19,7 @@ function registerSharedCSSTokenized(content, name, entry, after) {
|
|
|
18
19
|
let insertIndex;
|
|
19
20
|
let previousEntry;
|
|
20
21
|
if (after) {
|
|
21
|
-
const afterPattern = new RegExp(`(?:^|/)${after
|
|
22
|
+
const afterPattern = new RegExp(`(?:^|/)${escapeRegex(after)}(?:\\s|\\)|$)`);
|
|
22
23
|
const afterToken = tokens.find((t) => afterPattern.test(t.raw));
|
|
23
24
|
if (afterToken) {
|
|
24
25
|
insertIndex = afterToken.lineIndex + 1;
|
|
@@ -10,36 +10,44 @@
|
|
|
10
10
|
* warning (e.g. `"pending JavaScript error cleanup"`) do not start with
|
|
11
11
|
* the prefix and should not trip the scanner.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
14
|
-
* Line prefixes that signal an actual runtime error in a Firefox chrome
|
|
15
|
-
* process. A hit on any of these patterns is a smoke failure unless the
|
|
16
|
-
* line also matches the caller-supplied allowlist.
|
|
17
|
-
*
|
|
18
|
-
* Additions here should be conservative — false positives turn every
|
|
19
|
-
* smoke run into noise for operators and every CI run into flake.
|
|
20
|
-
*/
|
|
21
13
|
export declare const SMOKE_ERROR_PATTERNS: readonly RegExp[];
|
|
22
14
|
/**
|
|
23
15
|
* Returns `true` when `line` matches any pattern in
|
|
24
16
|
* {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
|
|
25
|
-
* lives in {@link
|
|
17
|
+
* lives in {@link matchAllowlist}, so the smoke runner can count
|
|
26
18
|
* allowlisted hits separately from raw error matches for its summary.
|
|
27
19
|
*/
|
|
28
20
|
export declare function matchesSmokeError(line: string): boolean;
|
|
29
21
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
22
|
+
* One compiled allowlist entry with its provenance retained so the smoke
|
|
23
|
+
* summary can attribute hits per entry (FORGE G8) — an entry that silently
|
|
24
|
+
* stops matching (its suppressed shape changed upstream) is only
|
|
25
|
+
* detectable when zero-hit entries are visible.
|
|
26
|
+
*/
|
|
27
|
+
export interface CompiledAllowlistEntry {
|
|
28
|
+
pattern: RegExp;
|
|
29
|
+
/** Verbatim source text of the entry. */
|
|
30
|
+
source: string;
|
|
31
|
+
/** Human-readable origin: `allow.txt:12` or `--console-allow #2`. */
|
|
32
|
+
origin: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the index of the FIRST entry in `allow` matching `line`, or -1.
|
|
36
|
+
* First-match attribution is deterministic and cheap; a line matching
|
|
37
|
+
* several entries credits the earliest one. Safe to call with an empty
|
|
38
|
+
* allowlist (always returns -1).
|
|
32
39
|
*/
|
|
33
|
-
export declare function
|
|
40
|
+
export declare function matchAllowlist(line: string, allow: readonly CompiledAllowlistEntry[]): number;
|
|
34
41
|
/**
|
|
35
42
|
* Parses a newline-delimited allowlist file body. Lines are trimmed; blank
|
|
36
43
|
* lines and `#`-prefixed comments are skipped. Each remaining line is
|
|
37
|
-
* compiled as a RegExp
|
|
38
|
-
*
|
|
44
|
+
* compiled as a RegExp with its `<file>:<line>` origin retained. A bad
|
|
45
|
+
* pattern throws immediately — better to fail fast at CLI parse time than
|
|
46
|
+
* to silently let a typo match nothing.
|
|
39
47
|
*/
|
|
40
|
-
export declare function compileAllowlistFromFile(body: string, sourcePath: string):
|
|
48
|
+
export declare function compileAllowlistFromFile(body: string, sourcePath: string): CompiledAllowlistEntry[];
|
|
41
49
|
/**
|
|
42
50
|
* Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
|
|
43
51
|
* flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
|
|
44
52
|
*/
|
|
45
|
-
export declare function compileAllowlistFromStrings(sources: readonly string[]):
|
|
53
|
+
export declare function compileAllowlistFromStrings(sources: readonly string[]): CompiledAllowlistEntry[];
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* Additions here should be conservative — false positives turn every
|
|
20
20
|
* smoke run into noise for operators and every CI run into flake.
|
|
21
21
|
*/
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
22
23
|
export const SMOKE_ERROR_PATTERNS = [
|
|
23
24
|
// Firefox chrome error lines — `JavaScript error: chrome://…, line N: TypeError: …`.
|
|
24
25
|
/^\s*JavaScript error:/i,
|
|
@@ -32,7 +33,7 @@ export const SMOKE_ERROR_PATTERNS = [
|
|
|
32
33
|
/**
|
|
33
34
|
* Returns `true` when `line` matches any pattern in
|
|
34
35
|
* {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
|
|
35
|
-
* lives in {@link
|
|
36
|
+
* lives in {@link matchAllowlist}, so the smoke runner can count
|
|
36
37
|
* allowlisted hits separately from raw error matches for its summary.
|
|
37
38
|
*/
|
|
38
39
|
export function matchesSmokeError(line) {
|
|
@@ -44,22 +45,25 @@ export function matchesSmokeError(line) {
|
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
* Returns
|
|
48
|
-
*
|
|
48
|
+
* Returns the index of the FIRST entry in `allow` matching `line`, or -1.
|
|
49
|
+
* First-match attribution is deterministic and cheap; a line matching
|
|
50
|
+
* several entries credits the earliest one. Safe to call with an empty
|
|
51
|
+
* allowlist (always returns -1).
|
|
49
52
|
*/
|
|
50
|
-
export function
|
|
51
|
-
for (
|
|
52
|
-
if (pattern.test(line)) {
|
|
53
|
-
return
|
|
53
|
+
export function matchAllowlist(line, allow) {
|
|
54
|
+
for (let i = 0; i < allow.length; i++) {
|
|
55
|
+
if (allow[i]?.pattern.test(line)) {
|
|
56
|
+
return i;
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
|
-
return
|
|
59
|
+
return -1;
|
|
57
60
|
}
|
|
58
61
|
/**
|
|
59
62
|
* Parses a newline-delimited allowlist file body. Lines are trimmed; blank
|
|
60
63
|
* lines and `#`-prefixed comments are skipped. Each remaining line is
|
|
61
|
-
* compiled as a RegExp
|
|
62
|
-
*
|
|
64
|
+
* compiled as a RegExp with its `<file>:<line>` origin retained. A bad
|
|
65
|
+
* pattern throws immediately — better to fail fast at CLI parse time than
|
|
66
|
+
* to silently let a typo match nothing.
|
|
63
67
|
*/
|
|
64
68
|
export function compileAllowlistFromFile(body, sourcePath) {
|
|
65
69
|
const lines = body.split(/\r?\n/);
|
|
@@ -69,10 +73,14 @@ export function compileAllowlistFromFile(body, sourcePath) {
|
|
|
69
73
|
if (!line || line.startsWith('#'))
|
|
70
74
|
return;
|
|
71
75
|
try {
|
|
72
|
-
compiled.push(
|
|
76
|
+
compiled.push({
|
|
77
|
+
pattern: new RegExp(line),
|
|
78
|
+
source: line,
|
|
79
|
+
origin: `${sourcePath}:${String(index + 1)}`,
|
|
80
|
+
});
|
|
73
81
|
}
|
|
74
82
|
catch (error) {
|
|
75
|
-
const message = error
|
|
83
|
+
const message = toError(error).message;
|
|
76
84
|
throw new Error(`Invalid allowlist regex at ${sourcePath}:${String(index + 1)}: ${message}`, {
|
|
77
85
|
cause: error,
|
|
78
86
|
});
|
|
@@ -88,10 +96,14 @@ export function compileAllowlistFromStrings(sources) {
|
|
|
88
96
|
const compiled = [];
|
|
89
97
|
sources.forEach((source, index) => {
|
|
90
98
|
try {
|
|
91
|
-
compiled.push(
|
|
99
|
+
compiled.push({
|
|
100
|
+
pattern: new RegExp(source),
|
|
101
|
+
source,
|
|
102
|
+
origin: `--console-allow #${String(index + 1)}`,
|
|
103
|
+
});
|
|
92
104
|
}
|
|
93
105
|
catch (error) {
|
|
94
|
-
const message = error
|
|
106
|
+
const message = toError(error).message;
|
|
95
107
|
throw new Error(`Invalid --console-allow regex at position ${String(index + 1)} ("${source}"): ${message}`, { cause: error });
|
|
96
108
|
}
|
|
97
109
|
});
|