@hominis/fireforge 0.32.0 → 0.34.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 +25 -0
- package/dist/src/commands/build.js +9 -1
- package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
- package/dist/src/commands/doctor-furnace-jar.js +47 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/export-flow.js +4 -1
- package/dist/src/commands/export-placement-gate.js +4 -1
- package/dist/src/commands/export.js +61 -4
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
- package/dist/src/commands/furnace/chrome-doc.js +31 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
- package/dist/src/commands/furnace/create-browser-test.js +180 -0
- package/dist/src/commands/furnace/create-mochikit.js +11 -4
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +38 -12
- package/dist/src/commands/furnace/create.js +7 -114
- package/dist/src/commands/furnace/index.js +6 -1
- package/dist/src/commands/furnace/scan.d.ts +1 -0
- package/dist/src/commands/furnace/scan.js +57 -27
- package/dist/src/commands/furnace/validate.js +17 -1
- 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-options.d.ts +26 -0
- package/dist/src/commands/re-export-options.js +48 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +10 -1
- package/dist/src/commands/test-diagnose.js +12 -0
- package/dist/src/commands/test.js +29 -22
- 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 +26 -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-registration.d.ts +24 -0
- package/dist/src/core/furnace-registration.js +56 -3
- package/dist/src/core/furnace-validate-registration.js +18 -0
- package/dist/src/core/mach-resource-shim.d.ts +58 -0
- package/dist/src/core/mach-resource-shim.js +290 -0
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -25
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/manifest-rules.d.ts +13 -3
- package/dist/src/core/manifest-rules.js +44 -17
- package/dist/src/core/patch-export.d.ts +10 -0
- package/dist/src/core/patch-export.js +23 -2
- 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-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-module.js +15 -2
- package/dist/src/core/register-result.d.ts +9 -0
- package/dist/src/core/register-scaffold.d.ts +55 -0
- package/dist/src/core/register-scaffold.js +146 -0
- package/dist/src/core/register-test-manifest.js +3 -1
- package/dist/src/core/register-xpcshell-test.d.ts +30 -0
- package/dist/src/core/register-xpcshell-test.js +96 -0
- package/dist/src/core/test-harness-crash.d.ts +32 -4
- package/dist/src/core/test-harness-crash.js +152 -14
- 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.js +56 -28
- package/dist/src/core/xpcshell-appdir.d.ts +7 -0
- package/dist/src/core/xpcshell-appdir.js +12 -3
- package/dist/src/types/commands/options.d.ts +17 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +4 -4
|
@@ -401,6 +401,32 @@ function parseTypecheckBlock(rec) {
|
|
|
401
401
|
if (extraShim !== undefined) {
|
|
402
402
|
out.extraShim = parseShimPath(extraShim, 'typecheck.extraShim');
|
|
403
403
|
}
|
|
404
|
+
const overrides = parseTypecheckProjectOverrides(rec.raw('projectOverrides'), projects);
|
|
405
|
+
if (overrides) {
|
|
406
|
+
out.projectOverrides = overrides;
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Validates the optional `typecheck.projectOverrides` map: keys must name a
|
|
412
|
+
* declared project, values must be either `null` (opt out of the shared extra
|
|
413
|
+
* shim) or a contained relative `.d.ts` path (per-project override). Returns
|
|
414
|
+
* `undefined` when the field is absent.
|
|
415
|
+
*/
|
|
416
|
+
function parseTypecheckProjectOverrides(raw, projects) {
|
|
417
|
+
if (raw === undefined)
|
|
418
|
+
return undefined;
|
|
419
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
420
|
+
throw new ConfigError('Config field "typecheck.projectOverrides" must be an object');
|
|
421
|
+
}
|
|
422
|
+
const known = new Set(projects);
|
|
423
|
+
const out = {};
|
|
424
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
425
|
+
if (!known.has(key)) {
|
|
426
|
+
throw new ConfigError(`Config field "typecheck.projectOverrides" key "${key}" does not match any entry in "typecheck.projects"`);
|
|
427
|
+
}
|
|
428
|
+
out[key] = value === null ? null : parseShimPath(value, `typecheck.projectOverrides["${key}"]`);
|
|
429
|
+
}
|
|
404
430
|
return out;
|
|
405
431
|
}
|
|
406
432
|
//# sourceMappingURL=config-validate.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-run action planning for custom-component apply. Extracted from
|
|
3
|
+
* `furnace-apply-helpers.ts` so the apply path and its dry-run mirror each
|
|
4
|
+
* stay within the per-file line budget. Consumed only by that module.
|
|
5
|
+
*/
|
|
6
|
+
import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
|
|
7
|
+
interface DirectoryEntry {
|
|
8
|
+
isFile(): boolean;
|
|
9
|
+
isSymbolicLink?(): boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
|
|
13
|
+
export declare function buildCustomDryRunActions(name: string, componentDir: string, engineDir: string, config: CustomComponentConfig, targetDir: string, entries: DirectoryEntry[], ftlDir: string): Promise<{
|
|
14
|
+
actions: DryRunAction[];
|
|
15
|
+
stepErrors: StepError[];
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
|
@@ -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`;
|
|
@@ -71,6 +71,30 @@ export declare function removeLocaleFtlJarMnEntry(engineDir: string, jarMnRelPat
|
|
|
71
71
|
* @param tagName - Custom element tag name whose entries should be removed
|
|
72
72
|
*/
|
|
73
73
|
export declare function removeJarMnEntries(engineDir: string, tagName: string): Promise<void>;
|
|
74
|
+
/** A jar.mn widget registration line whose source file no longer exists. */
|
|
75
|
+
export interface StaleJarMnEntry {
|
|
76
|
+
/** Component tag name from the `(widgets/<tag>/<file>)` source mapping. */
|
|
77
|
+
tagName: string;
|
|
78
|
+
/** File name from the source mapping. */
|
|
79
|
+
fileName: string;
|
|
80
|
+
/** The full (trimmed) jar.mn line. */
|
|
81
|
+
line: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Scans jar.mn for widget registration lines `(widgets/<tag>/<file>)`
|
|
85
|
+
* whose workspace source file no longer exists (0.34.0 field report: a
|
|
86
|
+
* renamed component helper left the old line pointing at a deleted file,
|
|
87
|
+
* and every build failed at packaging). Only tags in `managedTags`
|
|
88
|
+
* (furnace-managed custom components) are inspected so upstream lines are
|
|
89
|
+
* never touched.
|
|
90
|
+
*/
|
|
91
|
+
export declare function findStaleJarMnEntries(engineDir: string, customDir: string, managedTags: readonly string[]): Promise<StaleJarMnEntry[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Removes every stale widget registration line found by
|
|
94
|
+
* {@link findStaleJarMnEntries}. Returns the removed entries. Used by
|
|
95
|
+
* `furnace validate --fix` and `doctor --repair-furnace`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pruneStaleJarMnEntries(engineDir: string, customDir: string, managedTags: readonly string[]): Promise<StaleJarMnEntry[]>;
|
|
74
98
|
/**
|
|
75
99
|
* Validates that jar.mn entries *could* be added without writing anything.
|
|
76
100
|
* Used by dry-run to surface structural problems early.
|
|
@@ -224,14 +224,67 @@ export async function removeJarMnEntries(engineDir, tagName) {
|
|
|
224
224
|
}
|
|
225
225
|
let content = await readText(filePath);
|
|
226
226
|
const lines = content.split('\n');
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
// Match by the SOURCE MAPPING segment `(widgets/<tagName>/...)` so every
|
|
228
|
+
// line the component registered is removed regardless of the target
|
|
229
|
+
// basename — a helper `.mjs` whose name does not start with the tag
|
|
230
|
+
// (e.g. a renamed `foo-utils.mjs`) used to survive the remove pass and
|
|
231
|
+
// leave a stale registration that broke packaging (0.34.0 field
|
|
232
|
+
// report). The legacy target-path match is kept as an OR for lines
|
|
233
|
+
// written by older FireForge versions without a source mapping. Word
|
|
234
|
+
// boundaries keep "moz-card" from matching "moz-card-group".
|
|
235
|
+
const sourcePattern = new RegExp(`\\(widgets/${escapeForRegex(tagName)}/`);
|
|
236
|
+
const legacyTargetPattern = new RegExp(`content/global/elements/${escapeForRegex(tagName)}\\.`);
|
|
237
|
+
const filtered = lines.filter((line) => !sourcePattern.test(line) && !legacyTargetPattern.test(line));
|
|
230
238
|
if (filtered.length === lines.length)
|
|
231
239
|
return;
|
|
232
240
|
content = filtered.join('\n');
|
|
233
241
|
await writeText(filePath, content);
|
|
234
242
|
}
|
|
243
|
+
const WIDGET_SOURCE_MAPPING_PATTERN = /\(widgets\/([^/)]+)\/([^)]+)\)/;
|
|
244
|
+
/**
|
|
245
|
+
* Scans jar.mn for widget registration lines `(widgets/<tag>/<file>)`
|
|
246
|
+
* whose workspace source file no longer exists (0.34.0 field report: a
|
|
247
|
+
* renamed component helper left the old line pointing at a deleted file,
|
|
248
|
+
* and every build failed at packaging). Only tags in `managedTags`
|
|
249
|
+
* (furnace-managed custom components) are inspected so upstream lines are
|
|
250
|
+
* never touched.
|
|
251
|
+
*/
|
|
252
|
+
export async function findStaleJarMnEntries(engineDir, customDir, managedTags) {
|
|
253
|
+
const filePath = join(engineDir, JAR_MN);
|
|
254
|
+
if (!(await pathExists(filePath)))
|
|
255
|
+
return [];
|
|
256
|
+
const managed = new Set(managedTags);
|
|
257
|
+
const stale = [];
|
|
258
|
+
for (const line of (await readText(filePath)).split('\n')) {
|
|
259
|
+
const match = WIDGET_SOURCE_MAPPING_PATTERN.exec(line);
|
|
260
|
+
if (!match)
|
|
261
|
+
continue;
|
|
262
|
+
const tagName = match[1] ?? '';
|
|
263
|
+
const fileName = match[2] ?? '';
|
|
264
|
+
if (!managed.has(tagName))
|
|
265
|
+
continue;
|
|
266
|
+
if (!(await pathExists(join(customDir, tagName, fileName)))) {
|
|
267
|
+
stale.push({ tagName, fileName, line: line.trim() });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return stale;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Removes every stale widget registration line found by
|
|
274
|
+
* {@link findStaleJarMnEntries}. Returns the removed entries. Used by
|
|
275
|
+
* `furnace validate --fix` and `doctor --repair-furnace`.
|
|
276
|
+
*/
|
|
277
|
+
export async function pruneStaleJarMnEntries(engineDir, customDir, managedTags) {
|
|
278
|
+
const stale = await findStaleJarMnEntries(engineDir, customDir, managedTags);
|
|
279
|
+
if (stale.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const filePath = join(engineDir, JAR_MN);
|
|
282
|
+
const staleLines = new Set(stale.map((entry) => entry.line));
|
|
283
|
+
const lines = (await readText(filePath)).split('\n');
|
|
284
|
+
const filtered = lines.filter((line) => !staleLines.has(line.trim()));
|
|
285
|
+
await writeText(filePath, filtered.join('\n'));
|
|
286
|
+
return stale;
|
|
287
|
+
}
|
|
235
288
|
/**
|
|
236
289
|
* Validates that jar.mn entries *could* be added without writing anything.
|
|
237
290
|
* Used by dry-run to surface structural problems early.
|
|
@@ -10,6 +10,7 @@ import { getProjectPaths, loadConfig } from './config.js';
|
|
|
10
10
|
import { getFurnacePaths } from './furnace-config.js';
|
|
11
11
|
import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
|
|
12
12
|
import { expandCssFragments, listFragmentIncludes } from './furnace-css-fragments.js';
|
|
13
|
+
import { findStaleJarMnEntries } from './furnace-registration.js';
|
|
13
14
|
import { isTagInCorrectCustomElementsPlacement } from './furnace-registration-validate.js';
|
|
14
15
|
import { getTokensCssPath } from './token-manager.js';
|
|
15
16
|
/**
|
|
@@ -210,6 +211,23 @@ export async function validateJarMnEntries(root, config) {
|
|
|
210
211
|
});
|
|
211
212
|
}
|
|
212
213
|
}
|
|
214
|
+
// Stale registrations: lines pointing at component files that no longer
|
|
215
|
+
// exist in the workspace (left behind by a rename/delete under an older
|
|
216
|
+
// FireForge). These break `mach build` at packaging ("File ... not
|
|
217
|
+
// found"), so they are errors and `--fix` prunes them (0.34.0 field
|
|
218
|
+
// report: both validate --fix and doctor --repair-furnace reported
|
|
219
|
+
// success without pruning).
|
|
220
|
+
const staleEntries = await findStaleJarMnEntries(engineDir, furnacePaths.customDir, Object.keys(config.custom));
|
|
221
|
+
for (const stale of staleEntries) {
|
|
222
|
+
issues.push({
|
|
223
|
+
component: stale.tagName,
|
|
224
|
+
severity: 'error',
|
|
225
|
+
check: 'stale-jar-registration',
|
|
226
|
+
message: `jar.mn registers ${stale.fileName} for ${stale.tagName}, but the source file no longer exists ` +
|
|
227
|
+
`(stale line: "${stale.line}"). Packaging will fail until the line is removed — run ` +
|
|
228
|
+
'"fireforge furnace validate --fix" or "fireforge doctor --repair-furnace" to prune it.',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
213
231
|
return issues;
|
|
214
232
|
}
|
|
215
233
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource-monitor degrade guard for mach dispatches.
|
|
3
|
+
*
|
|
4
|
+
* mozbuild's build resource monitor calls `psutil.virtual_memory()` at
|
|
5
|
+
* startup (`start_resource_recording`) and again later from
|
|
6
|
+
* `mozbuild.base._run_make`. On some hosts (psutil vs Darwin 27) those
|
|
7
|
+
* raise `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall failed`,
|
|
8
|
+
* and a `SystemResourceMonitor.__init__` that fails partway leaves an
|
|
9
|
+
* instance without `poll_interval`, so later polling dies with
|
|
10
|
+
* `AttributeError: ... poll_interval`. Any of these aborts `mach build` /
|
|
11
|
+
* `mach build faster` before the compiler ever runs.
|
|
12
|
+
*
|
|
13
|
+
* 0.33.0 injected a `sitecustomize.py` via the mach subprocess PYTHONPATH.
|
|
14
|
+
* Field report (0.34.0 cycle): mach re-execs itself into its private
|
|
15
|
+
* virtualenv and drops PYTHONPATH, so the env-var route never loads and
|
|
16
|
+
* the crash family escaped on every protected entry point. FireForge now
|
|
17
|
+
* owns the guard in-process on its mach dispatches by installing a
|
|
18
|
+
* `fireforge_mach_guard.pth` + module pair directly into every discovered
|
|
19
|
+
* mach virtualenv site-packages directory: `.pth` files execute at
|
|
20
|
+
* interpreter startup from the venv's own site-packages, which survives
|
|
21
|
+
* the re-exec. The PYTHONPATH `sitecustomize.py` is retained only as a
|
|
22
|
+
* belt-and-suspenders for the pre-venv bootstrap phase (first build, no
|
|
23
|
+
* `_virtualenvs` on disk yet).
|
|
24
|
+
*
|
|
25
|
+
* The guard itself covers the whole crash family:
|
|
26
|
+
* - wraps `psutil.virtual_memory` / `swap_memory` / `cpu_percent` /
|
|
27
|
+
* `cpu_times` / `disk_io_counters` module-wide, so every call site —
|
|
28
|
+
* including the direct `psutil.virtual_memory()` in
|
|
29
|
+
* `mozbuild.base._run_make` — degrades to a `UserWarning` and a zeroed
|
|
30
|
+
* reading instead of aborting;
|
|
31
|
+
* - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
|
|
32
|
+
* `poll_interval` is pre-populated before the real `__init__` runs, a
|
|
33
|
+
* failing `__init__` marks the instance degraded instead of raising,
|
|
34
|
+
* and every monitor method no-ops on a degraded instance — so a
|
|
35
|
+
* partially-constructed monitor can never surface the
|
|
36
|
+
* `AttributeError: poll_interval` variant.
|
|
37
|
+
*/
|
|
38
|
+
/** Result of installing the mach resource guard before a dispatch. */
|
|
39
|
+
export interface MachResourceGuardInstallation {
|
|
40
|
+
/**
|
|
41
|
+
* Env overlay for the mach subprocess: the PYTHONPATH sitecustomize
|
|
42
|
+
* fallback that covers the pre-venv bootstrap phase. Merged over
|
|
43
|
+
* `process.env` by the exec layer.
|
|
44
|
+
*/
|
|
45
|
+
env: Record<string, string>;
|
|
46
|
+
/** site-packages directories the in-venv guard was written into. */
|
|
47
|
+
sitePackagesDirs: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Installs the resource-monitor degrade guard for a mach dispatch:
|
|
51
|
+
* `fireforge_mach_guard.pth` + `fireforge_mach_guard.py` into every
|
|
52
|
+
* discovered mach virtualenv site-packages (survives mach's venv re-exec),
|
|
53
|
+
* plus the PYTHONPATH sitecustomize fallback for the pre-venv bootstrap
|
|
54
|
+
* phase. Idempotent and re-run before EVERY protected dispatch (including
|
|
55
|
+
* each retry) so a venv created by a crashed first attempt is guarded on
|
|
56
|
+
* the next one instead of re-dying on the same wedged state.
|
|
57
|
+
*/
|
|
58
|
+
export declare function installMachResourceGuard(engineDir: string): Promise<MachResourceGuardInstallation>;
|