@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
|
@@ -3,20 +3,18 @@ import { readdir, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { confirm } from '@clack/prompts';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
6
|
-
import { removeCustomFtlJarMnEntry } from '../../core/furnace-apply-ftl.js';
|
|
7
|
-
import { extractComponentChecksums, getOverrideEngineTargetPath, isOverrideCopyCandidate, restoreOverrideFileToBaseline, } from '../../core/furnace-apply-helpers.js';
|
|
8
6
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
9
7
|
import { resolveFtlDir, xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
10
8
|
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
11
|
-
import { removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
12
9
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
13
10
|
import { isGitRepository } from '../../core/git.js';
|
|
14
11
|
import { deregisterTestManifest } from '../../core/manifest-register.js';
|
|
15
12
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
16
13
|
import { toError } from '../../utils/errors.js';
|
|
17
|
-
import { pathExists, readText, removeDir,
|
|
14
|
+
import { pathExists, readText, removeDir, writeText } from '../../utils/fs.js';
|
|
18
15
|
import { cancel, info, intro, isCancel, outro, warn } from '../../utils/logger.js';
|
|
19
|
-
import {
|
|
16
|
+
import { performCustomRemovalMutations, performOverrideRemovalMutations, } from './remove-mutations.js';
|
|
17
|
+
import { dropChecksumsByPrefix } from './remove-state.js';
|
|
20
18
|
/**
|
|
21
19
|
* Removes an entire TOML section (header + body lines) for a given test file.
|
|
22
20
|
* Matches from `["filename"]` up to the next section header `[` or end-of-file,
|
|
@@ -69,61 +67,6 @@ function findComponentType(config, name) {
|
|
|
69
67
|
return 'custom';
|
|
70
68
|
return undefined;
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Restores every override-deployed file in `engine/` to its pristine HEAD
|
|
74
|
-
* state, inverting what `applyOverrideComponent` would have written. Files that
|
|
75
|
-
* existed in HEAD are restored via `git restore`; files the override
|
|
76
|
-
* introduced (not in HEAD) are deleted outright.
|
|
77
|
-
*
|
|
78
|
-
* The restore set is the **union** of (a) files currently in the override
|
|
79
|
-
* workspace directory and (b) filenames recorded in `previousChecksumKeys`
|
|
80
|
-
* — i.e. files we know we deployed last time, even if the developer has
|
|
81
|
-
* since deleted them from the workspace. Without (b), a workspace deletion
|
|
82
|
-
* leaves an orphaned engine copy that `furnace remove` would never see.
|
|
83
|
-
*
|
|
84
|
-
* Every touched engine file is snapshotted into the rollback journal before
|
|
85
|
-
* mutation so a mid-remove failure still rolls the engine back to its
|
|
86
|
-
* pre-command state.
|
|
87
|
-
*/
|
|
88
|
-
async function restoreOverrideEngineFiles(engineDir, overrideDir, overrideConfig, previousChecksumKeys, ftlDir, journal) {
|
|
89
|
-
// Engine-as-git is a hard precondition for restoration: git HEAD is the only
|
|
90
|
-
// honest oracle for "what was there before the override". If the engine is
|
|
91
|
-
// not a git repo we refuse rather than silently leaving files behind — the
|
|
92
|
-
// previous warn-and-continue behaviour is exactly what this fix removes.
|
|
93
|
-
if (!(await isGitRepository(engineDir))) {
|
|
94
|
-
throw new FurnaceError('Cannot restore override files: engine is not a git repository. Run "fireforge download" to initialise it.');
|
|
95
|
-
}
|
|
96
|
-
// Build the union of "files we still see on disk" and "files state.json
|
|
97
|
-
// claims we deployed". The state set is the only authority for files that
|
|
98
|
-
// were deployed and later deleted from the workspace; the workspace set is
|
|
99
|
-
// the only authority for files added since last apply that have not yet
|
|
100
|
-
// been recorded in state. We need both.
|
|
101
|
-
const fileSet = new Set();
|
|
102
|
-
if (await pathExists(overrideDir)) {
|
|
103
|
-
const entries = await readdir(overrideDir, { withFileTypes: true });
|
|
104
|
-
for (const entry of entries) {
|
|
105
|
-
if (!entry.isFile())
|
|
106
|
-
continue;
|
|
107
|
-
if (!isOverrideCopyCandidate(entry.name, overrideConfig.type))
|
|
108
|
-
continue;
|
|
109
|
-
fileSet.add(entry.name);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (const key of previousChecksumKeys) {
|
|
113
|
-
fileSet.add(key);
|
|
114
|
-
}
|
|
115
|
-
let restored = 0;
|
|
116
|
-
let removed = 0;
|
|
117
|
-
for (const fileName of fileSet) {
|
|
118
|
-
const enginePath = getOverrideEngineTargetPath(engineDir, overrideConfig, fileName, ftlDir);
|
|
119
|
-
const action = await restoreOverrideFileToBaseline(engineDir, enginePath, journal);
|
|
120
|
-
if (action === 'restored')
|
|
121
|
-
restored += 1;
|
|
122
|
-
else if (action === 'removed')
|
|
123
|
-
removed += 1;
|
|
124
|
-
}
|
|
125
|
-
return { restored, removed };
|
|
126
|
-
}
|
|
127
70
|
/**
|
|
128
71
|
* Removes generated browser mochitest files associated with a custom component.
|
|
129
72
|
* @param name - Custom component tag name
|
|
@@ -410,76 +353,26 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
410
353
|
try {
|
|
411
354
|
const { config: freshConfig, ftlDir, state: freshState, type: freshType, } = await loadFreshRemoveTarget(projectRoot, name, paths.engine);
|
|
412
355
|
if (freshType === 'override') {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const { restored, removed } = await restoreOverrideEngineFiles(paths.engine, dir, overrideConfig, previousKeys, ftlDir, journal);
|
|
423
|
-
if (restored > 0) {
|
|
424
|
-
info(`Restored ${restored} file${restored === 1 ? '' : 's'} in engine/${overrideConfig.basePath} to Firefox baseline`);
|
|
425
|
-
}
|
|
426
|
-
if (removed > 0) {
|
|
427
|
-
info(`Removed ${removed} override-introduced file${removed === 1 ? '' : 's'} from engine/${overrideConfig.basePath}`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (await pathExists(dir)) {
|
|
431
|
-
await snapshotDir(journal, dir);
|
|
432
|
-
await removeDir(dir);
|
|
433
|
-
info(`Deleted components/overrides/${name}/`);
|
|
434
|
-
}
|
|
356
|
+
await performOverrideRemovalMutations({
|
|
357
|
+
name,
|
|
358
|
+
paths,
|
|
359
|
+
furnacePaths,
|
|
360
|
+
freshConfig,
|
|
361
|
+
freshState,
|
|
362
|
+
ftlDir,
|
|
363
|
+
journal,
|
|
364
|
+
});
|
|
435
365
|
}
|
|
436
366
|
else if (freshType === 'custom') {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
await snapshotFile(journal, join(paths.engine, 'toolkit/content/customElements.js'));
|
|
447
|
-
await removeCustomElementRegistration(paths.engine, name);
|
|
448
|
-
info(`Deregistered ${name} from customElements.js`);
|
|
449
|
-
}
|
|
450
|
-
// jar.mn is the only file removeJarMnEntries touches.
|
|
451
|
-
await snapshotFile(journal, join(paths.engine, 'toolkit/content/jar.mn'));
|
|
452
|
-
await removeJarMnEntries(paths.engine, name);
|
|
453
|
-
info(`Removed ${name} entries from toolkit/content/jar.mn`);
|
|
454
|
-
const dir = join(furnacePaths.customDir, name);
|
|
455
|
-
if (await pathExists(dir)) {
|
|
456
|
-
await snapshotDir(journal, dir);
|
|
457
|
-
await removeDir(dir);
|
|
458
|
-
info(`Deleted components/custom/${name}/`);
|
|
459
|
-
}
|
|
460
|
-
// Clean up deployed files in engine (per-file — see helper doc).
|
|
461
|
-
if (customConfig?.targetPath) {
|
|
462
|
-
await removeDeployedCustomFiles(projectRoot, paths.engine, name, customConfig.targetPath, journal);
|
|
463
|
-
}
|
|
464
|
-
// Localized components deploy a .ftl outside targetPath into the
|
|
465
|
-
// shared Fluent tree; apply writes it, so remove must delete it too
|
|
466
|
-
// or the locale payload is orphaned.
|
|
467
|
-
if (customConfig?.localized) {
|
|
468
|
-
const ftlRel = join(ftlDir, `${name}.ftl`);
|
|
469
|
-
const ftlPath = join(paths.engine, ftlRel);
|
|
470
|
-
if (await pathExists(ftlPath)) {
|
|
471
|
-
await snapshotFile(journal, ftlPath);
|
|
472
|
-
await removeFile(ftlPath);
|
|
473
|
-
info(`Deleted localized file engine/${ftlRel}`);
|
|
474
|
-
}
|
|
475
|
-
// Drop the locale jar.mn chrome registration that `applyCustomFtlFile`
|
|
476
|
-
// wrote during deploy — otherwise the engine is left with a
|
|
477
|
-
// `locale/.../${name}.ftl` entry pointing at a file we just
|
|
478
|
-
// deleted. 2026-04-21 eval (Finding #1): `furnace remove` left
|
|
479
|
-
// `browser/locales/jar.mn` referencing the missing FTL, which
|
|
480
|
-
// would break the next package-manifest validation.
|
|
481
|
-
await removeCustomFtlJarMnEntry(paths.engine, `${name}.ftl`, ftlDir, customConfig, journal);
|
|
482
|
-
}
|
|
367
|
+
await performCustomRemovalMutations({
|
|
368
|
+
projectRoot,
|
|
369
|
+
name,
|
|
370
|
+
paths,
|
|
371
|
+
furnacePaths,
|
|
372
|
+
freshConfig,
|
|
373
|
+
ftlDir,
|
|
374
|
+
journal,
|
|
375
|
+
});
|
|
483
376
|
}
|
|
484
377
|
const testCleanupFailures = freshType === 'custom' ? await cleanupAllCustomTestFiles(name, projectRoot, journal) : [];
|
|
485
378
|
// Remove entry from furnace.json
|
|
@@ -16,9 +16,58 @@ export function updateBrowserChromeTestContent(content, oldName, newName, binary
|
|
|
16
16
|
const newUnderscored = newName.replace(/-/g, '_');
|
|
17
17
|
const oldTestStem = deriveTestStem(oldName, binaryName);
|
|
18
18
|
const newTestStem = deriveTestStem(newName, binaryName);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
// ONE pass over the content, not four chained `.replace()` calls.
|
|
20
|
+
//
|
|
21
|
+
// Chaining let each rule see the previous rule's output. Renaming
|
|
22
|
+
// `acme-widget` to `acme-widget-v2` rewrote `test_acme_widget_defined` to
|
|
23
|
+
// `test_acme_widget_v2_defined` via the underscored form, and then the stem
|
|
24
|
+
// rule matched the `acme_widget` still inside that result and produced
|
|
25
|
+
// `test_acme_widget_v2_v2_defined`. A single alternation with a callback
|
|
26
|
+
// cannot re-enter its own output.
|
|
27
|
+
//
|
|
28
|
+
// Ordering is longest-first, because JS alternation takes the first
|
|
29
|
+
// alternative that matches at a given position.
|
|
30
|
+
const alternatives = [
|
|
31
|
+
// The stem is a short bare fragment (`moz-` and binary prefixes stripped,
|
|
32
|
+
// hyphens underscored) — `panel` for `moz-panel`. Matching it loose, even
|
|
33
|
+
// with hyphen guards, rewrote unrelated underscore-delimited identifiers
|
|
34
|
+
// such as `test_panel_group_integration`, because `_` must stay a legal
|
|
35
|
+
// neighbour for the generated name to match at all. The scaffold emits the
|
|
36
|
+
// stem in exactly one shape, so anchor to that shape instead of guessing
|
|
37
|
+
// from boundaries — with its own guards, so the anchored shape cannot
|
|
38
|
+
// match inside a longer identifier (`mytest_panel_defined_extra`).
|
|
39
|
+
{
|
|
40
|
+
pattern: `(?<![\\w-])test_${escapeRegex(oldTestStem)}_defined(?![\\w-])`,
|
|
41
|
+
to: `test_${newTestStem}_defined`,
|
|
42
|
+
},
|
|
43
|
+
// Word-boundary-aware, matching `furnace/rename.ts` and
|
|
44
|
+
// `rename-xpcshell.ts`: without the guards a rename of `moz-panel` rewrote
|
|
45
|
+
// `moz-panel-group`. `_` stays a legal neighbour for the underscored form
|
|
46
|
+
// because it is deliberately embedded in generated identifiers; `-` does
|
|
47
|
+
// not.
|
|
48
|
+
//
|
|
49
|
+
// Skipped when the underscored form IS the tag name (a hyphen-less
|
|
50
|
+
// component): both rules then match the same text and this one would win
|
|
51
|
+
// on order, substituting `newUnderscored` where the tag `newName` was
|
|
52
|
+
// meant. The stem rule still covers the one generated identifier shape.
|
|
53
|
+
...(oldUnderscored === oldName
|
|
54
|
+
? []
|
|
55
|
+
: [
|
|
56
|
+
{
|
|
57
|
+
pattern: `(?<![A-Za-z0-9-])${escapeRegex(oldUnderscored)}(?![A-Za-z0-9-])`,
|
|
58
|
+
to: newUnderscored,
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
{ pattern: `(?<![\\w-])${escapeRegex(oldName)}(?![\\w-])`, to: newName },
|
|
62
|
+
{ pattern: `\\b${escapeRegex(oldClassName)}\\b`, to: newClassName },
|
|
63
|
+
];
|
|
64
|
+
// Each alternative is its own capture group and the replacement is picked by
|
|
65
|
+
// the group that matched — not by matched TEXT, which is ambiguous whenever
|
|
66
|
+
// two rules can produce the same match with different substitutions.
|
|
67
|
+
const combined = new RegExp(alternatives.map(({ pattern }) => `(${pattern})`).join('|'), 'g');
|
|
68
|
+
return content.replace(combined, (matched, ...args) => {
|
|
69
|
+
const groups = args.slice(0, alternatives.length);
|
|
70
|
+
const index = groups.findIndex((group) => group !== undefined);
|
|
71
|
+
return alternatives[index]?.to ?? matched;
|
|
72
|
+
});
|
|
24
73
|
}
|
|
@@ -28,17 +28,10 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
28
28
|
}
|
|
29
29
|
const selected = await multiselect({
|
|
30
30
|
message: 'Select components to add as stock',
|
|
31
|
-
options: untrackedComponents.map((c) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (c.hasFTL)
|
|
36
|
-
features.push('FTL');
|
|
37
|
-
if (c.isRegistered)
|
|
38
|
-
features.push('registered');
|
|
39
|
-
const label = features.length > 0 ? `${c.tagName} — ${features.join(', ')}` : c.tagName;
|
|
40
|
-
return { value: c.tagName, label };
|
|
41
|
-
}),
|
|
31
|
+
options: untrackedComponents.map((c) => ({
|
|
32
|
+
value: c.tagName,
|
|
33
|
+
label: `${c.tagName}${formatComponentFeatures(c)}`,
|
|
34
|
+
})),
|
|
42
35
|
});
|
|
43
36
|
if (isCancel(selected)) {
|
|
44
37
|
cancel('Cancelled');
|
|
@@ -110,6 +103,22 @@ async function persistStockComponents(projectRoot, names) {
|
|
|
110
103
|
}
|
|
111
104
|
});
|
|
112
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Renders a component's discovered capabilities as a display suffix.
|
|
108
|
+
* Written twice before 0.41.0 — once for the interactive multiselect labels
|
|
109
|
+
* and once for the report rows — which is part of what pinned
|
|
110
|
+
* `furnaceScanCommand` at complexity 30/30.
|
|
111
|
+
*/
|
|
112
|
+
function formatComponentFeatures(component) {
|
|
113
|
+
const features = [];
|
|
114
|
+
if (component.hasCSS)
|
|
115
|
+
features.push('CSS');
|
|
116
|
+
if (component.hasFTL)
|
|
117
|
+
features.push('FTL');
|
|
118
|
+
if (component.isRegistered)
|
|
119
|
+
features.push('registered');
|
|
120
|
+
return features.length > 0 ? ` — ${features.join(', ')}` : '';
|
|
121
|
+
}
|
|
113
122
|
/**
|
|
114
123
|
* Runs the furnace scan command to discover MozLitElement components.
|
|
115
124
|
* @param projectRoot - Root directory of the project
|
|
@@ -153,33 +162,15 @@ export async function furnaceScanCommand(projectRoot, options = {}) {
|
|
|
153
162
|
tracked.set(name, 'custom');
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
//
|
|
165
|
+
// Partition once and reuse: the tracked/untracked split was computed twice
|
|
166
|
+
// (imperatively here, declaratively again for --track) before 0.41.0.
|
|
167
|
+
const untrackedComponentList = components.filter((c) => !tracked.has(c.tagName));
|
|
168
|
+
const untrackedCount = untrackedComponentList.length;
|
|
169
|
+
const trackedCount = components.length - untrackedCount;
|
|
157
170
|
for (const component of components) {
|
|
158
|
-
const features = [];
|
|
159
|
-
if (component.hasCSS)
|
|
160
|
-
features.push('CSS');
|
|
161
|
-
if (component.hasFTL)
|
|
162
|
-
features.push('FTL');
|
|
163
|
-
if (component.isRegistered)
|
|
164
|
-
features.push('registered');
|
|
165
|
-
let line = component.tagName;
|
|
166
|
-
if (features.length > 0) {
|
|
167
|
-
line += ` — ${features.join(', ')}`;
|
|
168
|
-
}
|
|
169
171
|
const type = tracked.get(component.tagName);
|
|
170
|
-
|
|
171
|
-
line += ` [${type}]`;
|
|
172
|
-
}
|
|
173
|
-
info(line);
|
|
174
|
-
}
|
|
175
|
-
// Summary
|
|
176
|
-
let trackedCount = 0;
|
|
177
|
-
for (const component of components) {
|
|
178
|
-
if (tracked.has(component.tagName)) {
|
|
179
|
-
trackedCount++;
|
|
180
|
-
}
|
|
172
|
+
info(`${component.tagName}${formatComponentFeatures(component)}${type ? ` [${type}]` : ''}`);
|
|
181
173
|
}
|
|
182
|
-
const untrackedCount = components.length - trackedCount;
|
|
183
174
|
note(`Total: ${components.length} Tracked: ${trackedCount} Untracked: ${untrackedCount}`, 'Summary');
|
|
184
175
|
// --track: persist the discovered untracked inventory into the `stock`
|
|
185
176
|
// section without prompting (works non-interactively). Without it, scan
|
|
@@ -191,7 +182,7 @@ export async function furnaceScanCommand(projectRoot, options = {}) {
|
|
|
191
182
|
outro('Scan complete');
|
|
192
183
|
return;
|
|
193
184
|
}
|
|
194
|
-
const untrackedNames =
|
|
185
|
+
const untrackedNames = untrackedComponentList.map((c) => c.tagName);
|
|
195
186
|
await persistStockComponents(projectRoot, untrackedNames);
|
|
196
187
|
success(`Tracked ${untrackedNames.length} component${untrackedNames.length === 1 ? '' : 's'} in the stock section of furnace.json`);
|
|
197
188
|
outro('Scan complete');
|
|
@@ -157,42 +157,44 @@ export async function furnaceStatusCommand(projectRoot, name) {
|
|
|
157
157
|
// hints, so report them separately rather than collapsing into a single
|
|
158
158
|
// "something is off" message.
|
|
159
159
|
if (await pathExists(paths.engine)) {
|
|
160
|
-
let
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for (const [
|
|
178
|
-
|
|
160
|
+
// Held in an object rather than two `let`s: the scanner below mutates
|
|
161
|
+
// them from inside a closure, and TS's control-flow analysis does not
|
|
162
|
+
// propagate that through the call — it would narrow both to the literal
|
|
163
|
+
// `false` and flag every later read as always-falsy.
|
|
164
|
+
const drift = { workspaceChanged: false, engineDrifted: false };
|
|
165
|
+
/**
|
|
166
|
+
* Scans one component family for workspace or engine drift.
|
|
167
|
+
*
|
|
168
|
+
* The engine probe sits behind `else if` deliberately: a component whose
|
|
169
|
+
* workspace already changed does not need an engine comparison to decide
|
|
170
|
+
* what `status` reports, and the probe is the expensive half (it re-hashes
|
|
171
|
+
* deployed files). The consequence is that engine drift can go UNREPORTED
|
|
172
|
+
* for a component that also has workspace changes — the summary says
|
|
173
|
+
* "workspace changed", not both. Preserved verbatim from the two
|
|
174
|
+
* hand-written copies this replaced.
|
|
175
|
+
*/
|
|
176
|
+
const scanFamily = async (entries, baseDir, kind, probeEngineDrift) => {
|
|
177
|
+
for (const [componentName, componentConfig] of entries) {
|
|
178
|
+
if (drift.workspaceChanged && drift.engineDrifted)
|
|
179
|
+
return;
|
|
180
|
+
const componentDir = join(baseDir, componentName);
|
|
179
181
|
if (!(await pathExists(componentDir)))
|
|
180
182
|
continue;
|
|
181
|
-
const previous = extractComponentChecksums(state.appliedChecksums,
|
|
183
|
+
const previous = extractComponentChecksums(state.appliedChecksums, kind, componentName);
|
|
182
184
|
if (await hasComponentChanged(componentDir, previous)) {
|
|
183
|
-
workspaceChanged = true;
|
|
185
|
+
drift.workspaceChanged = true;
|
|
184
186
|
}
|
|
185
|
-
else if (await
|
|
186
|
-
engineDrifted = true;
|
|
187
|
+
else if (await probeEngineDrift(componentName, componentDir, componentConfig)) {
|
|
188
|
+
drift.engineDrifted = true;
|
|
187
189
|
}
|
|
188
|
-
if (workspaceChanged && engineDrifted)
|
|
189
|
-
break;
|
|
190
190
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
191
|
+
};
|
|
192
|
+
await scanFamily(Object.entries(config.overrides), furnacePaths.overridesDir, 'override', (_name, componentDir, cfg) => hasOverrideEngineDrift(paths.engine, componentDir, cfg, ftlDir));
|
|
193
|
+
await scanFamily(Object.entries(config.custom), furnacePaths.customDir, 'custom', (name, componentDir, cfg) => hasCustomEngineDrift(projectRoot, name, componentDir, cfg, ftlDir));
|
|
194
|
+
if (drift.workspaceChanged) {
|
|
193
195
|
warn('Components have been modified since last apply. Run `fireforge build` or `fireforge furnace apply`.');
|
|
194
196
|
}
|
|
195
|
-
if (engineDrifted) {
|
|
197
|
+
if (drift.engineDrifted) {
|
|
196
198
|
warn('Engine drift detected since last apply (reset/download/manual edit). Run `fireforge furnace apply` to re-deploy.');
|
|
197
199
|
}
|
|
198
200
|
}
|
|
@@ -6,6 +6,7 @@ import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, } from '../../
|
|
|
6
6
|
import { addCustomElementRegistration, addJarMnEntries, pruneStaleJarMnEntries, } from '../../core/furnace-registration.js';
|
|
7
7
|
import { validateAllComponents, validateComponent } from '../../core/furnace-validate.js';
|
|
8
8
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
9
|
+
import { toError } from '../../utils/errors.js';
|
|
9
10
|
import { pathExists } from '../../utils/fs.js';
|
|
10
11
|
import { info, intro, note, outro, success, warn } from '../../utils/logger.js';
|
|
11
12
|
import { displayValidationIssues } from './validation-output.js';
|
|
@@ -15,6 +16,7 @@ const FIXABLE_CHECKS = new Set([
|
|
|
15
16
|
'missing-jar-mn-css',
|
|
16
17
|
'wrong-registration-pattern',
|
|
17
18
|
'stale-jar-registration',
|
|
19
|
+
'missing-custom-element-registration',
|
|
18
20
|
]);
|
|
19
21
|
/**
|
|
20
22
|
* Auto-fixes registration issues that have deterministic solutions.
|
|
@@ -53,7 +55,7 @@ async function autoFixIssues(projectRoot, issues) {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
catch (err) {
|
|
56
|
-
warn(`Could not prune stale jar.mn lines: ${err
|
|
58
|
+
warn(`Could not prune stale jar.mn lines: ${toError(err).message}`);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
// Fix jar.mn entries
|
|
@@ -72,21 +74,26 @@ async function autoFixIssues(projectRoot, issues) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
catch (err) {
|
|
75
|
-
warn(`Could not fix jar.mn for ${componentName}: ${err
|
|
77
|
+
warn(`Could not fix jar.mn for ${componentName}: ${toError(err).message}`);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
// `wrong-registration-pattern` is deliberately NOT auto-fixed: it means the
|
|
81
|
+
// component IS registered but in the wrong block, and moving code between
|
|
82
|
+
// blocks is too risky to do unattended. Only truly missing registrations are
|
|
83
|
+
// repaired, below.
|
|
84
|
+
// Repair components missing from customElements.js entirely (reported by
|
|
85
|
+
// validate as `missing-custom-element-registration`).
|
|
86
|
+
//
|
|
87
|
+
// Scoped to the components named in `issues`. Until 0.41.0 this iterated
|
|
88
|
+
// every entry in `config.custom`, so `furnace validate <one-component> --fix`
|
|
89
|
+
// wrote customElements.js registrations for EVERY custom component — outside
|
|
90
|
+
// the issue list it was handed, and invisibly, since `fixed` was never
|
|
91
|
+
// incremented here. The block's own comment already claimed this scoping;
|
|
92
|
+
// the code did not implement it.
|
|
93
|
+
const componentsWithIssues = new Set(issues.map((issue) => issue.component));
|
|
94
|
+
for (const componentName of componentsWithIssues) {
|
|
95
|
+
const customConfig = config.custom[componentName];
|
|
96
|
+
if (!customConfig?.register)
|
|
90
97
|
continue;
|
|
91
98
|
const componentDir = join(furnacePaths.customDir, componentName);
|
|
92
99
|
if (!(await pathExists(componentDir)))
|
|
@@ -97,13 +104,13 @@ async function autoFixIssues(projectRoot, issues) {
|
|
|
97
104
|
continue;
|
|
98
105
|
const modulePath = `chrome://global/content/elements/${componentName}.mjs`;
|
|
99
106
|
try {
|
|
107
|
+
// Idempotent: returns without error when already registered, so this
|
|
108
|
+
// stays a no-op for components whose registration is already correct.
|
|
100
109
|
await addCustomElementRegistration(engineDir, componentName, modulePath);
|
|
101
|
-
// addCustomElementRegistration is idempotent — it returns without error
|
|
102
|
-
// if already registered. We only count it as fixed if a matching issue
|
|
103
|
-
// existed in the input.
|
|
104
110
|
}
|
|
105
111
|
catch {
|
|
106
|
-
//
|
|
112
|
+
// Best-effort repair — the re-validation pass at the call site reports
|
|
113
|
+
// whatever is still outstanding.
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
return fixed;
|
|
@@ -5,12 +5,13 @@ import { Command } from 'commander';
|
|
|
5
5
|
import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
|
|
6
6
|
import { getHead } from '../core/git.js';
|
|
7
7
|
import { getDirtyFiles } from '../core/git-status.js';
|
|
8
|
-
import { applyPatchesWithContinue, countPatches,
|
|
8
|
+
import { applyPatchesWithContinue, countPatches, createPatchedContentContext, discoverPatches, extractAffectedFiles, matchesUntilFilename, PatchError, } from '../core/patch-apply.js';
|
|
9
9
|
import { checkVersionCompatibility, loadPatchesManifest, validatePatchesManifestConsistency, validatePatchIntegrity, } from '../core/patch-manifest.js';
|
|
10
10
|
import { getPatchSourceVersion } from '../core/patch-source-metadata.js';
|
|
11
11
|
import { warnIfStaticComponentsStale } from '../core/test-stale-check.js';
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
|
-
import {
|
|
13
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
14
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
14
15
|
import { pathExists, readText } from '../utils/fs.js';
|
|
15
16
|
import { error, info, intro, isCancel, outro, spinner, success, verbose, warn, } from '../utils/logger.js';
|
|
16
17
|
import { pickDefined } from '../utils/options.js';
|
|
@@ -24,8 +25,8 @@ import { pickDefined } from '../utils/options.js';
|
|
|
24
25
|
*/
|
|
25
26
|
const SAFE_IO_FALLBACK_CODES = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'EBUSY']);
|
|
26
27
|
function isSafeIoFallback(error) {
|
|
27
|
-
const code = error
|
|
28
|
-
return
|
|
28
|
+
const code = getNodeErrorCode(error);
|
|
29
|
+
return code !== undefined && SAFE_IO_FALLBACK_CODES.has(code);
|
|
29
30
|
}
|
|
30
31
|
/** Concurrency bound for per-file classification (each call spawns git). */
|
|
31
32
|
const UNMANAGED_CLASSIFY_CONCURRENCY = 8;
|
|
@@ -33,7 +34,7 @@ async function getUnmanagedDirtyFiles(engineDir, patchesDir, dirtyFiles) {
|
|
|
33
34
|
// One manifest+patch-discovery load for the whole batch (the per-call
|
|
34
35
|
// computePatchedContent re-read everything for every file), and bounded
|
|
35
36
|
// concurrency instead of an unbounded Promise.all over git spawns.
|
|
36
|
-
const computeExpected = await
|
|
37
|
+
const { computePatched: computeExpected } = await createPatchedContentContext(patchesDir, engineDir);
|
|
37
38
|
const classifications = await mapWithConcurrency(dirtyFiles, UNMANAGED_CLASSIFY_CONCURRENCY, async (file) => {
|
|
38
39
|
try {
|
|
39
40
|
const [expected, exists] = await Promise.all([
|
|
@@ -62,21 +63,6 @@ async function getUnmanagedDirtyFiles(engineDir, patchesDir, dirtyFiles) {
|
|
|
62
63
|
});
|
|
63
64
|
return classifications.filter((file) => file !== null).sort();
|
|
64
65
|
}
|
|
65
|
-
/** Maps items with at most `limit` in-flight promises. Preserves order. */
|
|
66
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
67
|
-
const results = new Array(items.length);
|
|
68
|
-
let next = 0;
|
|
69
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
70
|
-
for (;;) {
|
|
71
|
-
const index = next++;
|
|
72
|
-
if (index >= items.length)
|
|
73
|
-
return;
|
|
74
|
-
results[index] = await fn(items[index]);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
await Promise.all(workers);
|
|
78
|
-
return results;
|
|
79
|
-
}
|
|
80
66
|
function reportForcedOverwriteRisk(unmanagedDirtyFiles) {
|
|
81
67
|
warn(`--force will overwrite ${unmanagedDirtyFiles.length} unmanaged change${unmanagedDirtyFiles.length === 1 ? '' : 's'} in patch-touched file${unmanagedDirtyFiles.length === 1 ? '' : 's'}:`);
|
|
82
68
|
for (const file of unmanagedDirtyFiles) {
|