@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Coverage replaces by default for a reason documented in
|
|
8
|
-
* `build-baseline-types.ts`: every baseline write refreshes
|
|
9
|
-
* `packageableFingerprints` for all dirty packageable paths, so a blind
|
|
10
|
-
* union would whitewash an earlier scope's edited fixtures while
|
|
11
|
-
* `obj-*`/`_tests/` still holds that scope's stale staging. Extending is
|
|
12
|
-
* honest only while everything the previous record vouched for is still
|
|
13
|
-
* true, which is what {@link checkExtendCoverageAnchor} and
|
|
14
|
-
* {@link checkExtendMozconfigAnchor} verify:
|
|
15
|
-
*
|
|
16
|
-
* 1. engine HEAD is unchanged, so every committed edit is excluded.
|
|
17
|
-
* 2. every path the previous baseline fingerprinted still hashes to the
|
|
18
|
-
* recorded digest, so the earlier scope's staging inputs are
|
|
19
|
-
* byte-identical. Files that became dirty since that build are fine:
|
|
20
|
-
* the current whole-tree `mach build faster` repackages `dist/` and
|
|
21
|
-
* vouches for them.
|
|
22
|
-
* 3. the generated `engine/mozconfig` is unchanged. Engine HEAD does not
|
|
23
|
-
* cover this: the mozconfig is regenerated from project-side
|
|
24
|
-
* `configs/*.mozconfig` templates plus `fireforge.json` on every build,
|
|
25
|
-
* so two builds at the same engine SHA can configure differently.
|
|
26
|
-
*
|
|
27
|
-
* Every failure refuses fail-closed. The operator's remedy is always a plain
|
|
28
|
-
* scoped build, which resets the claim to the paths it actually vouches for.
|
|
29
|
-
*
|
|
30
|
-
* Known boundary, documented rather than closed: dirty non-packageable
|
|
31
|
-
* fixtures under previously covered paths (plain test files, manifests
|
|
32
|
-
* `isPackageablePath` rejects) are invisible to check 2. Editing one
|
|
33
|
-
* uncommitted between two builds leaves the earlier scope's `_tests/`
|
|
34
|
-
* staging stale while extend still vouches for it. Plain replacement has no such
|
|
35
|
-
* window, because the earlier scope simply stops being covered.
|
|
3
|
+
* Retains prior test staging only when HEAD, mozconfig and every recorded
|
|
4
|
+
* dirty input outside the newly rebuilt scope are unchanged. Complete
|
|
5
|
+
* input fingerprints are required: packageable-only legacy records cannot
|
|
6
|
+
* prove that a test fixture or manifest still matches its staged copy.
|
|
36
7
|
*/
|
|
37
8
|
import { readFile } from 'node:fs/promises';
|
|
38
9
|
import { join } from 'node:path';
|
|
@@ -41,6 +12,8 @@ import { sha256Hex } from '../utils/hash.js';
|
|
|
41
12
|
import { verbose } from '../utils/logger.js';
|
|
42
13
|
import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
|
|
43
14
|
import { getHead, isMissingHeadError } from './git.js';
|
|
15
|
+
import { git } from './git-base.js';
|
|
16
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
44
17
|
/** Filename of the generated mozconfig, relative to the engine directory. */
|
|
45
18
|
const MOZCONFIG_FILENAME = 'mozconfig';
|
|
46
19
|
/** How many diverged paths a refusal names before truncating. */
|
|
@@ -52,14 +25,15 @@ const MAX_DIVERGED_REPORTED = 5;
|
|
|
52
25
|
* @param engineDir - Path to the engine directory
|
|
53
26
|
* @param previousBaseline - Baseline recorded by the previous build
|
|
54
27
|
*/
|
|
55
|
-
export async function checkExtendCoverageAnchor(engineDir, previousBaseline) {
|
|
28
|
+
export async function checkExtendCoverageAnchor(engineDir, previousBaseline, rebuiltPaths = []) {
|
|
56
29
|
if (previousBaseline === undefined) {
|
|
57
30
|
return { ok: false, reason: 'no-baseline', detail: [] };
|
|
58
31
|
}
|
|
59
32
|
if (previousBaseline.mozconfigHash === undefined) {
|
|
60
33
|
return { ok: false, reason: 'no-mozconfig-hash', detail: [] };
|
|
61
34
|
}
|
|
62
|
-
const recordedFingerprints = previousBaseline.
|
|
35
|
+
const recordedFingerprints = previousBaseline.testInputFingerprints;
|
|
36
|
+
const rebuilt = (file) => rebuiltPaths.some((path) => file === path || file.startsWith(path.replace(/\/$/, '') + '/'));
|
|
63
37
|
if (recordedFingerprints === undefined) {
|
|
64
38
|
return { ok: false, reason: 'no-fingerprints', detail: [] };
|
|
65
39
|
}
|
|
@@ -77,10 +51,25 @@ export async function checkExtendCoverageAnchor(engineDir, previousBaseline) {
|
|
|
77
51
|
}
|
|
78
52
|
const diverged = [];
|
|
79
53
|
for (const [relPath, recordedHash] of Object.entries(recordedFingerprints)) {
|
|
54
|
+
if (rebuilt(relPath))
|
|
55
|
+
continue;
|
|
80
56
|
const liveHash = await hashEngineFile(engineDir, relPath);
|
|
81
57
|
if (liveHash !== recordedHash)
|
|
82
58
|
diverged.push(relPath);
|
|
83
59
|
}
|
|
60
|
+
// New dirty test inputs are absent from the previous fingerprint map.
|
|
61
|
+
// They invalidate retained staging too; explicitly rebuilt inputs are safe.
|
|
62
|
+
if (previousBaseline.testInputFingerprints !== undefined) {
|
|
63
|
+
const changed = (await git(['diff', '--name-only', 'HEAD'], engineDir)).split('\n');
|
|
64
|
+
changed.push(...(await getUntrackedFiles(engineDir)));
|
|
65
|
+
for (const file of changed.filter(Boolean)) {
|
|
66
|
+
if (file === MOZCONFIG_FILENAME || rebuilt(file) || Object.hasOwn(recordedFingerprints, file))
|
|
67
|
+
continue;
|
|
68
|
+
// A retained manifest may stage a shared fixture outside test-named
|
|
69
|
+
// directories. Without its full dependency graph, refuse conservatively.
|
|
70
|
+
diverged.push(file);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
84
73
|
if (diverged.length > 0) {
|
|
85
74
|
return { ok: false, reason: 'fingerprint-diverged', detail: diverged.sort() };
|
|
86
75
|
}
|
|
@@ -136,7 +125,7 @@ export function formatExtendCoverageRefusal(result) {
|
|
|
136
125
|
return ('--extend-coverage refused: the recorded baseline predates the mozconfig anchor, so it ' +
|
|
137
126
|
`cannot be proven that this build configures identically. ${remedy}`);
|
|
138
127
|
case 'no-fingerprints':
|
|
139
|
-
return ('--extend-coverage refused: the recorded baseline carries no
|
|
128
|
+
return ('--extend-coverage refused: the recorded baseline carries no complete test-input fingerprints, so ' +
|
|
140
129
|
`the earlier scope's staging inputs cannot be verified unchanged. ${remedy}`);
|
|
141
130
|
case 'head-moved':
|
|
142
131
|
return ('--extend-coverage refused: engine HEAD moved since the recorded build ' +
|
|
@@ -144,7 +133,7 @@ export function formatExtendCoverageRefusal(result) {
|
|
|
144
133
|
`longer anchored. ${remedy}`);
|
|
145
134
|
case 'fingerprint-diverged':
|
|
146
135
|
return ('--extend-coverage refused: ' +
|
|
147
|
-
`${result.detail.length}
|
|
136
|
+
`${result.detail.length} staging input(s) the recorded build staged have changed ` +
|
|
148
137
|
`since (${formatDivergedPaths(result.detail)}). Extending would vouch for the earlier scope ` +
|
|
149
138
|
`while obj-*/_tests/ still holds its stale staging. ${remedy}`);
|
|
150
139
|
case 'mozconfig-changed':
|
|
@@ -53,11 +53,12 @@ export async function planDiscardBaselines(patchesDir, engineDir, entries) {
|
|
|
53
53
|
if (claimedEntries.length === 0) {
|
|
54
54
|
return planUpstreamDiscards(entries);
|
|
55
55
|
}
|
|
56
|
-
const { computePatched } = await createPatchedContentContext(patchesDir, engineDir);
|
|
56
|
+
const { computePatched, prefetchBase } = await createPatchedContentContext(patchesDir, engineDir);
|
|
57
57
|
const claimedFiles = [
|
|
58
58
|
...new Set(claimedEntries.flatMap((entry) => entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file])),
|
|
59
59
|
];
|
|
60
60
|
const trackedInHead = await listTrackedInHead(engineDir, claimedFiles);
|
|
61
|
+
await prefetchBase(claimedFiles);
|
|
61
62
|
const computeExpected = async (path) => {
|
|
62
63
|
try {
|
|
63
64
|
return await computePatched(path);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FurnaceConfig } from '../types/furnace.js';
|
|
2
|
+
/** Checks all component destinations before an apply starts writing. */
|
|
3
|
+
export declare function assertComponentWriteBoundaries(engineDir: string, config: FurnaceConfig, ftlDir: string): Promise<void>;
|
|
4
|
+
/** Resolves the nearest existing parent before any mkdir/copy can write through an alias. */
|
|
5
|
+
export declare function assertEngineWriteBoundary(engineDir: string, directory: string): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
5
|
+
import { getNodeErrorCode } from '../utils/errors.js';
|
|
6
|
+
import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
7
|
+
/** Checks all component destinations before an apply starts writing. */
|
|
8
|
+
export async function assertComponentWriteBoundaries(engineDir, config, ftlDir) {
|
|
9
|
+
const destinations = new Set([
|
|
10
|
+
ftlDir,
|
|
11
|
+
dirname(JAR_MN),
|
|
12
|
+
dirname(CUSTOM_ELEMENTS_JS),
|
|
13
|
+
...Object.values(config.custom).map((entry) => entry.targetPath),
|
|
14
|
+
...Object.values(config.overrides).map((entry) => entry.basePath),
|
|
15
|
+
]);
|
|
16
|
+
for (const destination of destinations) {
|
|
17
|
+
await assertEngineWriteBoundary(engineDir, join(engineDir, destination));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Resolves the nearest existing parent before any mkdir/copy can write through an alias. */
|
|
21
|
+
export async function assertEngineWriteBoundary(engineDir, directory) {
|
|
22
|
+
const root = await realpath(engineDir);
|
|
23
|
+
let parent = resolve(directory);
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
const canonical = await realpath(parent);
|
|
27
|
+
if (canonical !== root && !canonical.startsWith(root + sep)) {
|
|
28
|
+
throw new FurnaceError(`Engine destination ${directory} resolves outside engine/ (${canonical}). Refusing to write through into component sources or another checkout.`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (getNodeErrorCode(error) !== 'ENOENT')
|
|
34
|
+
throw error;
|
|
35
|
+
const next = dirname(parent);
|
|
36
|
+
if (next === parent)
|
|
37
|
+
throw error;
|
|
38
|
+
parent = next;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, queue-level entry point to the `forward-registration` census.
|
|
3
|
+
*
|
|
4
|
+
* A consumer that keeps its own audit (a ratchet baseline over forward
|
|
5
|
+
* edges, say) runs the rule's own predicate here instead of mirroring it.
|
|
6
|
+
* Lives apart from the rule module because it builds the queue context,
|
|
7
|
+
* which imports the rule back.
|
|
8
|
+
*/
|
|
9
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
10
|
+
import type { ForwardRegistration, ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
11
|
+
/** Options for {@link findForwardRegistrations}. */
|
|
12
|
+
export interface FindForwardRegistrationsOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Which carriers to check. Defaults to `'extended'` (every carrier the
|
|
15
|
+
* rule can resolve), since a census wants the whole picture whatever the
|
|
16
|
+
* lint gate is configured to enforce.
|
|
17
|
+
*/
|
|
18
|
+
scope?: ForwardRegistrationScope;
|
|
19
|
+
/** Project config, for the queue's patchPolicy ordering context. Optional. */
|
|
20
|
+
config?: FireForgeConfig;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lists every undeclared forward registration in a patch queue: a line a
|
|
24
|
+
* patch adds (to a test manifest, jar.mn, moz.build or customElements.js)
|
|
25
|
+
* that names a file only a later-ordered patch creates. Each record carries
|
|
26
|
+
* the paste-and-run `patch staged-dependency` command that declares it.
|
|
27
|
+
*
|
|
28
|
+
* @param patchesDir - The project's `patches/` directory
|
|
29
|
+
* @param options - Scope and optional config
|
|
30
|
+
* @returns One record per (patch, carrier file, created file)
|
|
31
|
+
*/
|
|
32
|
+
export declare function findForwardRegistrations(patchesDir: string, options?: FindForwardRegistrationsOptions): Promise<ForwardRegistration[]>;
|
|
33
|
+
export type { ForwardRegistration, ForwardRegistrationKind, ForwardRegistrationScope, } from './patch-lint-forward-registration-extended.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildPatchQueueContext } from './patch-lint-cross.js';
|
|
2
|
+
import { collectForwardRegistrations } from './patch-lint-forward-registration.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lists every undeclared forward registration in a patch queue: a line a
|
|
5
|
+
* patch adds (to a test manifest, jar.mn, moz.build or customElements.js)
|
|
6
|
+
* that names a file only a later-ordered patch creates. Each record carries
|
|
7
|
+
* the paste-and-run `patch staged-dependency` command that declares it.
|
|
8
|
+
*
|
|
9
|
+
* @param patchesDir - The project's `patches/` directory
|
|
10
|
+
* @param options - Scope and optional config
|
|
11
|
+
* @returns One record per (patch, carrier file, created file)
|
|
12
|
+
*/
|
|
13
|
+
export async function findForwardRegistrations(patchesDir, options = {}) {
|
|
14
|
+
const ctx = await buildPatchQueueContext(patchesDir, options.config);
|
|
15
|
+
return collectForwardRegistrations(ctx, options.scope ?? 'extended');
|
|
16
|
+
}
|
|
@@ -323,13 +323,16 @@ export async function applyCustomComponent(ctx, config, dryRun = false, rollback
|
|
|
323
323
|
// fragment-expanded form. The workspace source keeps only the directive,
|
|
324
324
|
// so shared CSS stays single-sourced.
|
|
325
325
|
const sharedDir = join(componentDir, '..', '..', SHARED_FRAGMENTS_DIR);
|
|
326
|
-
await Promise.
|
|
326
|
+
const copies = await Promise.allSettled(filesToCopy.map(async (entry) => {
|
|
327
327
|
const src = join(componentDir, entry.name);
|
|
328
328
|
const dest = join(targetDir, entry.name);
|
|
329
329
|
await deployFileWithFragments(src, dest, sharedDir);
|
|
330
330
|
affectedPaths.push(normalizePathSlashes(relative(engineDir, dest)));
|
|
331
331
|
copiedFileNames.push(entry.name);
|
|
332
332
|
}));
|
|
333
|
+
const failedCopy = copies.find((copy) => copy.status === 'rejected');
|
|
334
|
+
if (failedCopy?.status === 'rejected')
|
|
335
|
+
throw failedCopy.reason;
|
|
333
336
|
// See buildCustomDryRunActions for the rationale: when `sharedFtl` is set
|
|
334
337
|
// the shared bundle is owned elsewhere and FireForge must not copy or
|
|
335
338
|
// register a per-component `.ftl` on its behalf.
|
|
@@ -416,12 +419,15 @@ export async function applyOverrideComponent(ctx, config, dryRun = false, rollba
|
|
|
416
419
|
}
|
|
417
420
|
}
|
|
418
421
|
// Copy phase (parallel)
|
|
419
|
-
await Promise.
|
|
422
|
+
const copies = await Promise.allSettled(candidateEntries.map(async (entry) => {
|
|
420
423
|
const src = join(componentDir, entry.name);
|
|
421
424
|
const dest = getOverrideEngineTargetPath(engineDir, config, entry.name, ftlDir);
|
|
422
425
|
await copyFile(src, dest);
|
|
423
426
|
affectedPaths.push(normalizePathSlashes(relative(engineDir, dest)));
|
|
424
427
|
}));
|
|
428
|
+
const failedCopy = copies.find((copy) => copy.status === 'rejected');
|
|
429
|
+
if (failedCopy?.status === 'rejected')
|
|
430
|
+
throw failedCopy.reason;
|
|
425
431
|
if (affectedPaths.length === 0) {
|
|
426
432
|
throw new FurnaceError(`No matching files found in override directory for "${name}"`, name);
|
|
427
433
|
}
|
|
@@ -5,7 +5,9 @@ import { assert } from '../utils/assert.js';
|
|
|
5
5
|
import { toError } from '../utils/errors.js';
|
|
6
6
|
import { pathExists } from '../utils/fs.js';
|
|
7
7
|
import { info } from '../utils/logger.js';
|
|
8
|
+
import { captureComponentSources, componentSourceErrors } from './component-source-guard.js';
|
|
8
9
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
10
|
+
import { assertComponentWriteBoundaries } from './engine-write-boundary.js';
|
|
9
11
|
import { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, diffDeletedFiles, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, undeployCustomFiles, undeployOverrideFiles, } from './furnace-apply-helpers.js';
|
|
10
12
|
import { findPatchOwnedOverwrites, loadPatchClaimsForApply, recordOverwriteWarnings, } from './furnace-apply-overwrite-warn.js';
|
|
11
13
|
import { buildCustomUndeployActions, buildOverrideUndeployActions, } from './furnace-apply-undeploy-actions.js';
|
|
@@ -334,6 +336,10 @@ export async function applyAllComponents(root, dryRun = false, options) {
|
|
|
334
336
|
const forgeConfig = await loadConfig(root).catch(() => undefined);
|
|
335
337
|
const markerComment = resolveFurnaceMarkerComment(forgeConfig);
|
|
336
338
|
await assertFurnaceEngineDirReady(engineDir);
|
|
339
|
+
if (!dryRun) {
|
|
340
|
+
await assertComponentWriteBoundaries(engineDir, config, ftlDir);
|
|
341
|
+
}
|
|
342
|
+
const sourceSnapshot = await captureComponentSources(furnacePaths, config, dryRun, options);
|
|
337
343
|
const rollbackJournal = dryRun ? undefined : createRollbackJournal();
|
|
338
344
|
if (rollbackJournal && operationContext) {
|
|
339
345
|
operationContext.registerJournal(rollbackJournal);
|
|
@@ -381,6 +387,7 @@ export async function applyAllComponents(root, dryRun = false, options) {
|
|
|
381
387
|
};
|
|
382
388
|
await applyOverrideBatch(batchContext);
|
|
383
389
|
await applyCustomBatch(batchContext, root, markerComment);
|
|
390
|
+
result.errors.push(...(await componentSourceErrors(sourceSnapshot)));
|
|
384
391
|
// Check for any partial failures (blocking step errors on applied
|
|
385
392
|
// components). Advisory step errors (e.g. FTL degradation) are warnings
|
|
386
393
|
// and never gate rollback.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
4
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
5
|
+
import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
|
|
5
6
|
/**
|
|
6
7
|
* Validates accessibility patterns in a component's .mjs file.
|
|
7
8
|
* Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
|
|
@@ -103,15 +104,16 @@ function hasUnlabelledFormInput(content) {
|
|
|
103
104
|
// (id implies an external <label for="..."> could exist). A control wrapped
|
|
104
105
|
// in a <label> that carries actual text is implicitly associated and is
|
|
105
106
|
// exempt as well.
|
|
107
|
+
content = collectLitTemplateRegions(content).join('\n');
|
|
106
108
|
const labelledSpans = collectLabelledSpans(content);
|
|
107
|
-
const inputPattern = /<(input|select|textarea)\b([
|
|
109
|
+
const inputPattern = /<(input|select|textarea)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi;
|
|
108
110
|
let inputMatch;
|
|
109
111
|
while ((inputMatch = inputPattern.exec(content)) !== null) {
|
|
110
|
-
const attrs = inputMatch[2] ?? '';
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const attrs = parseTemplateAttributes(inputMatch[2] ?? '');
|
|
113
|
+
if (attrs.has('aria-label') ||
|
|
114
|
+
attrs.has('aria-labelledby') ||
|
|
115
|
+
attrs.has('id') ||
|
|
116
|
+
attrs.get('type')?.toLowerCase() === 'hidden') {
|
|
115
117
|
continue;
|
|
116
118
|
}
|
|
117
119
|
const index = inputMatch.index;
|
|
@@ -315,12 +317,7 @@ function isFlaggableText(text) {
|
|
|
315
317
|
return true;
|
|
316
318
|
}
|
|
317
319
|
function hasFlaggedTextInLitTemplates(content) {
|
|
318
|
-
|
|
319
|
-
// so substrings like `otherhtml` do not spuriously open a template.
|
|
320
|
-
const htmlPattern = /(?:^|[^a-zA-Z0-9_$])html`([\s\S]*?)`/g;
|
|
321
|
-
let litMatch;
|
|
322
|
-
while ((litMatch = htmlPattern.exec(content)) !== null) {
|
|
323
|
-
const region = litMatch[1] ?? '';
|
|
320
|
+
for (const region of collectLitTemplateRegions(content)) {
|
|
324
321
|
const textPattern = />([^<$\s][^<$]*)</g;
|
|
325
322
|
let textMatch;
|
|
326
323
|
while ((textMatch = textPattern.exec(region)) !== null) {
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { toError } from '../utils/errors.js';
|
|
5
4
|
import { pathExists } from '../utils/fs.js';
|
|
6
5
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
7
6
|
import { extractComponentChecksums } from './furnace-checksum-utils.js';
|
|
8
7
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState } from './furnace-config.js';
|
|
9
8
|
import { resolveFtlDir, xpcshellTestParentDir } from './furnace-constants.js';
|
|
10
9
|
import { validateCssFragments } from './furnace-css-fragments.js';
|
|
11
|
-
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
12
10
|
import { findJsconfigPathsDrift } from './furnace-jsconfig.js';
|
|
13
11
|
import { validateAccessibility, validateCompatibility, validateJarMnEntries, validateRegistrationPatterns, validateStructure, validateTokenLink, } from './furnace-validate-checks.js';
|
|
14
12
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
@@ -110,47 +108,6 @@ export async function validateAllComponents(root) {
|
|
|
110
108
|
const config = await loadFurnaceConfig(root);
|
|
111
109
|
const furnacePaths = getFurnacePaths(root);
|
|
112
110
|
const results = new Map();
|
|
113
|
-
// Validate composition graph integrity (dangling references and cycles)
|
|
114
|
-
try {
|
|
115
|
-
validateComposesReferences(config.stock, config.overrides, config.custom);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
const message = toError(err).message;
|
|
119
|
-
// Attribute the issue to the first custom component with a bad composes reference
|
|
120
|
-
for (const [name, cfg] of Object.entries(config.custom)) {
|
|
121
|
-
if (cfg.composes) {
|
|
122
|
-
const existing = results.get(name) ?? [];
|
|
123
|
-
existing.push({
|
|
124
|
-
component: name,
|
|
125
|
-
severity: 'error',
|
|
126
|
-
check: 'composes-dangling-reference',
|
|
127
|
-
message,
|
|
128
|
-
});
|
|
129
|
-
results.set(name, existing);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
detectComposesCycles(config.custom);
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
const message = toError(err).message;
|
|
139
|
-
// Attribute the cycle issue to the first custom component in the cycle
|
|
140
|
-
for (const name of Object.keys(config.custom)) {
|
|
141
|
-
if (config.custom[name]?.composes) {
|
|
142
|
-
const existing = results.get(name) ?? [];
|
|
143
|
-
existing.push({
|
|
144
|
-
component: name,
|
|
145
|
-
severity: 'error',
|
|
146
|
-
check: 'composes-cycle',
|
|
147
|
-
message,
|
|
148
|
-
});
|
|
149
|
-
results.set(name, existing);
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
111
|
// Override components
|
|
155
112
|
for (const name of Object.keys(config.overrides)) {
|
|
156
113
|
const componentDir = join(furnacePaths.overridesDir, name);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/** Adds Gecko's cross-compartment DOM predicates to virtual lib.dom constructor types. */
|
|
3
|
+
export function withGeckoDomTypes(ts, host) {
|
|
4
|
+
const original = host.getSourceFile.bind(host);
|
|
5
|
+
return {
|
|
6
|
+
...host,
|
|
7
|
+
getSourceFile(...args) {
|
|
8
|
+
const file = original(...args);
|
|
9
|
+
if (!file || !args[0].endsWith('/lib.dom.d.ts'))
|
|
10
|
+
return file;
|
|
11
|
+
const source = file.text.replace(/declare var (\w+): \{\s*prototype: (\w+);/g, (declaration, name, instance) => declaration +
|
|
12
|
+
`\n isInstance(value: unknown): value is ${instance};` +
|
|
13
|
+
(name === 'MouseEvent' ? '\n readonly MOZ_SOURCE_PEN: number;' : ''));
|
|
14
|
+
return ts.createSourceFile(args[0], source, args[1], true);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -77,6 +77,27 @@ export declare function hashObjectBatch(repoDir: string, fullPaths: string[]): P
|
|
|
77
77
|
* @returns File content or null if file doesn't exist at that ref
|
|
78
78
|
*/
|
|
79
79
|
export declare function getFileContentAtRef(repoDir: string, filePath: string, ref?: string): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Batched equivalent of {@link getFileContentAtRef}: reads every path at
|
|
82
|
+
* `ref` through one `git cat-file --batch` process instead of one
|
|
83
|
+
* `git show` per file. A Firefox-sized ownership scan paid 1,493 of those,
|
|
84
|
+
* about 18 ms each and almost all of it spawn overhead.
|
|
85
|
+
*
|
|
86
|
+
* The first request line is `ref` itself, so an unresolvable ref throws a
|
|
87
|
+
* {@link GitError} exactly as the per-file helper does, rather than being
|
|
88
|
+
* read as "every path missing". A path reported `missing` maps to `null`,
|
|
89
|
+
* as the per-file helper returns for a path absent at that ref. Anything
|
|
90
|
+
* the batch cannot answer as a plain blob (a path containing a newline,
|
|
91
|
+
* which the line protocol cannot carry, or an object that is not a blob)
|
|
92
|
+
* is fetched through {@link getFileContentAtRef}, so the result for every
|
|
93
|
+
* path is what that helper returns.
|
|
94
|
+
*
|
|
95
|
+
* @param repoDir - Repository directory
|
|
96
|
+
* @param filePaths - Paths relative to the repository root
|
|
97
|
+
* @param ref - Git ref to read from. Defaults to HEAD.
|
|
98
|
+
* @returns Content (or `null` when absent at `ref`) for every input path
|
|
99
|
+
*/
|
|
100
|
+
export declare function getFilesContentAtRef(repoDir: string, filePaths: readonly string[], ref?: string): Promise<Map<string, string | null>>;
|
|
80
101
|
/**
|
|
81
102
|
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
82
103
|
* Uses the same heuristic as git.
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { GitError } from '../errors/git.js';
|
|
5
5
|
import { removeFile } from '../utils/fs.js';
|
|
6
6
|
import { exec } from '../utils/process.js';
|
|
7
|
+
import { execWithInput } from '../utils/process-input.js';
|
|
7
8
|
import { chunkPathspecs, ensureGit, git } from './git-base.js';
|
|
8
9
|
/**
|
|
9
10
|
* Restores a tracked path from HEAD, including staged changes.
|
|
@@ -175,6 +176,80 @@ export async function getFileContentAtRef(repoDir, filePath, ref = 'HEAD') {
|
|
|
175
176
|
}
|
|
176
177
|
return result.stdout;
|
|
177
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Batched equivalent of {@link getFileContentAtRef}: reads every path at
|
|
181
|
+
* `ref` through one `git cat-file --batch` process instead of one
|
|
182
|
+
* `git show` per file. A Firefox-sized ownership scan paid 1,493 of those,
|
|
183
|
+
* about 18 ms each and almost all of it spawn overhead.
|
|
184
|
+
*
|
|
185
|
+
* The first request line is `ref` itself, so an unresolvable ref throws a
|
|
186
|
+
* {@link GitError} exactly as the per-file helper does, rather than being
|
|
187
|
+
* read as "every path missing". A path reported `missing` maps to `null`,
|
|
188
|
+
* as the per-file helper returns for a path absent at that ref. Anything
|
|
189
|
+
* the batch cannot answer as a plain blob (a path containing a newline,
|
|
190
|
+
* which the line protocol cannot carry, or an object that is not a blob)
|
|
191
|
+
* is fetched through {@link getFileContentAtRef}, so the result for every
|
|
192
|
+
* path is what that helper returns.
|
|
193
|
+
*
|
|
194
|
+
* @param repoDir - Repository directory
|
|
195
|
+
* @param filePaths - Paths relative to the repository root
|
|
196
|
+
* @param ref - Git ref to read from. Defaults to HEAD.
|
|
197
|
+
* @returns Content (or `null` when absent at `ref`) for every input path
|
|
198
|
+
*/
|
|
199
|
+
export async function getFilesContentAtRef(repoDir, filePaths, ref = 'HEAD') {
|
|
200
|
+
const contents = new Map();
|
|
201
|
+
const unique = [...new Set(filePaths)];
|
|
202
|
+
const batchable = unique.filter((path) => !path.includes('\n'));
|
|
203
|
+
const fallback = unique.filter((path) => path.includes('\n'));
|
|
204
|
+
if (batchable.length > 0) {
|
|
205
|
+
await ensureGit();
|
|
206
|
+
const requests = [ref, ...batchable.map((path) => `${ref}:${path}`)];
|
|
207
|
+
const result = await execWithInput('git', ['cat-file', '--batch'], requests.join('\n') + '\n', {
|
|
208
|
+
cwd: repoDir,
|
|
209
|
+
});
|
|
210
|
+
if (result.exitCode !== 0) {
|
|
211
|
+
throw new GitError(result.stderr.trim() || 'git cat-file --batch failed', 'cat-file --batch');
|
|
212
|
+
}
|
|
213
|
+
const out = result.stdout;
|
|
214
|
+
let offset = 0;
|
|
215
|
+
for (const [index, request] of requests.entries()) {
|
|
216
|
+
const newline = out.indexOf(0x0a, offset);
|
|
217
|
+
if (newline === -1) {
|
|
218
|
+
throw new GitError(`Truncated cat-file --batch output at ${request}`, 'cat-file --batch');
|
|
219
|
+
}
|
|
220
|
+
const header = out.toString('utf8', offset, newline);
|
|
221
|
+
offset = newline + 1;
|
|
222
|
+
const path = index === 0 ? undefined : batchable[index - 1];
|
|
223
|
+
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
|
|
224
|
+
if (path === undefined) {
|
|
225
|
+
throw new GitError(`Cannot resolve ${ref}: ${header}`, `cat-file --batch ${ref}`);
|
|
226
|
+
}
|
|
227
|
+
if (header.endsWith(' missing'))
|
|
228
|
+
contents.set(path, null);
|
|
229
|
+
else
|
|
230
|
+
fallback.push(path);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const match = /^[0-9a-f]+ (\S+) (\d+)$/.exec(header);
|
|
234
|
+
if (!match) {
|
|
235
|
+
throw new GitError(`Unexpected cat-file --batch header: ${header}`, 'cat-file --batch');
|
|
236
|
+
}
|
|
237
|
+
const size = Number(match[2]);
|
|
238
|
+
const body = out.subarray(offset, offset + size);
|
|
239
|
+
offset += size + 1;
|
|
240
|
+
if (path === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
if (match[1] === 'blob')
|
|
243
|
+
contents.set(path, body.toString('utf8'));
|
|
244
|
+
else
|
|
245
|
+
fallback.push(path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const path of fallback) {
|
|
249
|
+
contents.set(path, await getFileContentAtRef(repoDir, path, ref));
|
|
250
|
+
}
|
|
251
|
+
return contents;
|
|
252
|
+
}
|
|
178
253
|
/**
|
|
179
254
|
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
180
255
|
* Uses the same heuristic as git.
|
|
@@ -39,6 +39,20 @@ export declare function resolveMaxUntrackedFilesPerDir(): number;
|
|
|
39
39
|
* files
|
|
40
40
|
*/
|
|
41
41
|
export declare function expandUntrackedDirectoryEntries(repoDir: string, entries: GitStatusEntry[]): Promise<GitStatusEntry[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Lists the untracked files under each collapsed `dir/` status entry with
|
|
44
|
+
* one `git ls-files` per ARG_MAX chunk, instead of one spawn per directory
|
|
45
|
+
* (a Firefox-sized worktree scan paid 73 of them). The result maps every
|
|
46
|
+
* requested directory to its files in git's order, which is exactly what
|
|
47
|
+
* {@link getUntrackedFilesInDir} returns for that directory alone. `-z`
|
|
48
|
+
* keeps non-ASCII names literal: the newline form C-quotes them under
|
|
49
|
+
* git's default `core.quotePath`, and a quoted path names no file.
|
|
50
|
+
*
|
|
51
|
+
* @param repoDir - Repository directory
|
|
52
|
+
* @param dirs - Collapsed untracked directories, each ending in `/`
|
|
53
|
+
* @returns Files per directory (every input key present, possibly empty)
|
|
54
|
+
*/
|
|
55
|
+
export declare function listUntrackedFilesInDirs(repoDir: string, dirs: readonly string[]): Promise<Map<string, string[]>>;
|
|
42
56
|
/**
|
|
43
57
|
* Gets all untracked files (including files inside untracked directories).
|
|
44
58
|
* @param repoDir - Repository directory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { warn } from '../utils/logger.js';
|
|
3
|
-
import { git } from './git-base.js';
|
|
3
|
+
import { chunkPathspecs, git } from './git-base.js';
|
|
4
4
|
/**
|
|
5
5
|
* True when a porcelain status column carries a rename or copy, i.e. the
|
|
6
6
|
* record is followed by a second NUL-terminated field holding the original
|
|
@@ -95,12 +95,16 @@ export function resolveMaxUntrackedFilesPerDir() {
|
|
|
95
95
|
export async function expandUntrackedDirectoryEntries(repoDir, entries) {
|
|
96
96
|
const expanded = [];
|
|
97
97
|
const maxPerDir = resolveMaxUntrackedFilesPerDir();
|
|
98
|
+
const collapsed = entries
|
|
99
|
+
.filter((entry) => entry.isUntracked && entry.file.endsWith('/'))
|
|
100
|
+
.map((entry) => entry.file);
|
|
101
|
+
const untrackedByDir = await listUntrackedFilesInDirs(repoDir, collapsed);
|
|
98
102
|
for (const entry of entries) {
|
|
99
103
|
if (!entry.isUntracked || !entry.file.endsWith('/')) {
|
|
100
104
|
expanded.push(entry);
|
|
101
105
|
continue;
|
|
102
106
|
}
|
|
103
|
-
const individualFiles =
|
|
107
|
+
const individualFiles = untrackedByDir.get(entry.file) ?? [];
|
|
104
108
|
if (individualFiles.length > maxPerDir) {
|
|
105
109
|
warn(`Untracked directory ${entry.file} contains ${individualFiles.length} files — only the first ${maxPerDir} are listed. Add a .gitignore entry or clean the directory.`);
|
|
106
110
|
}
|
|
@@ -118,6 +122,41 @@ export async function expandUntrackedDirectoryEntries(repoDir, entries) {
|
|
|
118
122
|
}
|
|
119
123
|
return expanded;
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Lists the untracked files under each collapsed `dir/` status entry with
|
|
127
|
+
* one `git ls-files` per ARG_MAX chunk, instead of one spawn per directory
|
|
128
|
+
* (a Firefox-sized worktree scan paid 73 of them). The result maps every
|
|
129
|
+
* requested directory to its files in git's order, which is exactly what
|
|
130
|
+
* {@link getUntrackedFilesInDir} returns for that directory alone. `-z`
|
|
131
|
+
* keeps non-ASCII names literal: the newline form C-quotes them under
|
|
132
|
+
* git's default `core.quotePath`, and a quoted path names no file.
|
|
133
|
+
*
|
|
134
|
+
* @param repoDir - Repository directory
|
|
135
|
+
* @param dirs - Collapsed untracked directories, each ending in `/`
|
|
136
|
+
* @returns Files per directory (every input key present, possibly empty)
|
|
137
|
+
*/
|
|
138
|
+
export async function listUntrackedFilesInDirs(repoDir, dirs) {
|
|
139
|
+
const byDir = new Map(dirs.map((dir) => [dir, []]));
|
|
140
|
+
if (byDir.size === 0)
|
|
141
|
+
return byDir;
|
|
142
|
+
for (const chunk of chunkPathspecs([...byDir.keys()])) {
|
|
143
|
+
const output = await git(['ls-files', '--others', '--exclude-standard', '-z', '--', ...chunk], repoDir);
|
|
144
|
+
for (const file of output.split('\0')) {
|
|
145
|
+
if (file.length === 0)
|
|
146
|
+
continue;
|
|
147
|
+
// Collapsed status directories never nest, so the first matching
|
|
148
|
+
// ancestor is the only one.
|
|
149
|
+
for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) {
|
|
150
|
+
const bucket = byDir.get(file.slice(0, slash + 1));
|
|
151
|
+
if (bucket !== undefined) {
|
|
152
|
+
bucket.push(file);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return byDir;
|
|
159
|
+
}
|
|
121
160
|
/**
|
|
122
161
|
* Gets all untracked files (including files inside untracked directories).
|
|
123
162
|
* @param repoDir - Repository directory
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Collects Lit html quasis, masking expressions while preserving nested templates. */
|
|
2
|
+
export declare function collectLitTemplateRegions(content: string): string[];
|
|
3
|
+
/** Reads actual attribute names and values, skipping quoted values as indivisible tokens. */
|
|
4
|
+
export declare function parseTemplateAttributes(source: string): Map<string, string | undefined>;
|