@hominis/fireforge 0.40.0 → 0.42.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 +85 -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/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- 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-mozconfig.js +9 -1
- 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
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
* `tree remove` + `tree create`.
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import {
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { lstat, readdir, readFile, realpath, rm } from 'node:fs/promises';
|
|
16
17
|
import { basename, join, resolve, sep } from 'node:path';
|
|
17
18
|
import { GeneralError } from '../errors/base.js';
|
|
19
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
20
|
+
import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
|
|
18
21
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
19
|
-
import { verbose } from '../utils/logger.js';
|
|
20
|
-
import {
|
|
22
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
23
|
+
import { isObject } from '../utils/validation.js';
|
|
24
|
+
import { getBuildBaselinePath } from './build-baseline.js';
|
|
25
|
+
import { LOCK_PID_FILE, withFileLock } from './file-lock.js';
|
|
21
26
|
import { git } from './git-base.js';
|
|
27
|
+
import { getAllDiff } from './git-diff.js';
|
|
28
|
+
import { attemptMozinfoRewrite } from './mach-build-artifacts.js';
|
|
22
29
|
import { cloneEntry } from './tree-cow.js';
|
|
23
30
|
const TREE_MARKER_FILENAME = 'tree.json';
|
|
24
31
|
const TREES_DIRNAME = 'trees';
|
|
32
|
+
const TREE_CLONE_CONCURRENCY = 8;
|
|
25
33
|
/**
|
|
26
34
|
* Top-level entries never cloned into a tree: per-tree state must start
|
|
27
35
|
* clean (`.fireforge/`), locks are never inherited, the destructive-
|
|
@@ -45,38 +53,137 @@ export async function withTreeLifecycleLock(primaryRoot, operation) {
|
|
|
45
53
|
onTimeoutMessage: 'Another fireforge tree create/remove is in progress. Wait for it to finish, then retry.',
|
|
46
54
|
});
|
|
47
55
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Validates every field of a tree marker.
|
|
58
|
+
*
|
|
59
|
+
* The read below checked only `schemaVersion` and `name` and asserted the
|
|
60
|
+
* rest, but the unvalidated fields are consumed unguarded: `primaryRoot`
|
|
61
|
+
* reaches a user-facing refusal message (`tree-guard.ts:105`), and
|
|
62
|
+
* `engineHead`/`patchesFingerprint` feed the staleness compare, where an
|
|
63
|
+
* `undefined` from a truncated marker reports every tree as stale.
|
|
64
|
+
*/
|
|
65
|
+
function isTreeMarker(value) {
|
|
66
|
+
if (!isObject(value))
|
|
67
|
+
return false;
|
|
68
|
+
return (value['schemaVersion'] === 1 &&
|
|
69
|
+
typeof value['name'] === 'string' &&
|
|
70
|
+
typeof value['primaryRoot'] === 'string' &&
|
|
71
|
+
typeof value['createdAt'] === 'string' &&
|
|
72
|
+
(typeof value['engineHead'] === 'string' || value['engineHead'] === null) &&
|
|
73
|
+
(value['engineFingerprint'] === undefined ||
|
|
74
|
+
typeof value['engineFingerprint'] === 'string' ||
|
|
75
|
+
value['engineFingerprint'] === null) &&
|
|
76
|
+
(typeof value['patchesFingerprint'] === 'string' || value['patchesFingerprint'] === null) &&
|
|
77
|
+
(value['clonedObjdir'] === undefined || typeof value['clonedObjdir'] === 'string'));
|
|
78
|
+
}
|
|
79
|
+
/** Absolute path of a root's tree marker. */
|
|
80
|
+
export function getTreeMarkerPath(root) {
|
|
81
|
+
return join(root, '.fireforge', TREE_MARKER_FILENAME);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reads the tree marker for a project root, distinguishing "not a tree" from
|
|
85
|
+
* "a tree whose marker we could not read". Callers that make a safety decision
|
|
86
|
+
* (the guard hook, the no-nesting refusal) must use this rather than
|
|
87
|
+
* {@link readTreeMarker}.
|
|
88
|
+
*
|
|
89
|
+
* Only ENOENT/ENOTDIR mean `absent`: any other probe failure (EACCES on the
|
|
90
|
+
* `.fireforge` directory, EIO, …) reports `corrupt`, because "we cannot read
|
|
91
|
+
* the marker" is not evidence that there is no marker — a `pathExists`
|
|
92
|
+
* pre-check here classified a tree with an unreadable `.fireforge` as
|
|
93
|
+
* `absent` and handed it the full mutating command set.
|
|
94
|
+
*/
|
|
95
|
+
export async function readTreeMarkerState(root) {
|
|
96
|
+
const markerPath = getTreeMarkerPath(root);
|
|
53
97
|
try {
|
|
54
98
|
const raw = await readJson(markerPath);
|
|
55
|
-
if (raw
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
if (!isTreeMarker(raw)) {
|
|
100
|
+
verbose(`Malformed tree marker at ${markerPath}`);
|
|
101
|
+
return { kind: 'corrupt', reason: 'the marker is missing or mistypes a required field' };
|
|
102
|
+
}
|
|
103
|
+
return { kind: 'valid', marker: raw };
|
|
58
104
|
}
|
|
59
105
|
catch (error) {
|
|
106
|
+
const code = getNodeErrorCode(error);
|
|
107
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
108
|
+
return { kind: 'absent' };
|
|
60
109
|
verbose(`Unreadable tree marker at ${markerPath}: ${String(error)}`);
|
|
61
|
-
return
|
|
110
|
+
return { kind: 'corrupt', reason: `the marker could not be read (${toError(error).message})` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Reads the tree marker for a project root, or undefined when it is absent
|
|
115
|
+
* OR unreadable.
|
|
116
|
+
*
|
|
117
|
+
* For display and best-effort paths only (`tree list` renders an unreadable
|
|
118
|
+
* marker as staleness 'unknown'). Anything that decides whether a mutation may
|
|
119
|
+
* proceed must call {@link readTreeMarkerState} and handle `corrupt`.
|
|
120
|
+
*/
|
|
121
|
+
export async function readTreeMarker(root) {
|
|
122
|
+
const state = await readTreeMarkerState(root);
|
|
123
|
+
return state.kind === 'valid' ? state.marker : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Refuses a build-less in-tree `test` when the objdir preflight found is not
|
|
127
|
+
* the one this tree's marker vouched for as rewritten-and-reconfigured. A
|
|
128
|
+
* mismatch means an objdir appeared in the tree through some path other than
|
|
129
|
+
* `tree create --with-objdir` (manual copy, partial restore), so nothing
|
|
130
|
+
* proves it was relocated — running against it could consult primary paths.
|
|
131
|
+
* No-op outside a tree; a corrupt marker is already refused by the guard.
|
|
132
|
+
*/
|
|
133
|
+
export async function assertObjdirMatchesTreeMarker(projectRoot, objDir) {
|
|
134
|
+
const state = await readTreeMarkerState(projectRoot);
|
|
135
|
+
if (state.kind !== 'valid' || state.marker.clonedObjdir === undefined)
|
|
136
|
+
return;
|
|
137
|
+
if (objDir !== state.marker.clonedObjdir) {
|
|
138
|
+
throw new GeneralError(`This verification tree's marker records "${state.marker.clonedObjdir}" as its cloned build, ` +
|
|
139
|
+
`but the preflight found ${objDir === undefined ? 'no objdir' : `"${objDir}"`}. ` +
|
|
140
|
+
'Only the objdir cloned by tree create --with-objdir is proven to be relocated to this tree. ' +
|
|
141
|
+
'Remove and re-create the tree (fireforge tree remove/create).');
|
|
62
142
|
}
|
|
63
143
|
}
|
|
64
|
-
/** Current primary-side snapshot identity
|
|
144
|
+
/** Current primary-side snapshot identity, including dirty source and patch bodies. */
|
|
65
145
|
export async function computePrimaryFingerprint(primaryRoot) {
|
|
146
|
+
const engineDir = join(primaryRoot, 'engine');
|
|
66
147
|
let engineHead;
|
|
67
148
|
try {
|
|
68
|
-
engineHead = (await git(['rev-parse', 'HEAD'],
|
|
149
|
+
engineHead = (await git(['rev-parse', 'HEAD'], engineDir)).trim();
|
|
69
150
|
}
|
|
70
151
|
catch {
|
|
152
|
+
// An engine that is missing, unborn, or not a git checkout has no HEAD to
|
|
153
|
+
// fingerprint. `null` is a legal marker value and reads as 'unknown', which
|
|
154
|
+
// `listTrees` renders as staleness 'unknown' rather than a false 'fresh'.
|
|
71
155
|
engineHead = null;
|
|
72
156
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.update(await
|
|
77
|
-
.digest('hex')
|
|
78
|
-
|
|
79
|
-
|
|
157
|
+
let engineFingerprint;
|
|
158
|
+
try {
|
|
159
|
+
engineFingerprint = createHash('sha256')
|
|
160
|
+
.update(await getAllDiff(engineDir))
|
|
161
|
+
.digest('hex');
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
engineFingerprint = null;
|
|
165
|
+
}
|
|
166
|
+
const patchesDir = join(primaryRoot, 'patches');
|
|
167
|
+
let patchesFingerprint = null;
|
|
168
|
+
try {
|
|
169
|
+
if (await pathExists(patchesDir)) {
|
|
170
|
+
const hash = createHash('sha256');
|
|
171
|
+
for (const name of (await readdir(patchesDir))
|
|
172
|
+
.filter((entry) => entry.endsWith('.patch') || entry === 'patches.json')
|
|
173
|
+
.sort()) {
|
|
174
|
+
hash
|
|
175
|
+
.update(name)
|
|
176
|
+
.update('\0')
|
|
177
|
+
.update(await readFile(join(patchesDir, name)))
|
|
178
|
+
.update('\0');
|
|
179
|
+
}
|
|
180
|
+
patchesFingerprint = hash.digest('hex');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
patchesFingerprint = null;
|
|
185
|
+
}
|
|
186
|
+
return { engineHead, engineFingerprint, patchesFingerprint };
|
|
80
187
|
}
|
|
81
188
|
/** Validates a tree name: one path segment, no traversal. */
|
|
82
189
|
export function assertValidTreeName(name) {
|
|
@@ -84,54 +191,156 @@ export function assertValidTreeName(name) {
|
|
|
84
191
|
throw new GeneralError(`Invalid tree name "${name}". Use a single path segment of letters, digits, ".", "_" or "-".`);
|
|
85
192
|
}
|
|
86
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Refuses an objdir that is not a plain directory physically inside
|
|
196
|
+
* `engineDir` — every `cp` mode `cloneEntry` uses preserves symlinks, so
|
|
197
|
+
* a symlinked `engine/obj-*` would be cloned as a symlink pointing back
|
|
198
|
+
* at the original (primary or fully external) build, and the clone's
|
|
199
|
+
* mozinfo rewrite and `_virtualenvs` removal would then mutate that
|
|
200
|
+
* original through the link. Checks lstat (the entry itself must be a
|
|
201
|
+
* real directory) AND realpath containment (a real directory reached
|
|
202
|
+
* through a symlinked parent resolves elsewhere) — the rewriter's
|
|
203
|
+
* lexical `resolve()` paths cannot detect either. Also validates the
|
|
204
|
+
* name as a single `obj-*` path segment. Role `'primary'` guards before
|
|
205
|
+
* any copying starts; `'cloned'` re-checks the tree's copy before any
|
|
206
|
+
* write goes through it (defense in depth).
|
|
207
|
+
*/
|
|
208
|
+
async function assertCloneSafeObjdir(engineDir, objDir, role) {
|
|
209
|
+
const where = role === 'primary' ? 'engine/' : "the tree's engine/";
|
|
210
|
+
if (!/^obj-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(objDir) || objDir.includes('..')) {
|
|
211
|
+
throw new GeneralError(`Invalid objdir name "${objDir}". Expected a single obj-* path segment of letters, digits, ".", "_" or "-".`);
|
|
212
|
+
}
|
|
213
|
+
const objDirPath = join(engineDir, objDir);
|
|
214
|
+
let stats;
|
|
215
|
+
try {
|
|
216
|
+
stats = await lstat(objDirPath);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
throw new GeneralError(`tree create --with-objdir cannot stat ${where}${objDir}: ${toError(error).message}`);
|
|
220
|
+
}
|
|
221
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
222
|
+
throw new GeneralError(`tree create --with-objdir refuses ${where}${objDir}: it is ${stats.isSymbolicLink() ? 'a symlink' : 'not a directory'}. A cloned symlinked objdir would be rewritten through the link, mutating the ` +
|
|
223
|
+
'original build it points at. Replace it with a real directory or create the tree ' +
|
|
224
|
+
'without --with-objdir.');
|
|
225
|
+
}
|
|
226
|
+
const [realObjDir, realEngineDir] = await Promise.all([
|
|
227
|
+
realpath(objDirPath),
|
|
228
|
+
realpath(engineDir),
|
|
229
|
+
]);
|
|
230
|
+
if (realObjDir !== join(realEngineDir, objDir)) {
|
|
231
|
+
throw new GeneralError(`tree create --with-objdir refuses ${where}${objDir}: it resolves to ${realObjDir}, ` +
|
|
232
|
+
`outside ${realEngineDir}. An objdir reached through a symlink would be rewritten ` +
|
|
233
|
+
'in place, mutating the original build. Replace it with a real directory or create ' +
|
|
234
|
+
'the tree without --with-objdir.');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
87
237
|
/**
|
|
88
238
|
* Materialises the tree at `treeRoot` from `primaryRoot` using
|
|
89
239
|
* `capability` ('none' = plain copy, already gated on --force-copy by
|
|
90
|
-
* the caller). The engine
|
|
91
|
-
* directories
|
|
92
|
-
*
|
|
93
|
-
*
|
|
240
|
+
* the caller). The engine is cloned one TOP-LEVEL entry at a time so unwanted
|
|
241
|
+
* `obj-*` directories are never traversed or materialised. That distinction
|
|
242
|
+
* is substantial on Firefox trees: even CoW-cloning and deleting an objdir
|
|
243
|
+
* touches metadata for millions of build outputs. With `withObjdir` exactly
|
|
244
|
+
* that objdir is included — after {@link assertCloneSafeObjdir}
|
|
245
|
+
* refuses symlinked or engine-escaping objdirs before any copying —
|
|
246
|
+
* rewritten to the tree (see below), and
|
|
247
|
+
* reconfigured in-tree via the caller-supplied `reconfigure` hook. A
|
|
248
|
+
* fresh `.fireforge/` is created with only `state.json` (when present),
|
|
249
|
+
* the last-build baseline (withObjdir only), and the tree marker.
|
|
94
250
|
*/
|
|
95
251
|
export async function cloneTree(args) {
|
|
96
|
-
const { primaryRoot, treeRoot, name, capability, createdAt } = args;
|
|
252
|
+
const { primaryRoot, treeRoot, name, capability, createdAt, withObjdir } = args;
|
|
97
253
|
const { mkdir, cp } = await import('node:fs/promises');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await
|
|
254
|
+
if (withObjdir) {
|
|
255
|
+
// Refuse a symlinked/escaping primary objdir BEFORE any copying: the
|
|
256
|
+
// clone would carry the symlink and every later write would land in
|
|
257
|
+
// the original build (see assertCloneSafeObjdir).
|
|
258
|
+
await assertCloneSafeObjdir(join(primaryRoot, 'engine'), withObjdir.objDir, 'primary');
|
|
103
259
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// in-tree builds/tests would silently operate against the primary —
|
|
107
|
-
// remove the clones; `tree` is read-verification only in 0.40.0.
|
|
260
|
+
await mkdir(treeRoot, { recursive: true });
|
|
261
|
+
const rootEntries = (await readdir(primaryRoot)).filter((entry) => !isExcludedRootEntry(entry));
|
|
108
262
|
const engineDir = join(treeRoot, 'engine');
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
263
|
+
await mapWithConcurrency(rootEntries, TREE_CLONE_CONCURRENCY, async (entry) => {
|
|
264
|
+
if (entry !== 'engine') {
|
|
265
|
+
await cloneEntry(capability, join(primaryRoot, entry), join(treeRoot, entry));
|
|
266
|
+
return;
|
|
114
267
|
}
|
|
268
|
+
const primaryEngineDir = join(primaryRoot, 'engine');
|
|
269
|
+
await mkdir(engineDir, { recursive: true });
|
|
270
|
+
const engineEntries = (await readdir(primaryEngineDir)).filter((engineEntry) => !engineEntry.startsWith('obj-') || engineEntry === withObjdir?.objDir);
|
|
271
|
+
await mapWithConcurrency(engineEntries, TREE_CLONE_CONCURRENCY, async (engineEntry) => {
|
|
272
|
+
await cloneEntry(capability, join(primaryEngineDir, engineEntry), join(engineDir, engineEntry));
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
// A selected objdir embeds absolute topsrcdir/topobjdir paths back into
|
|
276
|
+
// the PRIMARY tree. It is rewritten below before the marker records it as
|
|
277
|
+
// usable; no other objdir ever enters the snapshot.
|
|
278
|
+
if (await pathExists(engineDir)) {
|
|
115
279
|
// Defensive: never inherit a mid-operation git index lock.
|
|
116
280
|
await rm(join(engineDir, '.git', 'index.lock'), { force: true });
|
|
117
281
|
}
|
|
282
|
+
if (withObjdir) {
|
|
283
|
+
await rewriteClonedObjdir(engineDir, withObjdir.objDir);
|
|
284
|
+
// Regenerate configure output (config.status, backend.mk, Makefile,
|
|
285
|
+
// config/autoconf.mk) against the tree's paths — the mozinfo rewrite
|
|
286
|
+
// alone leaves those naming the primary. A failed reconfigure throws
|
|
287
|
+
// before the marker below records the objdir as usable (fail-closed).
|
|
288
|
+
await withObjdir.reconfigure(engineDir);
|
|
289
|
+
}
|
|
118
290
|
await mkdir(join(treeRoot, '.fireforge'), { recursive: true });
|
|
119
291
|
const stateJson = join(primaryRoot, '.fireforge', 'state.json');
|
|
120
292
|
if (await pathExists(stateJson)) {
|
|
121
293
|
await cp(stateJson, join(treeRoot, '.fireforge', 'state.json'));
|
|
122
294
|
}
|
|
123
|
-
|
|
295
|
+
if (withObjdir) {
|
|
296
|
+
// The in-tree stale-build and static-components gates anchor on the
|
|
297
|
+
// last-build baseline; without the copy they would read `undefined`
|
|
298
|
+
// and degrade to first-build semantics inside a fully built tree.
|
|
299
|
+
const baseline = getBuildBaselinePath(primaryRoot);
|
|
300
|
+
if (await pathExists(baseline)) {
|
|
301
|
+
await cp(baseline, getBuildBaselinePath(treeRoot));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const { engineHead, engineFingerprint, patchesFingerprint } = await computePrimaryFingerprint(primaryRoot);
|
|
124
305
|
const marker = {
|
|
125
306
|
schemaVersion: 1,
|
|
126
307
|
name,
|
|
127
308
|
primaryRoot,
|
|
128
309
|
createdAt,
|
|
129
310
|
engineHead,
|
|
311
|
+
engineFingerprint: engineFingerprint ?? null,
|
|
130
312
|
patchesFingerprint,
|
|
313
|
+
...(withObjdir ? { clonedObjdir: withObjdir.objDir } : {}),
|
|
131
314
|
};
|
|
132
315
|
await writeJson(join(treeRoot, '.fireforge', TREE_MARKER_FILENAME), marker);
|
|
133
316
|
return marker;
|
|
134
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Points the cloned objdir at the tree: rewrites mozinfo.json's
|
|
320
|
+
* topsrcdir/topobjdir (and an in-tree mozconfig) via the same
|
|
321
|
+
* safe-relocation rewriter `build --rewrite-mozinfo` uses, refusing
|
|
322
|
+
* fail-closed when its safety rules cannot prove the rewrite correct —
|
|
323
|
+
* a tree must never hold an objdir that still names the primary. Before
|
|
324
|
+
* any write, the cloned objdir is re-checked as a real directory inside
|
|
325
|
+
* the tree ({@link assertCloneSafeObjdir}) so no write ever goes through
|
|
326
|
+
* a symlink into the original build. The cloned `_virtualenvs` are
|
|
327
|
+
* removed outright: their scripts carry primary-tree shebang paths, and
|
|
328
|
+
* mach rebuilds venvs in-tree on first use. The configure-generated root
|
|
329
|
+
* files (config.status, backend.mk, Makefile, config/autoconf.mk) still
|
|
330
|
+
* name the primary after this rewrite; they are regenerated by the
|
|
331
|
+
* in-tree `mach configure` the caller supplies to {@link cloneTree},
|
|
332
|
+
* which runs before the marker is written.
|
|
333
|
+
*/
|
|
334
|
+
async function rewriteClonedObjdir(engineDir, objDir) {
|
|
335
|
+
await assertCloneSafeObjdir(engineDir, objDir, 'cloned');
|
|
336
|
+
const rewrite = await attemptMozinfoRewrite(engineDir, objDir);
|
|
337
|
+
if (!rewrite.rewritten) {
|
|
338
|
+
throw new GeneralError(`Cannot keep the cloned build: ${rewrite.reason ?? 'mozinfo.json rewrite refused'}. ` +
|
|
339
|
+
'The objdir would still operate against the primary tree. ' +
|
|
340
|
+
'Re-run tree create without --with-objdir, or rebuild the primary objdir with "fireforge build" and retry.');
|
|
341
|
+
}
|
|
342
|
+
await rm(join(engineDir, objDir, '_virtualenvs'), { recursive: true, force: true });
|
|
343
|
+
}
|
|
135
344
|
/** Lists trees with staleness against the current primary state. */
|
|
136
345
|
export async function listTrees(primaryRoot) {
|
|
137
346
|
const treesDir = getTreesDir(primaryRoot);
|
|
@@ -150,47 +359,68 @@ export async function listTrees(primaryRoot) {
|
|
|
150
359
|
}
|
|
151
360
|
const staleness = marker.engineHead !== current.engineHead
|
|
152
361
|
? 'stale (engine advanced)'
|
|
153
|
-
: marker.
|
|
154
|
-
|
|
155
|
-
|
|
362
|
+
: marker.engineFingerprint !== undefined &&
|
|
363
|
+
marker.engineFingerprint !== current.engineFingerprint
|
|
364
|
+
? 'stale (engine worktree changed)'
|
|
365
|
+
: marker.patchesFingerprint !== current.patchesFingerprint
|
|
366
|
+
? 'stale (patches changed)'
|
|
367
|
+
: 'fresh';
|
|
156
368
|
entries.push({ name, path, createdAt: marker.createdAt, staleness });
|
|
157
369
|
}
|
|
158
370
|
return entries;
|
|
159
371
|
}
|
|
160
|
-
/** Uses `kill(pid, 0)` — no signal sent, just an existence check. */
|
|
161
|
-
function isProcessAlive(pid) {
|
|
162
|
-
try {
|
|
163
|
-
process.kill(pid, 0);
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
372
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* internals.
|
|
373
|
+
* Classifies a mkdir-style lock directory. Reads the lock's owner file
|
|
374
|
+
* directly (line 1 = PID — the format `file-lock.ts` documents as stable,
|
|
375
|
+
* whose name it shares via {@link LOCK_PID_FILE}) instead of exporting
|
|
376
|
+
* file-lock internals.
|
|
377
|
+
*
|
|
378
|
+
* Liveness comes from the shared {@link isProcessAlive}, which treats EPERM
|
|
379
|
+
* as alive. The local copy this replaced returned `false` for EPERM, so a
|
|
380
|
+
* tree whose build lock was held by a live process running under a different
|
|
381
|
+
* uid passed the guard below and was `rm -rf`'d mid-build.
|
|
382
|
+
*
|
|
383
|
+
* The `unknown` state is not a theoretical case. `withFileLock` takes the lock
|
|
384
|
+
* by creating the directory and only *then* writes the owner file, treating a
|
|
385
|
+
* write failure as non-fatal — so every acquisition passes through a window in
|
|
386
|
+
* which the lock is genuinely held and has no readable PID, and a read-only or
|
|
387
|
+
* full filesystem leaves it that way for the lock's whole life. Reporting that
|
|
388
|
+
* as `free` let `removeTree` recursively delete a tree out from under a live
|
|
389
|
+
* build. `file-lock.ts` reaches the same conclusion for its own stale
|
|
390
|
+
* recovery, where an unreadable PID falls back to an age-only heuristic rather
|
|
391
|
+
* than assuming the lock is abandoned.
|
|
175
392
|
*/
|
|
176
|
-
async function
|
|
393
|
+
async function inspectTreeLock(lockPath) {
|
|
394
|
+
if (!existsSync(lockPath))
|
|
395
|
+
return { kind: 'free' };
|
|
396
|
+
let pidLine;
|
|
177
397
|
try {
|
|
178
|
-
|
|
179
|
-
const pid = parseInt(pidLine.trim(), 10);
|
|
180
|
-
if (Number.isFinite(pid) && isProcessAlive(pid))
|
|
181
|
-
return pid;
|
|
398
|
+
pidLine = (await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8')).split('\n')[0] ?? '';
|
|
182
399
|
}
|
|
183
|
-
catch {
|
|
184
|
-
//
|
|
400
|
+
catch (error) {
|
|
401
|
+
// A lock cleanly released between the existence probe above and this read
|
|
402
|
+
// leaves no directory behind — that is a release, not an unreadable owner,
|
|
403
|
+
// and demanding --force for it would be a spurious refusal.
|
|
404
|
+
if (!existsSync(lockPath))
|
|
405
|
+
return { kind: 'free' };
|
|
406
|
+
return {
|
|
407
|
+
kind: 'unknown',
|
|
408
|
+
reason: `its owner record is missing or unreadable (${toError(error).message})`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const pid = parseInt(pidLine.trim(), 10);
|
|
412
|
+
if (!Number.isFinite(pid)) {
|
|
413
|
+
return { kind: 'unknown', reason: 'its owner record does not name a process id' };
|
|
185
414
|
}
|
|
186
|
-
return
|
|
415
|
+
return isProcessAlive(pid) ? { kind: 'held', pid } : { kind: 'free' };
|
|
187
416
|
}
|
|
188
417
|
/**
|
|
189
418
|
* Removes one tree. Refuses when a live process holds the tree's build
|
|
190
|
-
* or engine-session lock
|
|
191
|
-
*
|
|
419
|
+
* or engine-session lock — or when a lock directory exists whose ownership
|
|
420
|
+
* cannot be established, which `--force` overrides — and never deletes a path
|
|
421
|
+
* outside the trees directory (containment check before `rm -rf`).
|
|
192
422
|
*/
|
|
193
|
-
export async function removeTree(primaryRoot, name) {
|
|
423
|
+
export async function removeTree(primaryRoot, name, options = {}) {
|
|
194
424
|
assertValidTreeName(name);
|
|
195
425
|
const treesDir = getTreesDir(primaryRoot);
|
|
196
426
|
const treeRoot = resolve(treesDir, name);
|
|
@@ -204,11 +434,19 @@ export async function removeTree(primaryRoot, name) {
|
|
|
204
434
|
join(treeRoot, '.fireforge-build.lock'),
|
|
205
435
|
join(treeRoot, '.fireforge', 'engine-session.lock'),
|
|
206
436
|
]) {
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(
|
|
437
|
+
const state = await inspectTreeLock(lockPath);
|
|
438
|
+
if (state.kind === 'held') {
|
|
439
|
+
throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(state.pid)}) holds ${lockPath}. ` +
|
|
210
440
|
'Wait for it to finish, then retry.');
|
|
211
441
|
}
|
|
442
|
+
if (state.kind === 'unknown' && options.force !== true) {
|
|
443
|
+
throw new GeneralError(`Refusing to remove tree "${name}": ${lockPath} exists but ${state.reason}, so it may be ` +
|
|
444
|
+
'held by a live process. Wait for any in-flight build or test to finish; if none is ' +
|
|
445
|
+
'running, re-run with --force to delete the tree and the stale lock.');
|
|
446
|
+
}
|
|
447
|
+
if (state.kind === 'unknown') {
|
|
448
|
+
warn(`Removing tree "${name}" through ${lockPath}, whose ownership is unknown (--force).`);
|
|
449
|
+
}
|
|
212
450
|
}
|
|
213
451
|
await rm(treeRoot, { recursive: true, force: true });
|
|
214
452
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* at `npm install typescript`.
|
|
20
20
|
*/
|
|
21
21
|
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
22
23
|
import { pathExists } from '../utils/fs.js';
|
|
23
24
|
import { verbose } from '../utils/logger.js';
|
|
24
25
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
@@ -113,7 +114,7 @@ export async function runTypecheck(projectRoot, cfg) {
|
|
|
113
114
|
catch (err) {
|
|
114
115
|
// A missing or unreadable shim fails only the project(s) that use it,
|
|
115
116
|
// not the whole run — projects with a different (or no) shim still run.
|
|
116
|
-
const message = err
|
|
117
|
+
const message = toError(err).message;
|
|
117
118
|
results.push({
|
|
118
119
|
project: projectPath,
|
|
119
120
|
issues: [
|
|
@@ -8,7 +8,7 @@ import { GeneralError } from '../errors/base.js';
|
|
|
8
8
|
import { BuildError } from '../errors/build.js';
|
|
9
9
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
10
10
|
import { escapeRegex } from '../utils/regex.js';
|
|
11
|
-
import { detectIndent, parseScript } from './ast-utils.js';
|
|
11
|
+
import { asEstree, detectIndent, parseScript } from './ast-utils.js';
|
|
12
12
|
import { withParserFallback } from './parser-fallback.js';
|
|
13
13
|
import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findMethodBody, findMethodBraceIndex, validateWireName, } from './wire-utils.js';
|
|
14
14
|
const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
@@ -36,7 +36,10 @@ export function addDestroyAST(content, expression, marker = DEFAULT_MARKER) {
|
|
|
36
36
|
throw new BuildError('Could not find onUnload/uninit method body via AST');
|
|
37
37
|
}
|
|
38
38
|
// Insert at top of method body (LIFO ordering)
|
|
39
|
-
|
|
39
|
+
// `noUncheckedIndexedAccess` makes this `Statement | undefined`; narrow
|
|
40
|
+
// before casting rather than laundering the undefined through the cast.
|
|
41
|
+
const firstRaw = body.body[0];
|
|
42
|
+
const firstStmt = firstRaw ? asEstree(firstRaw) : undefined;
|
|
40
43
|
let insertPos;
|
|
41
44
|
let indent;
|
|
42
45
|
if (firstStmt) {
|
|
@@ -114,7 +117,15 @@ export async function addDestroyToBrowserInit(engineDir, expression, marker = DE
|
|
|
114
117
|
if (destroyPattern.test(content)) {
|
|
115
118
|
return false;
|
|
116
119
|
}
|
|
117
|
-
const { value, usedFallback } = withParserFallback(() => addDestroyAST(content, expression, marker), () => legacyAddDestroy(content, expression, marker), BROWSER_INIT_JS
|
|
120
|
+
const { value, usedFallback } = withParserFallback(() => addDestroyAST(content, expression, marker), () => legacyAddDestroy(content, expression, marker), BROWSER_INIT_JS,
|
|
121
|
+
// Rethrow only the internal-invariant GeneralErrors ("Unexpected empty
|
|
122
|
+
// …array"): those are programming bugs, and retrying the legacy scanner
|
|
123
|
+
// cannot fix a broken invariant — it just buries the stack. Everything
|
|
124
|
+
// else still falls back, which is load-bearing: the AST path raises a raw
|
|
125
|
+
// acorn SyntaxError on chrome sources acorn cannot parse (preprocessor
|
|
126
|
+
// directives), and BuildError when the file's shape is unexpected. Both
|
|
127
|
+
// are exactly what the legacy scanner is here to handle.
|
|
128
|
+
(error) => error instanceof GeneralError);
|
|
118
129
|
if (usedFallback) {
|
|
119
130
|
assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
|
|
120
131
|
}
|
|
@@ -8,7 +8,7 @@ import { GeneralError } from '../errors/base.js';
|
|
|
8
8
|
import { BuildError } from '../errors/build.js';
|
|
9
9
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
10
10
|
import { escapeRegex } from '../utils/regex.js';
|
|
11
|
-
import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
|
|
11
|
+
import { asEstree, detectIndent, getNodeSource, parseScript, } from './ast-utils.js';
|
|
12
12
|
import { withParserFallback } from './parser-fallback.js';
|
|
13
13
|
import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
|
|
14
14
|
const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
@@ -18,6 +18,30 @@ const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
|
18
18
|
* fallback code paths agree on the shape.
|
|
19
19
|
*/
|
|
20
20
|
const DEFAULT_MARKER = 'FIREFORGE:';
|
|
21
|
+
/**
|
|
22
|
+
* Default insertion point: after the last consecutive fireforge block, or at
|
|
23
|
+
* the top of the method body when there is none.
|
|
24
|
+
*
|
|
25
|
+
* Both callers below — the `--after`-target-not-found fallthrough and the
|
|
26
|
+
* no-`--after` default — ran a character-for-character copy of this ladder,
|
|
27
|
+
* including both throw guards. `browser-init.js` is written through those two
|
|
28
|
+
* independent paths and they MUST emit identical blocks, so having one
|
|
29
|
+
* implementation enforces the invariant rather than merely tidying.
|
|
30
|
+
*/
|
|
31
|
+
function resolveDefaultInsertion(content, body, fireforgeBlocks) {
|
|
32
|
+
if (fireforgeBlocks.length > 0) {
|
|
33
|
+
const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
|
|
34
|
+
if (!lastBlock)
|
|
35
|
+
throw new GeneralError('Unexpected empty fireforgeBlocks array');
|
|
36
|
+
return { insertPos: lastBlock.end, indent: detectIndent(content, lastBlock.start) };
|
|
37
|
+
}
|
|
38
|
+
const firstStmt = body.body[0];
|
|
39
|
+
if (firstStmt) {
|
|
40
|
+
const insertPos = asEstree(firstStmt).start;
|
|
41
|
+
return { insertPos, indent: detectIndent(content, insertPos) };
|
|
42
|
+
}
|
|
43
|
+
return { insertPos: body.start + 1, indent: ' ' };
|
|
44
|
+
}
|
|
21
45
|
/**
|
|
22
46
|
* AST-based implementation: finds onLoad() method body, locates existing
|
|
23
47
|
* fireforge init blocks (TryStatements containing typeof guards), and inserts
|
|
@@ -47,7 +71,7 @@ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER)
|
|
|
47
71
|
const fireforgeBlocks = [];
|
|
48
72
|
for (const stmt of body.body) {
|
|
49
73
|
if (stmt.type === 'TryStatement') {
|
|
50
|
-
const tryNode = stmt;
|
|
74
|
+
const tryNode = asEstree(stmt);
|
|
51
75
|
const src = getNodeSource(content, tryNode);
|
|
52
76
|
if (/typeof\s+\w+\s*!==\s*"undefined"/.test(src)) {
|
|
53
77
|
fireforgeBlocks.push(tryNode);
|
|
@@ -68,47 +92,12 @@ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER)
|
|
|
68
92
|
}
|
|
69
93
|
else {
|
|
70
94
|
// --after target not found: fall through to default (after last fireforge block)
|
|
71
|
-
|
|
72
|
-
const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
|
|
73
|
-
if (!lastBlock)
|
|
74
|
-
throw new GeneralError('Unexpected empty fireforgeBlocks array');
|
|
75
|
-
insertPos = lastBlock.end;
|
|
76
|
-
indent = detectIndent(content, lastBlock.start);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
// No fireforge blocks, insert at top of method body
|
|
80
|
-
const firstStmt = body.body[0];
|
|
81
|
-
if (firstStmt) {
|
|
82
|
-
insertPos = firstStmt.start;
|
|
83
|
-
indent = detectIndent(content, insertPos);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
insertPos = body.start + 1;
|
|
87
|
-
indent = ' ';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
95
|
+
({ insertPos, indent } = resolveDefaultInsertion(content, body, fireforgeBlocks));
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
else {
|
|
93
99
|
// Default: insert after the last consecutive fireforge block at the start
|
|
94
|
-
|
|
95
|
-
const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
|
|
96
|
-
if (!lastBlock)
|
|
97
|
-
throw new GeneralError('Unexpected empty fireforgeBlocks array');
|
|
98
|
-
insertPos = lastBlock.end;
|
|
99
|
-
indent = detectIndent(content, lastBlock.start);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
const firstStmt = body.body[0];
|
|
103
|
-
if (firstStmt) {
|
|
104
|
-
insertPos = firstStmt.start;
|
|
105
|
-
indent = detectIndent(content, insertPos);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
insertPos = body.start + 1;
|
|
109
|
-
indent = ' ';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
100
|
+
({ insertPos, indent } = resolveDefaultInsertion(content, body, fireforgeBlocks));
|
|
112
101
|
}
|
|
113
102
|
const block = [
|
|
114
103
|
`${indent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
|
|
@@ -224,7 +213,15 @@ export async function addInitToBrowserInit(engineDir, expression, after, marker
|
|
|
224
213
|
if (initPattern.test(content)) {
|
|
225
214
|
return false;
|
|
226
215
|
}
|
|
227
|
-
const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS
|
|
216
|
+
const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS,
|
|
217
|
+
// Rethrow only the internal-invariant GeneralErrors ("Unexpected empty
|
|
218
|
+
// …array"): those are programming bugs, and retrying the legacy scanner
|
|
219
|
+
// cannot fix a broken invariant — it just buries the stack. Everything
|
|
220
|
+
// else still falls back, which is load-bearing: the AST path raises a raw
|
|
221
|
+
// acorn SyntaxError on chrome sources acorn cannot parse (preprocessor
|
|
222
|
+
// directives), and BuildError when the file's shape is unexpected. Both
|
|
223
|
+
// are exactly what the legacy scanner is here to handle.
|
|
224
|
+
(error) => error instanceof GeneralError);
|
|
228
225
|
if (usedFallback) {
|
|
229
226
|
assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
|
|
230
227
|
}
|