@hominis/fireforge 0.31.0 → 0.33.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 +22 -0
- package/dist/src/commands/export-all.js +4 -1
- package/dist/src/commands/export-shared.js +10 -1
- package/dist/src/commands/export.js +5 -1
- package/dist/src/commands/lint-per-patch.d.ts +2 -0
- package/dist/src/commands/lint-per-patch.js +206 -44
- package/dist/src/commands/lint.js +100 -7
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/test-run.d.ts +10 -0
- package/dist/src/commands/test-run.js +13 -4
- package/dist/src/commands/test.js +46 -7
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +52 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-jsconfig.js +22 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -0
- package/dist/src/core/git-file-ops.js +82 -1
- package/dist/src/core/mach-resource-shim.d.ts +21 -0
- package/dist/src/core/mach-resource-shim.js +92 -0
- package/dist/src/core/mach.d.ts +17 -0
- package/dist/src/core/mach.js +30 -2
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +213 -67
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +19 -163
- package/dist/src/core/test-harness-crash.d.ts +6 -3
- package/dist/src/core/test-harness-crash.js +32 -4
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +9 -4
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck-shim.d.ts +3 -1
- package/dist/src/core/typecheck-shim.js +43 -3
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/types/commands/options.d.ts +22 -0
- package/dist/src/types/config.d.ts +24 -2
- package/package.json +3 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Dry-run action planning for custom-component apply. Extracted from
|
|
4
|
+
* `furnace-apply-helpers.ts` so the apply path and its dry-run mirror each
|
|
5
|
+
* stay within the per-file line budget. Consumed only by that module.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { toError } from '../utils/errors.js';
|
|
9
|
+
import { pathExists } from '../utils/fs.js';
|
|
10
|
+
import { describeLocaleFtlJarMnRegistration, describeSharedFtlPrune } from './furnace-apply-ftl.js';
|
|
11
|
+
import { describeFragmentExpansion } from './furnace-css-fragments.js';
|
|
12
|
+
import { validateCustomElementRegistration, validateJarMnInsertionForFiles, } from './furnace-registration.js';
|
|
13
|
+
function isRegularFile(entry) {
|
|
14
|
+
if (!entry.isFile())
|
|
15
|
+
return false;
|
|
16
|
+
if (typeof entry.isSymbolicLink === 'function' && entry.isSymbolicLink())
|
|
17
|
+
return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
/** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
|
|
21
|
+
export async function buildCustomDryRunActions(name, componentDir, engineDir, config, targetDir, entries, ftlDir) {
|
|
22
|
+
const actions = [];
|
|
23
|
+
const stepErrors = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!isRegularFile(entry))
|
|
26
|
+
continue;
|
|
27
|
+
if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.css'))
|
|
28
|
+
continue;
|
|
29
|
+
const fragmentNote = await describeFragmentExpansion(join(componentDir, entry.name));
|
|
30
|
+
actions.push({
|
|
31
|
+
component: name,
|
|
32
|
+
action: fragmentNote ? 'expand-fragments' : 'copy',
|
|
33
|
+
source: join(componentDir, entry.name),
|
|
34
|
+
target: join(targetDir, entry.name),
|
|
35
|
+
description: `Copy ${entry.name} to ${config.targetPath}${fragmentNote}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Per-component .ftl handling is skipped when the component opts into a
|
|
39
|
+
// shared feature-scoped bundle via `sharedFtl`. The shared file is
|
|
40
|
+
// registered (and copied) by whoever owns the feature bundle, so
|
|
41
|
+
// emitting a copy-ftl / register-jar action here would duplicate (or
|
|
42
|
+
// later orphan) the entry.
|
|
43
|
+
if (config.localized && !config.sharedFtl) {
|
|
44
|
+
const ftlFile = `${name}.ftl`;
|
|
45
|
+
const ftlSrc = join(componentDir, ftlFile);
|
|
46
|
+
if (await pathExists(ftlSrc)) {
|
|
47
|
+
actions.push({
|
|
48
|
+
component: name,
|
|
49
|
+
action: 'copy-ftl',
|
|
50
|
+
source: ftlSrc,
|
|
51
|
+
target: join(engineDir, ftlDir, ftlFile),
|
|
52
|
+
description: `Copy ${ftlFile} to ${ftlDir}`,
|
|
53
|
+
});
|
|
54
|
+
const localeAction = describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile);
|
|
55
|
+
if (localeAction) {
|
|
56
|
+
actions.push(localeAction);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// A sharedFtl widget owns its strings via the shared bundle; surface the
|
|
61
|
+
// removal of any dangling per-widget locale jar.mn entry so the dry-run
|
|
62
|
+
// plan matches what apply will do (and explains the unblocked build).
|
|
63
|
+
const pruneAction = await describeSharedFtlPrune(engineDir, name, ftlDir, config);
|
|
64
|
+
if (pruneAction) {
|
|
65
|
+
actions.push(pruneAction);
|
|
66
|
+
}
|
|
67
|
+
if (config.register) {
|
|
68
|
+
try {
|
|
69
|
+
const modulePath = `chrome://global/content/elements/${name}.mjs`;
|
|
70
|
+
await validateCustomElementRegistration(engineDir, name, modulePath);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
stepErrors.push({
|
|
74
|
+
step: 'customElements.js registration',
|
|
75
|
+
error: toError(error).message,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
actions.push({
|
|
79
|
+
component: name,
|
|
80
|
+
action: 'register-ce',
|
|
81
|
+
description: `Register ${name} in customElements.js (DOMContentLoaded block)`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const copiedFileNames = entries
|
|
85
|
+
.filter((entry) => isRegularFile(entry) && (entry.name.endsWith('.mjs') || entry.name.endsWith('.css')))
|
|
86
|
+
.map((entry) => entry.name);
|
|
87
|
+
if (copiedFileNames.length > 0) {
|
|
88
|
+
try {
|
|
89
|
+
await validateJarMnInsertionForFiles(engineDir, name, copiedFileNames);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
stepErrors.push({
|
|
93
|
+
step: 'jar.mn registration',
|
|
94
|
+
error: toError(error).message,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
actions.push({
|
|
98
|
+
component: name,
|
|
99
|
+
action: 'register-jar',
|
|
100
|
+
description: `Add ${copiedFileNames.join(', ')} to jar.mn`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { actions, stepErrors };
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=furnace-apply-dry-run.js.map
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
|
|
12
12
|
import { type RollbackJournal } from './furnace-rollback.js';
|
|
13
|
+
/**
|
|
14
|
+
* Apply-path wrapper around {@link pruneSharedFtlPerWidgetLocaleEntry} that
|
|
15
|
+
* records the affected path / step error in the caller's collectors, mirroring
|
|
16
|
+
* {@link applyCustomFtlFile}'s contract so the main apply helper stays terse.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applySharedFtlPrune(engineDir: string, name: string, ftlDir: string, config: CustomComponentConfig, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Read-only dry-run describer for {@link pruneSharedFtlPerWidgetLocaleEntry}:
|
|
21
|
+
* returns an action when a dangling per-widget locale entry exists for a
|
|
22
|
+
* `sharedFtl` widget, else `undefined`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function describeSharedFtlPrune(engineDir: string, name: string, ftlDir: string, config: CustomComponentConfig): Promise<DryRunAction | undefined>;
|
|
13
25
|
/**
|
|
14
26
|
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
15
27
|
* in the locale jar.mn.
|
|
@@ -11,10 +11,106 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import { toError } from '../utils/errors.js';
|
|
14
|
-
import { copyFile, pathExists } from '../utils/fs.js';
|
|
14
|
+
import { copyFile, pathExists, readText } from '../utils/fs.js';
|
|
15
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
15
16
|
import { resolveFtlChromeSubPath, resolveFtlLocaleJarMnPath } from './furnace-constants.js';
|
|
16
17
|
import { addLocaleFtlJarMnEntry, removeLocaleFtlJarMnEntry } from './furnace-registration.js';
|
|
17
18
|
import { snapshotFile } from './furnace-rollback.js';
|
|
19
|
+
/**
|
|
20
|
+
* Builds the presence regex for a per-widget locale jar.mn line
|
|
21
|
+
* (`locale/@AB_CD@/<chromeSubPath>/<tagName>.ftl`). Shared by the prune
|
|
22
|
+
* helper and its dry-run describer so both agree on what "dangling" means.
|
|
23
|
+
*/
|
|
24
|
+
function perWidgetLocaleEntryPattern(chromeSubPath, tagName) {
|
|
25
|
+
return new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapeRegex(chromeSubPath)}\\/${escapeRegex(tagName)}\\.ftl`, 'm');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the engine-relative locale jar.mn and the per-widget entry regex
|
|
29
|
+
* for a `sharedFtl` widget, or `undefined` when the FTL tree exposes no
|
|
30
|
+
* locale jar.mn we can confidently name.
|
|
31
|
+
*/
|
|
32
|
+
function resolveSharedFtlPruneTarget(name, ftlDir) {
|
|
33
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
34
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
35
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { localeJarRel, pattern: perWidgetLocaleEntryPattern(chromeSubPath, name) };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Removes a dangling per-widget locale jar.mn entry for a `sharedFtl` widget.
|
|
41
|
+
*
|
|
42
|
+
* A `localized: true` widget that opts into a feature-scoped `sharedFtl`
|
|
43
|
+
* bundle (its strings live under `browser/...` and load via
|
|
44
|
+
* `insertFTLIfNeeded`) must NOT carry a per-widget
|
|
45
|
+
* `locale/@AB_CD@/<chromeSubPath>/<name>.ftl` line. Such a line — written by
|
|
46
|
+
* an older FireForge before the sharedFtl apply guard, or by a layout
|
|
47
|
+
* migration — points at a `.ftl` that does not exist, so `mach build` fails
|
|
48
|
+
* hard (`Cannot find <chromeSubPath>/<name>.ftl`) and blocks every build.
|
|
49
|
+
*
|
|
50
|
+
* The pruned line is the per-widget toolkit entry only; the shared bundle's
|
|
51
|
+
* own line (a different chrome sub-path / base name) is never matched, so
|
|
52
|
+
* pruning one widget cannot orphan the shared bundle. Idempotent: when no
|
|
53
|
+
* dangling entry exists the file is left untouched (no journal churn).
|
|
54
|
+
* Returns the engine-relative jar.mn path when a line was removed, else
|
|
55
|
+
* `undefined`.
|
|
56
|
+
*/
|
|
57
|
+
async function pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, config, rollbackJournal) {
|
|
58
|
+
if (!config.sharedFtl)
|
|
59
|
+
return undefined;
|
|
60
|
+
const target = resolveSharedFtlPruneTarget(name, ftlDir);
|
|
61
|
+
if (!target)
|
|
62
|
+
return undefined;
|
|
63
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
64
|
+
if (chromeSubPath === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const localeJarAbs = join(engineDir, target.localeJarRel);
|
|
67
|
+
if (!(await pathExists(localeJarAbs)))
|
|
68
|
+
return undefined;
|
|
69
|
+
if (!target.pattern.test(await readText(localeJarAbs)))
|
|
70
|
+
return undefined;
|
|
71
|
+
if (rollbackJournal) {
|
|
72
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
73
|
+
}
|
|
74
|
+
await removeLocaleFtlJarMnEntry(engineDir, target.localeJarRel, name, chromeSubPath);
|
|
75
|
+
return target.localeJarRel;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Apply-path wrapper around {@link pruneSharedFtlPerWidgetLocaleEntry} that
|
|
79
|
+
* records the affected path / step error in the caller's collectors, mirroring
|
|
80
|
+
* {@link applyCustomFtlFile}'s contract so the main apply helper stays terse.
|
|
81
|
+
*/
|
|
82
|
+
export async function applySharedFtlPrune(engineDir, name, ftlDir, config, affectedPaths, stepErrors, rollbackJournal) {
|
|
83
|
+
try {
|
|
84
|
+
const prunedPath = await pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, config, rollbackJournal);
|
|
85
|
+
if (prunedPath)
|
|
86
|
+
affectedPaths.push(prunedPath);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
stepErrors.push({ step: 'locale jar.mn prune', error: toError(error).message });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read-only dry-run describer for {@link pruneSharedFtlPerWidgetLocaleEntry}:
|
|
94
|
+
* returns an action when a dangling per-widget locale entry exists for a
|
|
95
|
+
* `sharedFtl` widget, else `undefined`.
|
|
96
|
+
*/
|
|
97
|
+
export async function describeSharedFtlPrune(engineDir, name, ftlDir, config) {
|
|
98
|
+
if (!config.sharedFtl)
|
|
99
|
+
return undefined;
|
|
100
|
+
const target = resolveSharedFtlPruneTarget(name, ftlDir);
|
|
101
|
+
if (!target)
|
|
102
|
+
return undefined;
|
|
103
|
+
const localeJarAbs = join(engineDir, target.localeJarRel);
|
|
104
|
+
if (!(await pathExists(localeJarAbs)))
|
|
105
|
+
return undefined;
|
|
106
|
+
if (!target.pattern.test(await readText(localeJarAbs)))
|
|
107
|
+
return undefined;
|
|
108
|
+
return {
|
|
109
|
+
component: name,
|
|
110
|
+
action: 'register-jar',
|
|
111
|
+
description: `Remove dangling per-widget locale entry for ${name} from ${target.localeJarRel} (sharedFtl bundle owns its strings)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
18
114
|
/**
|
|
19
115
|
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
20
116
|
* in the locale jar.mn.
|
|
@@ -6,10 +6,11 @@ import { FurnaceError } from '../errors/furnace.js';
|
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
7
7
|
import { copyFile, ensureDir, pathExists, readText, removeFile } from '../utils/fs.js';
|
|
8
8
|
import { verbose } from '../utils/logger.js';
|
|
9
|
-
import {
|
|
9
|
+
import { buildCustomDryRunActions } from './furnace-apply-dry-run.js';
|
|
10
|
+
import { applyCustomFtlFile, applySharedFtlPrune, removeCustomFtlJarMnEntry, } from './furnace-apply-ftl.js';
|
|
10
11
|
import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
11
|
-
import { deployFileWithFragments,
|
|
12
|
-
import { addCustomElementRegistration, addJarMnEntries
|
|
12
|
+
import { deployFileWithFragments, SHARED_FRAGMENTS_DIR } from './furnace-css-fragments.js';
|
|
13
|
+
import { addCustomElementRegistration, addJarMnEntries } from './furnace-registration.js';
|
|
13
14
|
import { recordCreatedDir, snapshotFile } from './furnace-rollback.js';
|
|
14
15
|
import { checkRegistrationConsistency } from './furnace-validate-registration.js';
|
|
15
16
|
import { isGitRepository } from './git.js';
|
|
@@ -278,83 +279,6 @@ export async function hasCustomEngineDrift(root, name, componentDir, config, ftl
|
|
|
278
279
|
}
|
|
279
280
|
return false;
|
|
280
281
|
}
|
|
281
|
-
async function buildCustomDryRunActions(name, componentDir, engineDir, config, targetDir, entries, ftlDir) {
|
|
282
|
-
const actions = [];
|
|
283
|
-
const stepErrors = [];
|
|
284
|
-
for (const entry of entries) {
|
|
285
|
-
if (!isRegularFile(entry))
|
|
286
|
-
continue;
|
|
287
|
-
if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.css'))
|
|
288
|
-
continue;
|
|
289
|
-
const fragmentNote = await describeFragmentExpansion(join(componentDir, entry.name));
|
|
290
|
-
actions.push({
|
|
291
|
-
component: name,
|
|
292
|
-
action: fragmentNote ? 'expand-fragments' : 'copy',
|
|
293
|
-
source: join(componentDir, entry.name),
|
|
294
|
-
target: join(targetDir, entry.name),
|
|
295
|
-
description: `Copy ${entry.name} to ${config.targetPath}${fragmentNote}`,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
// Per-component .ftl handling is skipped when the component opts into a
|
|
299
|
-
// shared feature-scoped bundle via `sharedFtl`. The shared file is
|
|
300
|
-
// registered (and copied) by whoever owns the feature bundle, so
|
|
301
|
-
// emitting a copy-ftl / register-jar action here would duplicate (or
|
|
302
|
-
// later orphan) the entry.
|
|
303
|
-
if (config.localized && !config.sharedFtl) {
|
|
304
|
-
const ftlFile = `${name}.ftl`;
|
|
305
|
-
const ftlSrc = join(componentDir, ftlFile);
|
|
306
|
-
if (await pathExists(ftlSrc)) {
|
|
307
|
-
actions.push({
|
|
308
|
-
component: name,
|
|
309
|
-
action: 'copy-ftl',
|
|
310
|
-
source: ftlSrc,
|
|
311
|
-
target: join(engineDir, ftlDir, ftlFile),
|
|
312
|
-
description: `Copy ${ftlFile} to ${ftlDir}`,
|
|
313
|
-
});
|
|
314
|
-
const localeAction = describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile);
|
|
315
|
-
if (localeAction) {
|
|
316
|
-
actions.push(localeAction);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (config.register) {
|
|
321
|
-
try {
|
|
322
|
-
const modulePath = `chrome://global/content/elements/${name}.mjs`;
|
|
323
|
-
await validateCustomElementRegistration(engineDir, name, modulePath);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
stepErrors.push({
|
|
327
|
-
step: 'customElements.js registration',
|
|
328
|
-
error: toError(error).message,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
actions.push({
|
|
332
|
-
component: name,
|
|
333
|
-
action: 'register-ce',
|
|
334
|
-
description: `Register ${name} in customElements.js (DOMContentLoaded block)`,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
const copiedFileNames = entries
|
|
338
|
-
.filter((entry) => isRegularFile(entry) && (entry.name.endsWith('.mjs') || entry.name.endsWith('.css')))
|
|
339
|
-
.map((entry) => entry.name);
|
|
340
|
-
if (copiedFileNames.length > 0) {
|
|
341
|
-
try {
|
|
342
|
-
await validateJarMnInsertionForFiles(engineDir, name, copiedFileNames);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
stepErrors.push({
|
|
346
|
-
step: 'jar.mn registration',
|
|
347
|
-
error: toError(error).message,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
actions.push({
|
|
351
|
-
component: name,
|
|
352
|
-
action: 'register-jar',
|
|
353
|
-
description: `Add ${copiedFileNames.join(', ')} to jar.mn`,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return { actions, stepErrors };
|
|
357
|
-
}
|
|
358
282
|
/** Applies a custom component into the engine tree and captures registration step errors. */
|
|
359
283
|
export async function applyCustomComponent(engineDir, name, componentDir, config, ftlDir, dryRun = false, rollbackJournal, applyOptions = {}) {
|
|
360
284
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
@@ -406,6 +330,12 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
|
|
|
406
330
|
if (config.localized && !config.sharedFtl) {
|
|
407
331
|
await applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal);
|
|
408
332
|
}
|
|
333
|
+
else if (config.localized && config.sharedFtl) {
|
|
334
|
+
// Drop any dangling per-widget locale jar.mn entry that would point at a
|
|
335
|
+
// non-existent `<chromeSubPath>/<name>.ftl` and fail `mach build`. The
|
|
336
|
+
// shared bundle (a different chrome path/base name) is never touched.
|
|
337
|
+
await applySharedFtlPrune(engineDir, name, ftlDir, config, affectedPaths, stepErrors, rollbackJournal);
|
|
338
|
+
}
|
|
409
339
|
if (config.register) {
|
|
410
340
|
try {
|
|
411
341
|
const modulePath = `chrome://global/content/elements/${name}.mjs`;
|
|
@@ -51,12 +51,28 @@ async function computeDesiredChromePathEntries(config, customDir, jsconfigAbsPat
|
|
|
51
51
|
for (const file of files.sort()) {
|
|
52
52
|
if (!file.endsWith('.mjs'))
|
|
53
53
|
continue;
|
|
54
|
-
|
|
54
|
+
// Emit a `./`-prefixed relative value. TypeScript treats a bare
|
|
55
|
+
// `paths` value (`moz-widget/moz-widget.mjs`) as non-relative and
|
|
56
|
+
// rejects it without `baseUrl` (TS5090); a `./`-prefixed value
|
|
57
|
+
// resolves against the jsconfig directory with no `baseUrl` (which
|
|
58
|
+
// TS6 deprecates, TS5101). `../`-prefixed paths are already relative
|
|
59
|
+
// and left untouched.
|
|
60
|
+
const rel = normalizePathSlashes(relative(jsconfigDir, join(componentDir, file)));
|
|
61
|
+
const sourcePath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
55
62
|
entries[`${CHROME_ELEMENTS_URL_PREFIX}${file}`] = [sourcePath];
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
return entries;
|
|
59
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Compares two `paths` values treating a leading `./` as insignificant, so
|
|
69
|
+
* the reconciler does not churn between `./x` and bare `x` forms (either
|
|
70
|
+
* direction). Used to decide whether a managed entry is stale.
|
|
71
|
+
*/
|
|
72
|
+
function samePathValue(a, b) {
|
|
73
|
+
const strip = (p) => (p.startsWith('./') ? p.slice(2) : p);
|
|
74
|
+
return strip(a) === strip(b);
|
|
75
|
+
}
|
|
60
76
|
/** True when `key`/`value` is a Furnace-managed chrome-elements mapping. */
|
|
61
77
|
function isManagedEntry(key, value, jsconfigDir, customDir) {
|
|
62
78
|
if (!key.startsWith(CHROME_ELEMENTS_URL_PREFIX))
|
|
@@ -119,7 +135,11 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
|
|
|
119
135
|
result.pruned.push(key);
|
|
120
136
|
continue;
|
|
121
137
|
}
|
|
122
|
-
|
|
138
|
+
// Treat `./x` and bare `x` as equal so a previously-synced bare value (or
|
|
139
|
+
// a hand-written `./` prefix) is not rewritten as "stale" on every run.
|
|
140
|
+
// The existing value is kept verbatim when equivalent — no churn either
|
|
141
|
+
// way; only a genuinely different target updates (to the `./` form).
|
|
142
|
+
if (!samePathValue(value[0] ?? '', want[0] ?? '')) {
|
|
123
143
|
result.updated.push(key);
|
|
124
144
|
nextPaths[key] = want;
|
|
125
145
|
}
|
|
@@ -63,6 +63,21 @@ export declare function git(args: string[], cwd: string, options?: {
|
|
|
63
63
|
timeout?: number;
|
|
64
64
|
env?: Record<string, string>;
|
|
65
65
|
}): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
68
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
69
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
70
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
71
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
72
|
+
*
|
|
73
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
74
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
75
|
+
* how the paths are grouped across invocations never affects the result.
|
|
76
|
+
* @param paths - Pathspecs to chunk
|
|
77
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
78
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
79
|
+
*/
|
|
80
|
+
export declare function chunkPathspecs(paths: string[], budgetBytes?: number): string[][];
|
|
66
81
|
/**
|
|
67
82
|
* Configures git performance settings for large trees.
|
|
68
83
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -72,6 +72,38 @@ export async function git(args, cwd, options) {
|
|
|
72
72
|
}
|
|
73
73
|
return result.stdout;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
77
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
78
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
79
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
80
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
81
|
+
*
|
|
82
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
83
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
84
|
+
* how the paths are grouped across invocations never affects the result.
|
|
85
|
+
* @param paths - Pathspecs to chunk
|
|
86
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
87
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
88
|
+
*/
|
|
89
|
+
export function chunkPathspecs(paths, budgetBytes = 96_000) {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let current = [];
|
|
92
|
+
let used = 0;
|
|
93
|
+
for (const path of paths) {
|
|
94
|
+
const cost = Buffer.byteLength(path) + 1;
|
|
95
|
+
if (current.length > 0 && used + cost > budgetBytes) {
|
|
96
|
+
chunks.push(current);
|
|
97
|
+
current = [];
|
|
98
|
+
used = 0;
|
|
99
|
+
}
|
|
100
|
+
current.push(path);
|
|
101
|
+
used += cost;
|
|
102
|
+
}
|
|
103
|
+
if (current.length > 0)
|
|
104
|
+
chunks.push(current);
|
|
105
|
+
return chunks;
|
|
106
|
+
}
|
|
75
107
|
/**
|
|
76
108
|
* Configures git performance settings for large trees.
|
|
77
109
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -40,6 +40,14 @@ export declare function getAllDiff(repoDir: string): Promise<string>;
|
|
|
40
40
|
* Builds a combined diff against HEAD for the provided files without touching
|
|
41
41
|
* the real git index. Tracked files use `git diff HEAD`; untracked files use
|
|
42
42
|
* synthesized new-file diffs.
|
|
43
|
+
*
|
|
44
|
+
* Performance: the work is batched into a handful of `git` invocations
|
|
45
|
+
* (one `ls-tree` to classify, one `diff` over all tracked files, one
|
|
46
|
+
* `hash-object` over all new text files) rather than the ~2 spawns per file the
|
|
47
|
+
* previous per-file loop issued — that fan-out dominated the cold-run cost on a
|
|
48
|
+
* Firefox-sized checkout (~700 serial spawns, ~99s). Binary, directory, and
|
|
49
|
+
* recursion paths stay per-file because they are rare and (for binary) mutate
|
|
50
|
+
* the index.
|
|
43
51
|
* @param repoDir - Repository directory
|
|
44
52
|
* @param files - File paths to diff (relative to repo root)
|
|
45
53
|
* @returns Combined diff content
|