@hominis/fireforge 0.14.0 → 0.15.2
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 +44 -1
- package/README.md +41 -3
- package/dist/src/commands/build.js +12 -1
- package/dist/src/commands/furnace/create-templates.d.ts +47 -0
- package/dist/src/commands/furnace/create-templates.js +135 -0
- package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
- package/dist/src/commands/furnace/create-xpcshell.js +53 -0
- package/dist/src/commands/furnace/create.js +81 -109
- package/dist/src/commands/furnace/deploy.js +3 -3
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/commands/test.js +20 -0
- package/dist/src/core/build-prepare.js +6 -1
- 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 +32 -0
- 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 +17 -0
- package/dist/src/core/furnace-config-tokens.js +43 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +16 -3
- 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 +27 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +39 -1
- package/dist/src/core/furnace-validate-helpers.js +182 -18
- 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 +46 -0
- package/dist/src/core/marionette-preflight.js +260 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +1 -1
- package/dist/src/core/patch-lint.js +29 -9
- package/dist/src/types/commands/options.d.ts +16 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +19 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
|
@@ -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,17 @@
|
|
|
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;
|
|
12
|
+
/**
|
|
13
|
+
* Validates a `runtimeVariables` raw value. Each entry must start with `--`
|
|
14
|
+
* (it is a CSS custom property name). Throws `FurnaceError` on violation;
|
|
15
|
+
* does nothing for `undefined` (field is optional).
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateRuntimeVariables(raw: unknown): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
/**
|
|
29
|
+
* Validates a `runtimeVariables` raw value. Each entry must start with `--`
|
|
30
|
+
* (it is a CSS custom property name). Throws `FurnaceError` on violation;
|
|
31
|
+
* does nothing for `undefined` (field is optional).
|
|
32
|
+
*/
|
|
33
|
+
export function validateRuntimeVariables(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const vars = parseStringArray(raw, 'runtimeVariables');
|
|
37
|
+
for (const name of vars) {
|
|
38
|
+
if (!name.startsWith('--')) {
|
|
39
|
+
throw new FurnaceError(`Furnace config: "runtimeVariables" entries must start with "--" (got ${JSON.stringify(name)})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# 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 { validateRuntimeVariables, 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,12 @@ export function validateFurnaceConfig(data) {
|
|
|
182
183
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
183
184
|
parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
184
185
|
}
|
|
186
|
+
// Validate optional runtimeVariables — CSS runtime state channels
|
|
187
|
+
// (e.g. `--cam-x`) that are exempt from `token-prefix-violation`.
|
|
188
|
+
validateRuntimeVariables(migrated['runtimeVariables']);
|
|
189
|
+
// Validate optional tokenHostDocuments — list of chrome XHTMLs that the
|
|
190
|
+
// `missing-token-link` validator scans for the tokens CSS link.
|
|
191
|
+
validateTokenHostDocuments(migrated['tokenHostDocuments']);
|
|
185
192
|
const stock = parseStringArray(migrated['stock'], 'stock');
|
|
186
193
|
const stockSet = new Set();
|
|
187
194
|
for (const name of stock) {
|
|
@@ -232,12 +239,18 @@ export function validateFurnaceConfig(data) {
|
|
|
232
239
|
overrides,
|
|
233
240
|
custom,
|
|
234
241
|
};
|
|
235
|
-
if (migrated['tokenPrefix'] !== undefined)
|
|
242
|
+
if (migrated['tokenPrefix'] !== undefined)
|
|
236
243
|
config.tokenPrefix = migrated['tokenPrefix'];
|
|
237
|
-
}
|
|
238
244
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
239
245
|
config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
240
246
|
}
|
|
247
|
+
if (migrated['runtimeVariables'] !== undefined) {
|
|
248
|
+
config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
|
|
249
|
+
}
|
|
250
|
+
if (migrated['tokenHostDocuments'] !== undefined) {
|
|
251
|
+
const docs = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
|
|
252
|
+
config.tokenHostDocuments = docs;
|
|
253
|
+
}
|
|
241
254
|
// Validate optional ftlBasePath
|
|
242
255
|
if (migrated['ftlBasePath'] !== undefined) {
|
|
243
256
|
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. `"MYBROWSER"`
|
|
12
|
+
* produces ` // MYBROWSER:` 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 `// MYBROWSER:`)
|
|
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');
|
|
@@ -35,6 +35,33 @@ export { addCustomElementRegistration, removeCustomElementRegistration, validate
|
|
|
35
35
|
* @param files - Filenames to register (e.g. ["moz-widget.mjs", "moz-widget.css"])
|
|
36
36
|
*/
|
|
37
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
|
*
|
|
@@ -113,6 +113,102 @@ export async function addJarMnEntries(engineDir, tagName, files) {
|
|
|
113
113
|
await writeText(filePath, content);
|
|
114
114
|
return newFiles.length;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Adds a locale jar.mn entry mapping `<chromeSubPath>/<tagName>.ftl` to the
|
|
118
|
+
* on-disk `.ftl` that `furnace apply` just copied under the FTL tree. Without
|
|
119
|
+
* this entry the chrome URI passed to `window.MozXULElement.insertFTLIfNeeded`
|
|
120
|
+
* does not resolve at runtime, so the generated `--localized` component
|
|
121
|
+
* silently ships broken l10n.
|
|
122
|
+
*
|
|
123
|
+
* Degrades gracefully — if the locale jar.mn (e.g. `toolkit/locales/jar.mn`)
|
|
124
|
+
* does not exist, returns 0 rather than throwing, so a custom fork without a
|
|
125
|
+
* standard locales package can still apply a localized component.
|
|
126
|
+
*
|
|
127
|
+
* The written entry mirrors the Mozilla convention for toolkit widgets:
|
|
128
|
+
*
|
|
129
|
+
* locale/@AB_CD@/<chromeSubPath>/<tagName>.ftl (%<chromeSubPath>/<tagName>.ftl)
|
|
130
|
+
*
|
|
131
|
+
* @param engineDir - Path to the Firefox engine source root
|
|
132
|
+
* @param jarMnRelPath - Engine-relative path to the locale jar.mn
|
|
133
|
+
* @param tagName - Custom element tag name (base of the `.ftl` file)
|
|
134
|
+
* @param chromeSubPath - Chrome sub-path (e.g. `toolkit/global`)
|
|
135
|
+
* @returns Number of entries inserted (0 when already present, or jar.mn missing)
|
|
136
|
+
*/
|
|
137
|
+
export async function addLocaleFtlJarMnEntry(engineDir, jarMnRelPath, tagName, chromeSubPath) {
|
|
138
|
+
const filePath = join(engineDir, jarMnRelPath);
|
|
139
|
+
if (!(await pathExists(filePath))) {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
const content = await readText(filePath);
|
|
143
|
+
const lines = content.split('\n');
|
|
144
|
+
const ftlFile = `${tagName}.ftl`;
|
|
145
|
+
const escapedTag = escapeForRegex(tagName);
|
|
146
|
+
const escapedChrome = escapeForRegex(chromeSubPath);
|
|
147
|
+
const presencePattern = new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapedChrome}\\/${escapedTag}\\.ftl`, 'm');
|
|
148
|
+
if (presencePattern.test(content)) {
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
const indent = detectLocaleJarMnIndent(lines, chromeSubPath);
|
|
152
|
+
const newEntry = `${indent}locale/@AB_CD@/${chromeSubPath}/${ftlFile} (%${chromeSubPath}/${ftlFile})`;
|
|
153
|
+
const sectionPattern = new RegExp(`^(\\s+)locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapedChrome}\\/([^.\\s]+)\\.ftl`);
|
|
154
|
+
let insertIndex = -1;
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const line = lines[i];
|
|
157
|
+
if (line === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
const match = sectionPattern.exec(line);
|
|
160
|
+
if (match) {
|
|
161
|
+
const existingTag = match[2] ?? '';
|
|
162
|
+
if (existingTag > tagName) {
|
|
163
|
+
insertIndex = i;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
insertIndex = i + 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (insertIndex === -1) {
|
|
170
|
+
// No existing entries under this chrome sub-path. Fall back to end-of-file
|
|
171
|
+
// placement so the operator can reorder manually if desired.
|
|
172
|
+
insertIndex = lines.length;
|
|
173
|
+
}
|
|
174
|
+
lines.splice(insertIndex, 0, newEntry);
|
|
175
|
+
await writeText(filePath, lines.join('\n'));
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
/** Detects locale jar.mn indentation by sampling an existing matching entry. */
|
|
179
|
+
function detectLocaleJarMnIndent(lines, chromeSubPath) {
|
|
180
|
+
const escapedChrome = escapeForRegex(chromeSubPath);
|
|
181
|
+
const pattern = new RegExp(`^(\\s+)locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapedChrome}\\/`);
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const match = pattern.exec(line);
|
|
184
|
+
if (match?.[1])
|
|
185
|
+
return match[1];
|
|
186
|
+
}
|
|
187
|
+
// Fall back to detecting any existing `locale/...` indent before giving up.
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const match = /^(\s+)locale\//.exec(line);
|
|
190
|
+
if (match?.[1])
|
|
191
|
+
return match[1];
|
|
192
|
+
}
|
|
193
|
+
return ' ';
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Removes a locale jar.mn entry previously written by `addLocaleFtlJarMnEntry`.
|
|
197
|
+
* Idempotent — if the entry is absent or the file is missing, nothing happens.
|
|
198
|
+
*/
|
|
199
|
+
export async function removeLocaleFtlJarMnEntry(engineDir, jarMnRelPath, tagName, chromeSubPath) {
|
|
200
|
+
const filePath = join(engineDir, jarMnRelPath);
|
|
201
|
+
if (!(await pathExists(filePath))) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const content = await readText(filePath);
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
const pattern = new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapeForRegex(chromeSubPath)}\\/${escapeForRegex(tagName)}\\.ftl`);
|
|
207
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
208
|
+
if (filtered.length === lines.length)
|
|
209
|
+
return;
|
|
210
|
+
await writeText(filePath, filtered.join('\n'));
|
|
211
|
+
}
|
|
116
212
|
/**
|
|
117
213
|
* Removes all jar.mn entries for a given tag name.
|
|
118
214
|
*
|