@hominis/fireforge 0.13.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +20 -1
- package/dist/bin/fireforge.js +19 -5
- package/dist/src/commands/config.js +7 -1
- package/dist/src/commands/discard.js +6 -1
- package/dist/src/commands/doctor.d.ts +12 -0
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +106 -7
- package/dist/src/commands/export-shared.js +7 -0
- package/dist/src/commands/export.js +5 -0
- package/dist/src/commands/furnace/apply.js +147 -47
- package/dist/src/commands/furnace/create-templates.d.ts +26 -0
- package/dist/src/commands/furnace/create-templates.js +86 -0
- package/dist/src/commands/furnace/create.js +77 -103
- package/dist/src/commands/furnace/deploy.js +20 -5
- package/dist/src/commands/furnace/diff.js +3 -1
- package/dist/src/commands/furnace/init.js +25 -7
- package/dist/src/commands/furnace/list.js +15 -7
- package/dist/src/commands/furnace/override.js +47 -15
- package/dist/src/commands/furnace/remove.js +68 -20
- package/dist/src/commands/furnace/rename.js +31 -3
- package/dist/src/commands/furnace/scan.js +8 -0
- package/dist/src/commands/furnace/validate.js +70 -7
- package/dist/src/commands/import.js +65 -11
- package/dist/src/commands/re-export.js +11 -4
- package/dist/src/commands/rebase/abort.js +26 -14
- package/dist/src/commands/rebase/confirm.d.ts +15 -2
- package/dist/src/commands/rebase/confirm.js +2 -2
- package/dist/src/commands/rebase/continue.js +39 -15
- package/dist/src/commands/rebase/index.js +2 -1
- package/dist/src/commands/rebase/patch-loop.js +90 -33
- package/dist/src/commands/register.js +13 -0
- package/dist/src/commands/resolve.js +31 -10
- package/dist/src/commands/run.js +9 -44
- package/dist/src/commands/setup-support.js +25 -7
- package/dist/src/commands/status.js +59 -8
- package/dist/src/commands/test.js +33 -7
- package/dist/src/commands/token.js +11 -1
- package/dist/src/commands/watch.js +51 -1
- package/dist/src/commands/wire.js +23 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +47 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +33 -0
- package/dist/src/core/furnace-apply-ftl.js +102 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +10 -1
- package/dist/src/core/furnace-apply-helpers.js +16 -12
- package/dist/src/core/furnace-apply.js +7 -4
- package/dist/src/core/furnace-config-tokens.d.ts +11 -0
- package/dist/src/core/furnace-config-tokens.js +28 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +8 -1
- package/dist/src/core/furnace-constants.d.ts +20 -0
- package/dist/src/core/furnace-constants.js +32 -0
- package/dist/src/core/furnace-registration-ast.d.ts +13 -1
- package/dist/src/core/furnace-registration-ast.js +58 -25
- package/dist/src/core/furnace-registration.d.ts +28 -1
- package/dist/src/core/furnace-registration.js +98 -1
- package/dist/src/core/furnace-staleness.d.ts +17 -0
- package/dist/src/core/furnace-staleness.js +58 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-helpers.d.ts +8 -0
- package/dist/src/core/furnace-validate-helpers.js +81 -0
- package/dist/src/core/furnace-validate-registration.d.ts +8 -2
- package/dist/src/core/furnace-validate-registration.js +34 -9
- package/dist/src/core/furnace-validate.js +2 -2
- package/dist/src/core/marionette-preflight.d.ts +39 -0
- package/dist/src/core/marionette-preflight.js +210 -0
- package/dist/src/core/signal-critical.d.ts +49 -0
- package/dist/src/core/signal-critical.js +80 -0
- package/dist/src/errors/download.d.ts +1 -1
- package/dist/src/errors/download.js +6 -3
- package/dist/src/types/commands/options.d.ts +6 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +8 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `.ftl` apply/undeploy helpers for custom components. Extracted from
|
|
4
|
+
* `furnace-apply-helpers.ts` so the main helper module stays under the
|
|
5
|
+
* per-file LOC budget.
|
|
6
|
+
*
|
|
7
|
+
* Every helper here degrades gracefully: if the locale jar.mn is missing or
|
|
8
|
+
* the FTL tree is non-standard, apply logs a `stepError` rather than
|
|
9
|
+
* aborting the whole command. Missing jar.mn on a fork without a locale
|
|
10
|
+
* package should not block a working `.mjs`/`.css` from shipping.
|
|
11
|
+
*/
|
|
12
|
+
import { join, relative } from 'node:path';
|
|
13
|
+
import { toError } from '../utils/errors.js';
|
|
14
|
+
import { copyFile, pathExists } from '../utils/fs.js';
|
|
15
|
+
import { resolveFtlChromeSubPath, resolveFtlLocaleJarMnPath } from './furnace-constants.js';
|
|
16
|
+
import { addLocaleFtlJarMnEntry, removeLocaleFtlJarMnEntry } from './furnace-registration.js';
|
|
17
|
+
import { snapshotFile } from './furnace-rollback.js';
|
|
18
|
+
/**
|
|
19
|
+
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
20
|
+
* in the locale jar.mn.
|
|
21
|
+
*
|
|
22
|
+
* Failure modes (missing jar.mn, regex write error) are captured as
|
|
23
|
+
* stepErrors rather than thrown — a well-formed `.mjs`/`.css` must never be
|
|
24
|
+
* blocked by a broken locale path.
|
|
25
|
+
*/
|
|
26
|
+
export async function applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal) {
|
|
27
|
+
const ftlFile = `${name}.ftl`;
|
|
28
|
+
const ftlSrc = join(componentDir, ftlFile);
|
|
29
|
+
if (!(await pathExists(ftlSrc)))
|
|
30
|
+
return;
|
|
31
|
+
const ftlDest = join(engineDir, ftlDir, ftlFile);
|
|
32
|
+
if (rollbackJournal) {
|
|
33
|
+
await snapshotFile(rollbackJournal, ftlDest);
|
|
34
|
+
}
|
|
35
|
+
await copyFile(ftlSrc, ftlDest);
|
|
36
|
+
affectedPaths.push(relative(engineDir, ftlDest));
|
|
37
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
38
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
39
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
40
|
+
return;
|
|
41
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
42
|
+
if (!(await pathExists(localeJarAbs))) {
|
|
43
|
+
stepErrors.push({
|
|
44
|
+
step: 'locale jar.mn registration',
|
|
45
|
+
error: `Locale jar.mn not found at ${localeJarRel}; component "${name}" ships without a chrome URI for ${ftlFile}. Add the file manually or set furnace.json "ftlBasePath" to a tree that owns a jar.mn.`,
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (rollbackJournal) {
|
|
51
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
52
|
+
}
|
|
53
|
+
const inserted = await addLocaleFtlJarMnEntry(engineDir, localeJarRel, name, chromeSubPath);
|
|
54
|
+
if (inserted > 0) {
|
|
55
|
+
affectedPaths.push(localeJarRel);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
stepErrors.push({
|
|
60
|
+
step: 'locale jar.mn registration',
|
|
61
|
+
error: toError(error).message,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns a dry-run action for registering a locale jar.mn entry for the
|
|
67
|
+
* `.ftl` that `applyCustomFtlFile` would write. `undefined` when the FTL
|
|
68
|
+
* tree does not expose a locale jar.mn we can confidently name.
|
|
69
|
+
*/
|
|
70
|
+
export function describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile) {
|
|
71
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
72
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
73
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
return {
|
|
76
|
+
component: name,
|
|
77
|
+
action: 'register-jar',
|
|
78
|
+
description: `Register ${chromeSubPath}/${ftlFile} in ${localeJarRel}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Drops the locale jar.mn entry for `fileName` when it's a `.ftl` whose
|
|
83
|
+
* source workspace file has been deleted. Idempotent — absent entries are a
|
|
84
|
+
* no-op.
|
|
85
|
+
*/
|
|
86
|
+
export async function removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, rollbackJournal) {
|
|
87
|
+
if (!fileName.endsWith('.ftl'))
|
|
88
|
+
return;
|
|
89
|
+
const tagName = fileName.slice(0, -'.ftl'.length);
|
|
90
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
91
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
92
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
93
|
+
return;
|
|
94
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
95
|
+
if (!(await pathExists(localeJarAbs)))
|
|
96
|
+
return;
|
|
97
|
+
if (rollbackJournal) {
|
|
98
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
99
|
+
}
|
|
100
|
+
await removeLocaleFtlJarMnEntry(engineDir, localeJarRel, tagName, chromeSubPath);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=furnace-apply-ftl.js.map
|
|
@@ -90,8 +90,17 @@ export declare function hasOverrideEngineDrift(engineDir: string, componentDir:
|
|
|
90
90
|
* the validate command.
|
|
91
91
|
*/
|
|
92
92
|
export declare function hasCustomEngineDrift(root: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string): Promise<boolean>;
|
|
93
|
+
/** Extra knobs threaded into `applyCustomComponent` from the project config. */
|
|
94
|
+
export interface CustomApplyOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Trailing project marker appended to inserted `customElements.js` entries
|
|
97
|
+
* (e.g. `"HOMINIS"` emits ` // HOMINIS:` on each line). Mirrors the
|
|
98
|
+
* `markerComment` field in fireforge.json.
|
|
99
|
+
*/
|
|
100
|
+
markerComment?: string;
|
|
101
|
+
}
|
|
93
102
|
/** Applies a custom component into the engine tree and captures registration step errors. */
|
|
94
|
-
export declare function applyCustomComponent(engineDir: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string, dryRun?: boolean, rollbackJournal?: RollbackJournal): Promise<{
|
|
103
|
+
export declare function applyCustomComponent(engineDir: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string, dryRun?: boolean, rollbackJournal?: RollbackJournal, applyOptions?: CustomApplyOptions): Promise<{
|
|
95
104
|
affectedPaths: string[];
|
|
96
105
|
stepErrors: StepError[];
|
|
97
106
|
actions?: DryRunAction[];
|
|
@@ -6,6 +6,7 @@ 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 { applyCustomFtlFile, describeLocaleFtlJarMnRegistration, removeCustomFtlJarMnEntry, } from './furnace-apply-ftl.js';
|
|
9
10
|
import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
10
11
|
import { addCustomElementRegistration, addJarMnEntries, validateCustomElementRegistration, validateJarMnEntries, } from './furnace-registration.js';
|
|
11
12
|
import { recordCreatedDir, snapshotFile } from './furnace-rollback.js';
|
|
@@ -138,6 +139,10 @@ export async function undeployCustomFiles(engineDir, config, deletedFiles, ftlDi
|
|
|
138
139
|
await removeFile(enginePath);
|
|
139
140
|
removed.push(relative(engineDir, enginePath));
|
|
140
141
|
}
|
|
142
|
+
// When an `.ftl` is deleted from the workspace, the corresponding locale
|
|
143
|
+
// jar.mn entry must also be dropped — otherwise the chrome URI points at
|
|
144
|
+
// a missing file and runtime Fluent resolution breaks silently.
|
|
145
|
+
await removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, rollbackJournal);
|
|
141
146
|
}
|
|
142
147
|
return removed;
|
|
143
148
|
}
|
|
@@ -312,6 +317,10 @@ async function buildCustomDryRunActions(name, componentDir, engineDir, config, t
|
|
|
312
317
|
target: join(engineDir, ftlDir, ftlFile),
|
|
313
318
|
description: `Copy ${ftlFile} to ${ftlDir}`,
|
|
314
319
|
});
|
|
320
|
+
const localeAction = describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile);
|
|
321
|
+
if (localeAction) {
|
|
322
|
+
actions.push(localeAction);
|
|
323
|
+
}
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
if (config.register) {
|
|
@@ -353,7 +362,7 @@ async function buildCustomDryRunActions(name, componentDir, engineDir, config, t
|
|
|
353
362
|
return { actions, stepErrors };
|
|
354
363
|
}
|
|
355
364
|
/** Applies a custom component into the engine tree and captures registration step errors. */
|
|
356
|
-
export async function applyCustomComponent(engineDir, name, componentDir, config, ftlDir, dryRun = false, rollbackJournal) {
|
|
365
|
+
export async function applyCustomComponent(engineDir, name, componentDir, config, ftlDir, dryRun = false, rollbackJournal, applyOptions = {}) {
|
|
357
366
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
358
367
|
throw new FurnaceError(`Invalid component name "${name}": must match /^[a-z][a-z0-9-]*$/`);
|
|
359
368
|
}
|
|
@@ -394,16 +403,7 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
|
|
|
394
403
|
copiedFileNames.push(entry.name);
|
|
395
404
|
}));
|
|
396
405
|
if (config.localized) {
|
|
397
|
-
|
|
398
|
-
const ftlSrc = join(componentDir, ftlFile);
|
|
399
|
-
if (await pathExists(ftlSrc)) {
|
|
400
|
-
const ftlDest = join(engineDir, ftlDir, ftlFile);
|
|
401
|
-
if (rollbackJournal) {
|
|
402
|
-
await snapshotFile(rollbackJournal, ftlDest);
|
|
403
|
-
}
|
|
404
|
-
await copyFile(ftlSrc, ftlDest);
|
|
405
|
-
affectedPaths.push(relative(engineDir, ftlDest));
|
|
406
|
-
}
|
|
406
|
+
await applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal);
|
|
407
407
|
}
|
|
408
408
|
if (config.register) {
|
|
409
409
|
try {
|
|
@@ -411,7 +411,11 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
|
|
|
411
411
|
if (rollbackJournal) {
|
|
412
412
|
await snapshotFile(rollbackJournal, join(engineDir, CUSTOM_ELEMENTS_JS));
|
|
413
413
|
}
|
|
414
|
-
await addCustomElementRegistration(engineDir, name, modulePath
|
|
414
|
+
await addCustomElementRegistration(engineDir, name, modulePath, {
|
|
415
|
+
...(applyOptions.markerComment !== undefined
|
|
416
|
+
? { markerComment: applyOptions.markerComment }
|
|
417
|
+
: {}),
|
|
418
|
+
});
|
|
415
419
|
affectedPaths.push(CUSTOM_ELEMENTS_JS);
|
|
416
420
|
}
|
|
417
421
|
catch (error) {
|
|
@@ -4,7 +4,7 @@ import { FurnaceError } from '../errors/furnace.js';
|
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
5
|
import { pathExists } from '../utils/fs.js';
|
|
6
6
|
import { info } from '../utils/logger.js';
|
|
7
|
-
import { getProjectPaths } from './config.js';
|
|
7
|
+
import { getProjectPaths, loadConfig } from './config.js';
|
|
8
8
|
import { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, diffDeletedFiles, extractComponentChecksums, getOverrideEngineTargetPath, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, undeployCustomFiles, undeployOverrideFiles, } from './furnace-apply-helpers.js';
|
|
9
9
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, } from './furnace-config.js';
|
|
10
10
|
import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from './furnace-constants.js';
|
|
@@ -161,7 +161,7 @@ async function reconcileCustomRegistrationAfterUndeploy(engineDir, name, config,
|
|
|
161
161
|
filesAffected.push(CUSTOM_ELEMENTS_JS);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
async function applyCustomBatch(root, config, furnacePaths, state, engineDir, ftlDir, dryRun, result, allActions, newChecksums, rollbackJournal, componentName) {
|
|
164
|
+
async function applyCustomBatch(root, config, furnacePaths, state, engineDir, ftlDir, dryRun, result, allActions, newChecksums, rollbackJournal, componentName, markerComment) {
|
|
165
165
|
const allKnown = new Set([
|
|
166
166
|
...config.stock,
|
|
167
167
|
...Object.keys(config.overrides),
|
|
@@ -246,7 +246,7 @@ async function applyCustomBatch(root, config, furnacePaths, state, engineDir, ft
|
|
|
246
246
|
const removed = await undeployCustomFiles(engineDir, customConfig, deletedFiles, ftlDir, rollbackJournal);
|
|
247
247
|
filesAffectedTotal.push(...removed);
|
|
248
248
|
}
|
|
249
|
-
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, dryRun, rollbackJournal);
|
|
249
|
+
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, dryRun, rollbackJournal, markerComment !== undefined ? { markerComment } : {});
|
|
250
250
|
if (dryRun && actions) {
|
|
251
251
|
allActions.push(...actions);
|
|
252
252
|
}
|
|
@@ -304,6 +304,9 @@ export async function applyAllComponents(root, dryRun = false, options) {
|
|
|
304
304
|
const { engine: engineDir } = getProjectPaths(root);
|
|
305
305
|
const furnacePaths = getFurnacePaths(root);
|
|
306
306
|
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
307
|
+
const markerComment = await loadConfig(root)
|
|
308
|
+
.then((forgeConfig) => forgeConfig.markerComment)
|
|
309
|
+
.catch(() => undefined);
|
|
307
310
|
if (!(await pathExists(engineDir))) {
|
|
308
311
|
throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
|
|
309
312
|
}
|
|
@@ -328,7 +331,7 @@ export async function applyAllComponents(root, dryRun = false, options) {
|
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
333
|
await applyOverrideBatch(config, furnacePaths, state, engineDir, ftlDir, dryRun, result, allActions, newChecksums, rollbackJournal, componentName);
|
|
331
|
-
await applyCustomBatch(root, config, furnacePaths, state, engineDir, ftlDir, dryRun, result, allActions, newChecksums, rollbackJournal, componentName);
|
|
334
|
+
await applyCustomBatch(root, config, furnacePaths, state, engineDir, ftlDir, dryRun, result, allActions, newChecksums, rollbackJournal, componentName, markerComment);
|
|
332
335
|
// Check for any partial failures (step errors on applied components).
|
|
333
336
|
const hasStepErrors = result.applied.some((entry) => 'stepErrors' in entry && entry.stepErrors.length > 0);
|
|
334
337
|
// Orphaned components are implicitly cleaned up: newChecksums only
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small validation helpers for furnace.json token-related fields. Extracted
|
|
3
|
+
* from `furnace-config.ts` so the main config module stays under the per-file
|
|
4
|
+
* LOC budget.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validates a `tokenHostDocuments` raw value. Each entry must be a non-empty
|
|
8
|
+
* relative path contained in the engine tree. Throws `FurnaceError` on
|
|
9
|
+
* violation; does nothing for `undefined` (field is optional).
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateTokenHostDocuments(raw: unknown): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Small validation helpers for furnace.json token-related fields. Extracted
|
|
4
|
+
* from `furnace-config.ts` so the main config module stays under the per-file
|
|
5
|
+
* LOC budget.
|
|
6
|
+
*/
|
|
7
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
8
|
+
import { isContainedRelativePath } from '../utils/paths.js';
|
|
9
|
+
import { parseStringArray } from './furnace-config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Validates a `tokenHostDocuments` raw value. Each entry must be a non-empty
|
|
12
|
+
* relative path contained in the engine tree. Throws `FurnaceError` on
|
|
13
|
+
* violation; does nothing for `undefined` (field is optional).
|
|
14
|
+
*/
|
|
15
|
+
export function validateTokenHostDocuments(raw) {
|
|
16
|
+
if (raw === undefined)
|
|
17
|
+
return;
|
|
18
|
+
const docs = parseStringArray(raw, 'tokenHostDocuments');
|
|
19
|
+
for (const doc of docs) {
|
|
20
|
+
if (doc.trim() === '') {
|
|
21
|
+
throw new FurnaceError('Furnace config: "tokenHostDocuments" entries must be non-empty strings');
|
|
22
|
+
}
|
|
23
|
+
if (!isContainedRelativePath(doc)) {
|
|
24
|
+
throw new FurnaceError(`Furnace config: "tokenHostDocuments" entry "${doc}" must stay within the engine tree (no absolute paths, no "..")`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=furnace-config-tokens.js.map
|
|
@@ -38,6 +38,12 @@ export declare function getFurnacePaths(root: string): FurnacePaths;
|
|
|
38
38
|
* @returns True if furnace.json exists
|
|
39
39
|
*/
|
|
40
40
|
export declare function furnaceConfigExists(root: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Validates an override component config object.
|
|
43
|
+
* @param data - Raw data to validate
|
|
44
|
+
* @param name - Component name for error messages
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseStringArray(value: unknown, fieldName: string): string[];
|
|
41
47
|
/**
|
|
42
48
|
* Migrates a furnace config from an older schema version to the current one.
|
|
43
49
|
* Returns the data unchanged if it is already at the current version.
|
|
@@ -7,6 +7,7 @@ import { warn } from '../utils/logger.js';
|
|
|
7
7
|
import { isExplicitAbsolutePath } from '../utils/paths.js';
|
|
8
8
|
import { isArray, isBoolean, isObject, isString } from '../utils/validation.js';
|
|
9
9
|
import { FIREFORGE_DIR } from './config.js';
|
|
10
|
+
import { validateTokenHostDocuments } from './furnace-config-tokens.js';
|
|
10
11
|
import { resolveFtlDir } from './furnace-constants.js';
|
|
11
12
|
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
12
13
|
import { quarantineStateFile, withStateFileLock } from './state-file.js';
|
|
@@ -50,7 +51,7 @@ export async function furnaceConfigExists(root) {
|
|
|
50
51
|
* @param data - Raw data to validate
|
|
51
52
|
* @param name - Component name for error messages
|
|
52
53
|
*/
|
|
53
|
-
function parseStringArray(value, fieldName) {
|
|
54
|
+
export function parseStringArray(value, fieldName) {
|
|
54
55
|
if (!isArray(value)) {
|
|
55
56
|
throw new FurnaceError(`Furnace config: "${fieldName}" must be an array`);
|
|
56
57
|
}
|
|
@@ -182,6 +183,9 @@ export function validateFurnaceConfig(data) {
|
|
|
182
183
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
183
184
|
parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
184
185
|
}
|
|
186
|
+
// Validate optional tokenHostDocuments — list of chrome XHTMLs that the
|
|
187
|
+
// `missing-token-link` validator scans for the tokens CSS link.
|
|
188
|
+
validateTokenHostDocuments(migrated['tokenHostDocuments']);
|
|
185
189
|
const stock = parseStringArray(migrated['stock'], 'stock');
|
|
186
190
|
const stockSet = new Set();
|
|
187
191
|
for (const name of stock) {
|
|
@@ -238,6 +242,9 @@ export function validateFurnaceConfig(data) {
|
|
|
238
242
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
239
243
|
config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
240
244
|
}
|
|
245
|
+
if (migrated['tokenHostDocuments'] !== undefined) {
|
|
246
|
+
config.tokenHostDocuments = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
|
|
247
|
+
}
|
|
241
248
|
// Validate optional ftlBasePath
|
|
242
249
|
if (migrated['ftlBasePath'] !== undefined) {
|
|
243
250
|
if (!isString(migrated['ftlBasePath'])) {
|
|
@@ -13,6 +13,26 @@ export declare function isComponentSourceFile(fileName: string): boolean;
|
|
|
13
13
|
* `furnace.json` over the built-in default.
|
|
14
14
|
*/
|
|
15
15
|
export declare function resolveFtlDir(configuredPath?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the chrome sub-path that `document.l10n` / `insertFTLIfNeeded`
|
|
18
|
+
* expects for a given `ftlBasePath`. Strips the mandatory `locales/<LOCALE>/`
|
|
19
|
+
* segment. For the default `toolkit/locales/en-US/toolkit/global` this yields
|
|
20
|
+
* `toolkit/global`.
|
|
21
|
+
*
|
|
22
|
+
* Returns `undefined` when no `locales/<LOCALE>/` segment is present. Callers
|
|
23
|
+
* must treat that as "cannot confidently locate the locale jar.mn entry" and
|
|
24
|
+
* degrade gracefully rather than inventing a path.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveFtlChromeSubPath(ftlBasePath?: string): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the engine-relative locale jar.mn that owns the FTL tree for a
|
|
29
|
+
* given `ftlBasePath`. For the default toolkit tree this yields
|
|
30
|
+
* `toolkit/locales/jar.mn`.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when the path does not contain a `locales/` segment —
|
|
33
|
+
* callers must treat that as "cannot locate" and degrade gracefully.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveFtlLocaleJarMnPath(ftlBasePath?: string): string | undefined;
|
|
16
36
|
/**
|
|
17
37
|
* Converts a kebab-case tag name to PascalCase class name.
|
|
18
38
|
* e.g. "moz-sidebar-panel" → "MozSidebarPanel"
|
|
@@ -18,6 +18,38 @@ export function isComponentSourceFile(fileName) {
|
|
|
18
18
|
export function resolveFtlDir(configuredPath) {
|
|
19
19
|
return configuredPath ?? FTL_DIR;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the chrome sub-path that `document.l10n` / `insertFTLIfNeeded`
|
|
23
|
+
* expects for a given `ftlBasePath`. Strips the mandatory `locales/<LOCALE>/`
|
|
24
|
+
* segment. For the default `toolkit/locales/en-US/toolkit/global` this yields
|
|
25
|
+
* `toolkit/global`.
|
|
26
|
+
*
|
|
27
|
+
* Returns `undefined` when no `locales/<LOCALE>/` segment is present. Callers
|
|
28
|
+
* must treat that as "cannot confidently locate the locale jar.mn entry" and
|
|
29
|
+
* degrade gracefully rather than inventing a path.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveFtlChromeSubPath(ftlBasePath) {
|
|
32
|
+
const path = (ftlBasePath ?? FTL_DIR).replace(/\\/g, '/');
|
|
33
|
+
const match = /^(.*?)\/locales\/[^/]+\/(.+?)\/?$/.exec(path);
|
|
34
|
+
if (!match?.[2])
|
|
35
|
+
return undefined;
|
|
36
|
+
return match[2];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns the engine-relative locale jar.mn that owns the FTL tree for a
|
|
40
|
+
* given `ftlBasePath`. For the default toolkit tree this yields
|
|
41
|
+
* `toolkit/locales/jar.mn`.
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` when the path does not contain a `locales/` segment —
|
|
44
|
+
* callers must treat that as "cannot locate" and degrade gracefully.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveFtlLocaleJarMnPath(ftlBasePath) {
|
|
47
|
+
const path = (ftlBasePath ?? FTL_DIR).replace(/\\/g, '/');
|
|
48
|
+
const match = /^(.*?)\/locales\/[^/]+\//.exec(path);
|
|
49
|
+
if (!match?.[1])
|
|
50
|
+
return undefined;
|
|
51
|
+
return `${match[1]}/locales/jar.mn`;
|
|
52
|
+
}
|
|
21
53
|
/**
|
|
22
54
|
* Converts a kebab-case tag name to PascalCase class name.
|
|
23
55
|
* e.g. "moz-sidebar-panel" → "MozSidebarPanel"
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
* AST-based custom element registration updates for customElements.js.
|
|
3
3
|
* Removal logic is in furnace-registration-remove.ts.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Options that shape the lines `addCustomElementRegistration` writes into
|
|
7
|
+
* `customElements.js`. Kept optional so existing call sites keep working.
|
|
8
|
+
*/
|
|
9
|
+
export interface RegistrationWriteOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Trailing project marker appended to every inserted line (e.g. `"HOMINIS"`
|
|
12
|
+
* produces ` // HOMINIS:` at end-of-line). Keeps modifications discoverable
|
|
13
|
+
* without requiring the operator to hand-tag them post-apply.
|
|
14
|
+
*/
|
|
15
|
+
markerComment?: string;
|
|
16
|
+
}
|
|
5
17
|
export { removeCustomElementRegistration } from './furnace-registration-remove.js';
|
|
6
18
|
export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
7
19
|
/**
|
|
@@ -21,7 +33,7 @@ export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
|
21
33
|
* @param tagName - Custom element tag name
|
|
22
34
|
* @param modulePath - chrome:// URI for the module
|
|
23
35
|
*/
|
|
24
|
-
export declare function addCustomElementRegistration(engineDir: string, tagName: string, modulePath: string): Promise<void>;
|
|
36
|
+
export declare function addCustomElementRegistration(engineDir: string, tagName: string, modulePath: string, options?: RegistrationWriteOptions): Promise<void>;
|
|
25
37
|
/**
|
|
26
38
|
* Validates that a custom element registration *would* succeed without
|
|
27
39
|
* writing anything. Used by dry-run to surface registration errors early.
|
|
@@ -9,9 +9,43 @@ import { FurnaceError } from '../errors/furnace.js';
|
|
|
9
9
|
import { toError } from '../utils/errors.js';
|
|
10
10
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
11
11
|
import { verbose, warn } from '../utils/logger.js';
|
|
12
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
12
13
|
import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
|
|
13
14
|
import { CUSTOM_ELEMENTS_JS } from './furnace-constants.js';
|
|
14
15
|
import { validateRegistrationPlacement, validateTagName } from './furnace-registration-validate.js';
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when `content` already contains a registration for `tagName`.
|
|
18
|
+
*
|
|
19
|
+
* Tolerates trailing line comments (e.g. a project marker like `// HOMINIS:`)
|
|
20
|
+
* that an operator may have appended to entries written by a previous apply.
|
|
21
|
+
* Without this, a re-apply would insert a duplicate entry, and the second
|
|
22
|
+
* `setElementCreationCallback` at window-load would throw `NotSupportedError`.
|
|
23
|
+
*
|
|
24
|
+
* Matches any of:
|
|
25
|
+
* 1. `setElementCreationCallback("tag"` / `setElementCreationCallback('tag'`
|
|
26
|
+
* 2. Single-line array entry: `["tag", "..."]` — column 0 only, comments allowed after
|
|
27
|
+
* 3. Multi-line array entry: `"tag",` on its own line, optional trailing `//` comment
|
|
28
|
+
*/
|
|
29
|
+
function isTagAlreadyRegistered(content, tagName) {
|
|
30
|
+
const escaped = escapeRegex(tagName);
|
|
31
|
+
if (content.includes(`setElementCreationCallback("${tagName}"`) ||
|
|
32
|
+
content.includes(`setElementCreationCallback('${tagName}'`)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (new RegExp(`^\\s*\\[\\s*["']${escaped}["']\\s*,`, 'm').test(content)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (new RegExp(`^\\s*["']${escaped}["']\\s*,\\s*(?:\\/\\/.*)?$`, 'm').test(content)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/** Validates a markerComment value and returns the formatted suffix (with leading spaces). */
|
|
44
|
+
function formatMarkerSuffix(markerComment) {
|
|
45
|
+
if (!markerComment)
|
|
46
|
+
return '';
|
|
47
|
+
return ` // ${markerComment}:`;
|
|
48
|
+
}
|
|
15
49
|
// Re-export from split modules so existing import sites continue working
|
|
16
50
|
export { removeCustomElementRegistration } from './furnace-registration-remove.js';
|
|
17
51
|
// Re-export constants so existing import sites continue working
|
|
@@ -62,23 +96,27 @@ function selectRegistrationTarget(targets, isESModule, tagName) {
|
|
|
62
96
|
}
|
|
63
97
|
throw new FurnaceError(`${tagName} would land in the DOMContentLoaded/importESModule block (Pattern B) instead of the loadSubScript block (Pattern A) — no non-DOMContentLoaded registration array found in customElements.js. The file structure may have changed upstream — manual intervention required.`, tagName);
|
|
64
98
|
}
|
|
65
|
-
function buildRegistrationEntry(referenceEntry, tagName, modulePath) {
|
|
99
|
+
function buildRegistrationEntry(referenceEntry, tagName, modulePath, markerComment) {
|
|
100
|
+
const marker = formatMarkerSuffix(markerComment);
|
|
66
101
|
if (!referenceEntry) {
|
|
67
|
-
return ` ["${tagName}", "${modulePath}"]
|
|
102
|
+
return ` ["${tagName}", "${modulePath}"],${marker}`;
|
|
68
103
|
}
|
|
69
104
|
if (referenceEntry.isMultiLine) {
|
|
70
105
|
const indent = referenceEntry.indent;
|
|
71
106
|
const inner = referenceEntry.innerIndent ?? indent + ' ';
|
|
72
|
-
return `${indent}[
|
|
107
|
+
return (`${indent}[${marker}\n` +
|
|
108
|
+
`${inner}"${tagName}",${marker}\n` +
|
|
109
|
+
`${inner}"${modulePath}",${marker}\n` +
|
|
110
|
+
`${indent}],${marker}`);
|
|
73
111
|
}
|
|
74
|
-
return `${referenceEntry.indent}["${tagName}", "${modulePath}"]
|
|
112
|
+
return `${referenceEntry.indent}["${tagName}", "${modulePath}"],${marker}`;
|
|
75
113
|
}
|
|
76
114
|
/**
|
|
77
115
|
* AST-based implementation: parses customElements.js, walks to find the
|
|
78
116
|
* target ForOfStatement array, and inserts the new entry at the correct
|
|
79
117
|
* alphabetical position using magic-string.
|
|
80
118
|
*/
|
|
81
|
-
function addRegistrationAST(content, tagName, modulePath, isESModule) {
|
|
119
|
+
function addRegistrationAST(content, tagName, modulePath, isESModule, markerComment) {
|
|
82
120
|
validateTagName(tagName);
|
|
83
121
|
const ast = parseScript(content);
|
|
84
122
|
const ancestors = [];
|
|
@@ -141,7 +179,7 @@ function addRegistrationAST(content, tagName, modulePath, isESModule) {
|
|
|
141
179
|
referenceEntry = entry;
|
|
142
180
|
}
|
|
143
181
|
// Build new entry string matching detected format
|
|
144
|
-
const newEntry = buildRegistrationEntry(referenceEntry, tagName, modulePath);
|
|
182
|
+
const newEntry = buildRegistrationEntry(referenceEntry, tagName, modulePath, markerComment);
|
|
145
183
|
const ms = new MagicString(content);
|
|
146
184
|
// Helper: find the start-of-line position for a given offset
|
|
147
185
|
function lineStart(pos) {
|
|
@@ -181,7 +219,7 @@ function addRegistrationAST(content, tagName, modulePath, isESModule) {
|
|
|
181
219
|
* validate indentation or multi-line format — but it is robust against
|
|
182
220
|
* upstream syntax changes that break the parser.
|
|
183
221
|
*/
|
|
184
|
-
function addRegistrationRegexFallback(content, tagName, modulePath, isESModule) {
|
|
222
|
+
function addRegistrationRegexFallback(content, tagName, modulePath, isESModule, markerComment) {
|
|
185
223
|
// Find all registration entries: ["tag", "path"],
|
|
186
224
|
const entryPattern = /^(\s*)\["([^"]+)",\s*"[^"]+"\],?\s*$/gm;
|
|
187
225
|
let lastMatch = null;
|
|
@@ -212,7 +250,8 @@ function addRegistrationRegexFallback(content, tagName, modulePath, isESModule)
|
|
|
212
250
|
throw new FurnaceError(`Regex fallback could not find any registration entries in the ${isESModule ? 'DOMContentLoaded' : 'non-DOMContentLoaded'} block of ${CUSTOM_ELEMENTS_JS}.`, tagName);
|
|
213
251
|
}
|
|
214
252
|
const indent = lastMatch[1] ?? ' ';
|
|
215
|
-
const
|
|
253
|
+
const marker = formatMarkerSuffix(markerComment);
|
|
254
|
+
const newEntry = `${indent}["${tagName}", "${modulePath}"],${marker}`;
|
|
216
255
|
if (insertAfterMatch) {
|
|
217
256
|
// Insert after the last entry that sorts before tagName
|
|
218
257
|
const insertPos = insertAfterMatch.index + insertAfterMatch[0].length;
|
|
@@ -243,20 +282,18 @@ function addRegistrationRegexFallback(content, tagName, modulePath, isESModule)
|
|
|
243
282
|
* @param tagName - Custom element tag name
|
|
244
283
|
* @param modulePath - chrome:// URI for the module
|
|
245
284
|
*/
|
|
246
|
-
export async function addCustomElementRegistration(engineDir, tagName, modulePath) {
|
|
285
|
+
export async function addCustomElementRegistration(engineDir, tagName, modulePath, options = {}) {
|
|
247
286
|
const filePath = join(engineDir, CUSTOM_ELEMENTS_JS);
|
|
248
287
|
if (!(await pathExists(filePath))) {
|
|
249
288
|
throw new FurnaceError('customElements.js not found in engine', tagName);
|
|
250
289
|
}
|
|
251
290
|
const content = await readText(filePath);
|
|
252
|
-
// Idempotency:
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
content.includes(`['${tagName}',`) ||
|
|
259
|
-
new RegExp(`^\\s*["']${tagName}["'],\\s*$`, 'm').test(content)) {
|
|
291
|
+
// Idempotency: check column-0 of each array entry rather than a literal
|
|
292
|
+
// substring match. A previous apply may have written this entry with
|
|
293
|
+
// trailing marker comments (see `options.markerComment`); matching on the
|
|
294
|
+
// full line would then miss it and insert a duplicate on re-apply, which
|
|
295
|
+
// throws NotSupportedError at every window-load.
|
|
296
|
+
if (isTagAlreadyRegistered(content, tagName)) {
|
|
260
297
|
return;
|
|
261
298
|
}
|
|
262
299
|
// Validate upfront — tag name errors must not fall through to the regex fallback.
|
|
@@ -279,7 +316,7 @@ export async function addCustomElementRegistration(engineDir, tagName, modulePat
|
|
|
279
316
|
}
|
|
280
317
|
let nextContent;
|
|
281
318
|
try {
|
|
282
|
-
nextContent = addRegistrationAST(content, tagName, modulePath, isESModule);
|
|
319
|
+
nextContent = addRegistrationAST(content, tagName, modulePath, isESModule, options.markerComment);
|
|
283
320
|
}
|
|
284
321
|
catch (error) {
|
|
285
322
|
if (error instanceof FurnaceError) {
|
|
@@ -287,7 +324,7 @@ export async function addCustomElementRegistration(engineDir, tagName, modulePat
|
|
|
287
324
|
warn(`AST-based registration failed for ${tagName}: ${error.message}. ` +
|
|
288
325
|
'Falling back to regex-based insertion. Please report this so the AST parser can be updated.');
|
|
289
326
|
try {
|
|
290
|
-
nextContent = addRegistrationRegexFallback(content, tagName, modulePath, isESModule);
|
|
327
|
+
nextContent = addRegistrationRegexFallback(content, tagName, modulePath, isESModule, options.markerComment);
|
|
291
328
|
verbose(`Regex fallback succeeded for ${tagName}. The registration may be less precise than the AST approach.`);
|
|
292
329
|
}
|
|
293
330
|
catch {
|
|
@@ -300,7 +337,7 @@ export async function addCustomElementRegistration(engineDir, tagName, modulePat
|
|
|
300
337
|
warn(`AST parser threw an unexpected error for ${tagName}: ${parserError.message}. ` +
|
|
301
338
|
'Falling back to regex-based insertion.');
|
|
302
339
|
try {
|
|
303
|
-
nextContent = addRegistrationRegexFallback(content, tagName, modulePath, isESModule);
|
|
340
|
+
nextContent = addRegistrationRegexFallback(content, tagName, modulePath, isESModule, options.markerComment);
|
|
304
341
|
verbose(`Regex fallback succeeded for ${tagName}. The registration may be less precise than the AST approach.`);
|
|
305
342
|
}
|
|
306
343
|
catch {
|
|
@@ -321,11 +358,7 @@ export async function validateCustomElementRegistration(engineDir, tagName, modu
|
|
|
321
358
|
throw new FurnaceError('customElements.js not found in engine', tagName);
|
|
322
359
|
}
|
|
323
360
|
const content = await readText(filePath);
|
|
324
|
-
if (content
|
|
325
|
-
content.includes(`setElementCreationCallback('${tagName}'`) ||
|
|
326
|
-
content.includes(`["${tagName}",`) ||
|
|
327
|
-
content.includes(`['${tagName}',`) ||
|
|
328
|
-
new RegExp(`^\\s*["']${tagName}["'],\\s*$`, 'm').test(content)) {
|
|
361
|
+
if (isTagAlreadyRegistered(content, tagName)) {
|
|
329
362
|
return;
|
|
330
363
|
}
|
|
331
364
|
const isESModule = modulePath.endsWith('.mjs');
|
|
@@ -34,7 +34,34 @@ export { addCustomElementRegistration, removeCustomElementRegistration, validate
|
|
|
34
34
|
* @param tagName - Custom element tag name
|
|
35
35
|
* @param files - Filenames to register (e.g. ["moz-widget.mjs", "moz-widget.css"])
|
|
36
36
|
*/
|
|
37
|
-
export declare function addJarMnEntries(engineDir: string, tagName: string, files: string[]): Promise<
|
|
37
|
+
export declare function addJarMnEntries(engineDir: string, tagName: string, files: string[]): Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* Adds a locale jar.mn entry mapping `<chromeSubPath>/<tagName>.ftl` to the
|
|
40
|
+
* on-disk `.ftl` that `furnace apply` just copied under the FTL tree. Without
|
|
41
|
+
* this entry the chrome URI passed to `window.MozXULElement.insertFTLIfNeeded`
|
|
42
|
+
* does not resolve at runtime, so the generated `--localized` component
|
|
43
|
+
* silently ships broken l10n.
|
|
44
|
+
*
|
|
45
|
+
* Degrades gracefully — if the locale jar.mn (e.g. `toolkit/locales/jar.mn`)
|
|
46
|
+
* does not exist, returns 0 rather than throwing, so a custom fork without a
|
|
47
|
+
* standard locales package can still apply a localized component.
|
|
48
|
+
*
|
|
49
|
+
* The written entry mirrors the Mozilla convention for toolkit widgets:
|
|
50
|
+
*
|
|
51
|
+
* locale/@AB_CD@/<chromeSubPath>/<tagName>.ftl (%<chromeSubPath>/<tagName>.ftl)
|
|
52
|
+
*
|
|
53
|
+
* @param engineDir - Path to the Firefox engine source root
|
|
54
|
+
* @param jarMnRelPath - Engine-relative path to the locale jar.mn
|
|
55
|
+
* @param tagName - Custom element tag name (base of the `.ftl` file)
|
|
56
|
+
* @param chromeSubPath - Chrome sub-path (e.g. `toolkit/global`)
|
|
57
|
+
* @returns Number of entries inserted (0 when already present, or jar.mn missing)
|
|
58
|
+
*/
|
|
59
|
+
export declare function addLocaleFtlJarMnEntry(engineDir: string, jarMnRelPath: string, tagName: string, chromeSubPath: string): Promise<number>;
|
|
60
|
+
/**
|
|
61
|
+
* Removes a locale jar.mn entry previously written by `addLocaleFtlJarMnEntry`.
|
|
62
|
+
* Idempotent — if the entry is absent or the file is missing, nothing happens.
|
|
63
|
+
*/
|
|
64
|
+
export declare function removeLocaleFtlJarMnEntry(engineDir: string, jarMnRelPath: string, tagName: string, chromeSubPath: string): Promise<void>;
|
|
38
65
|
/**
|
|
39
66
|
* Removes all jar.mn entries for a given tag name.
|
|
40
67
|
*
|