@hominis/fireforge 0.9.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 +7 -0
- package/LICENSE.md +294 -0
- package/README.md +435 -0
- package/dist/bin/fireforge.d.ts +10 -0
- package/dist/bin/fireforge.js +29 -0
- package/dist/src/cli.d.ts +33 -0
- package/dist/src/cli.js +180 -0
- package/dist/src/commands/bootstrap.d.ts +9 -0
- package/dist/src/commands/bootstrap.js +73 -0
- package/dist/src/commands/build.d.ts +11 -0
- package/dist/src/commands/build.js +102 -0
- package/dist/src/commands/config.d.ts +13 -0
- package/dist/src/commands/config.js +135 -0
- package/dist/src/commands/discard.d.ts +12 -0
- package/dist/src/commands/discard.js +84 -0
- package/dist/src/commands/doctor.d.ts +18 -0
- package/dist/src/commands/doctor.js +356 -0
- package/dist/src/commands/download.d.ts +11 -0
- package/dist/src/commands/download.js +127 -0
- package/dist/src/commands/export-all.d.ts +11 -0
- package/dist/src/commands/export-all.js +122 -0
- package/dist/src/commands/export-shared.d.ts +48 -0
- package/dist/src/commands/export-shared.js +208 -0
- package/dist/src/commands/export.d.ts +13 -0
- package/dist/src/commands/export.js +178 -0
- package/dist/src/commands/furnace/apply.d.ts +7 -0
- package/dist/src/commands/furnace/apply.js +80 -0
- package/dist/src/commands/furnace/create.d.ts +8 -0
- package/dist/src/commands/furnace/create.js +377 -0
- package/dist/src/commands/furnace/deploy.d.ts +8 -0
- package/dist/src/commands/furnace/deploy.js +338 -0
- package/dist/src/commands/furnace/diff.d.ts +7 -0
- package/dist/src/commands/furnace/diff.js +119 -0
- package/dist/src/commands/furnace/index.d.ts +16 -0
- package/dist/src/commands/furnace/index.js +121 -0
- package/dist/src/commands/furnace/list.d.ts +5 -0
- package/dist/src/commands/furnace/list.js +65 -0
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +188 -0
- package/dist/src/commands/furnace/preview.d.ts +7 -0
- package/dist/src/commands/furnace/preview.js +96 -0
- package/dist/src/commands/furnace/remove.d.ts +8 -0
- package/dist/src/commands/furnace/remove.js +159 -0
- package/dist/src/commands/furnace/scan.d.ts +5 -0
- package/dist/src/commands/furnace/scan.js +112 -0
- package/dist/src/commands/furnace/status.d.ts +7 -0
- package/dist/src/commands/furnace/status.js +137 -0
- package/dist/src/commands/furnace/validate.d.ts +6 -0
- package/dist/src/commands/furnace/validate.js +91 -0
- package/dist/src/commands/furnace/validation-output.d.ts +7 -0
- package/dist/src/commands/furnace/validation-output.js +22 -0
- package/dist/src/commands/import.d.ts +11 -0
- package/dist/src/commands/import.js +241 -0
- package/dist/src/commands/lint.d.ts +10 -0
- package/dist/src/commands/lint.js +118 -0
- package/dist/src/commands/package.d.ts +11 -0
- package/dist/src/commands/package.js +80 -0
- package/dist/src/commands/re-export.d.ts +12 -0
- package/dist/src/commands/re-export.js +242 -0
- package/dist/src/commands/rebase/abort.d.ts +7 -0
- package/dist/src/commands/rebase/abort.js +49 -0
- package/dist/src/commands/rebase/confirm.d.ts +18 -0
- package/dist/src/commands/rebase/confirm.js +33 -0
- package/dist/src/commands/rebase/continue.d.ts +7 -0
- package/dist/src/commands/rebase/continue.js +81 -0
- package/dist/src/commands/rebase/index.d.ts +22 -0
- package/dist/src/commands/rebase/index.js +127 -0
- package/dist/src/commands/rebase/patch-loop.d.ts +9 -0
- package/dist/src/commands/rebase/patch-loop.js +135 -0
- package/dist/src/commands/rebase/summary.d.ts +12 -0
- package/dist/src/commands/rebase/summary.js +43 -0
- package/dist/src/commands/rebase.d.ts +4 -0
- package/dist/src/commands/rebase.js +6 -0
- package/dist/src/commands/register.d.ts +13 -0
- package/dist/src/commands/register.js +67 -0
- package/dist/src/commands/reset.d.ts +11 -0
- package/dist/src/commands/reset.js +83 -0
- package/dist/src/commands/resolve.d.ts +9 -0
- package/dist/src/commands/resolve.js +124 -0
- package/dist/src/commands/run.d.ts +9 -0
- package/dist/src/commands/run.js +91 -0
- package/dist/src/commands/setup-support.d.ts +23 -0
- package/dist/src/commands/setup-support.js +310 -0
- package/dist/src/commands/setup.d.ts +11 -0
- package/dist/src/commands/setup.js +94 -0
- package/dist/src/commands/status.d.ts +11 -0
- package/dist/src/commands/status.js +268 -0
- package/dist/src/commands/test.d.ts +12 -0
- package/dist/src/commands/test.js +182 -0
- package/dist/src/commands/token-coverage.d.ts +5 -0
- package/dist/src/commands/token-coverage.js +57 -0
- package/dist/src/commands/token.d.ts +14 -0
- package/dist/src/commands/token.js +121 -0
- package/dist/src/commands/watch.d.ts +9 -0
- package/dist/src/commands/watch.js +112 -0
- package/dist/src/commands/wire.d.ts +13 -0
- package/dist/src/commands/wire.js +149 -0
- package/dist/src/core/ast-utils.d.ts +47 -0
- package/dist/src/core/ast-utils.js +57 -0
- package/dist/src/core/brand-validation.d.ts +7 -0
- package/dist/src/core/brand-validation.js +15 -0
- package/dist/src/core/branding.d.ts +49 -0
- package/dist/src/core/branding.js +229 -0
- package/dist/src/core/browser-wire.d.ts +40 -0
- package/dist/src/core/browser-wire.js +66 -0
- package/dist/src/core/build-prepare.d.ts +25 -0
- package/dist/src/core/build-prepare.js +93 -0
- package/dist/src/core/config-mutate.d.ts +15 -0
- package/dist/src/core/config-mutate.js +51 -0
- package/dist/src/core/config-paths.d.ts +28 -0
- package/dist/src/core/config-paths.js +65 -0
- package/dist/src/core/config-state.d.ts +28 -0
- package/dist/src/core/config-state.js +152 -0
- package/dist/src/core/config-validate.d.ts +11 -0
- package/dist/src/core/config-validate.js +141 -0
- package/dist/src/core/config.d.ts +39 -0
- package/dist/src/core/config.js +70 -0
- package/dist/src/core/file-lock.d.ts +11 -0
- package/dist/src/core/file-lock.js +80 -0
- package/dist/src/core/firefox-archive.d.ts +40 -0
- package/dist/src/core/firefox-archive.js +63 -0
- package/dist/src/core/firefox-cache.d.ts +23 -0
- package/dist/src/core/firefox-cache.js +134 -0
- package/dist/src/core/firefox-download.d.ts +21 -0
- package/dist/src/core/firefox-download.js +129 -0
- package/dist/src/core/firefox-extract.d.ts +21 -0
- package/dist/src/core/firefox-extract.js +53 -0
- package/dist/src/core/firefox.d.ts +34 -0
- package/dist/src/core/firefox.js +78 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +21 -0
- package/dist/src/core/furnace-apply-helpers.js +244 -0
- package/dist/src/core/furnace-apply.d.ts +16 -0
- package/dist/src/core/furnace-apply.js +147 -0
- package/dist/src/core/furnace-config.d.ts +94 -0
- package/dist/src/core/furnace-config.js +372 -0
- package/dist/src/core/furnace-constants.d.ts +4 -0
- package/dist/src/core/furnace-constants.js +6 -0
- package/dist/src/core/furnace-registration-ast.d.ts +24 -0
- package/dist/src/core/furnace-registration-ast.js +218 -0
- package/dist/src/core/furnace-registration-remove.d.ts +14 -0
- package/dist/src/core/furnace-registration-remove.js +89 -0
- package/dist/src/core/furnace-registration-validate.d.ts +20 -0
- package/dist/src/core/furnace-registration-validate.js +40 -0
- package/dist/src/core/furnace-registration.d.ts +29 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-rollback.d.ts +20 -0
- package/dist/src/core/furnace-rollback.js +66 -0
- package/dist/src/core/furnace-scanner.d.ts +40 -0
- package/dist/src/core/furnace-scanner.js +143 -0
- package/dist/src/core/furnace-stories.d.ts +37 -0
- package/dist/src/core/furnace-stories.js +185 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-accessibility.js +32 -0
- package/dist/src/core/furnace-validate-checks.d.ts +4 -0
- package/dist/src/core/furnace-validate-checks.js +7 -0
- package/dist/src/core/furnace-validate-compatibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-compatibility.js +57 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +28 -0
- package/dist/src/core/furnace-validate-helpers.js +129 -0
- package/dist/src/core/furnace-validate-registration.d.ts +37 -0
- package/dist/src/core/furnace-validate-registration.js +220 -0
- package/dist/src/core/furnace-validate-structure.d.ts +6 -0
- package/dist/src/core/furnace-validate-structure.js +66 -0
- package/dist/src/core/furnace-validate.d.ts +16 -0
- package/dist/src/core/furnace-validate.js +103 -0
- package/dist/src/core/git-base.d.ts +47 -0
- package/dist/src/core/git-base.js +50 -0
- package/dist/src/core/git-diff.d.ts +63 -0
- package/dist/src/core/git-diff.js +246 -0
- package/dist/src/core/git-file-ops.d.ts +65 -0
- package/dist/src/core/git-file-ops.js +141 -0
- package/dist/src/core/git-status.d.ts +65 -0
- package/dist/src/core/git-status.js +163 -0
- package/dist/src/core/git.d.ts +113 -0
- package/dist/src/core/git.js +363 -0
- package/dist/src/core/license-headers.d.ts +36 -0
- package/dist/src/core/license-headers.js +83 -0
- package/dist/src/core/mach-build-artifacts.d.ts +29 -0
- package/dist/src/core/mach-build-artifacts.js +117 -0
- package/dist/src/core/mach-mozconfig.d.ts +17 -0
- package/dist/src/core/mach-mozconfig.js +50 -0
- package/dist/src/core/mach-python.d.ts +16 -0
- package/dist/src/core/mach-python.js +126 -0
- package/dist/src/core/mach.d.ts +106 -0
- package/dist/src/core/mach.js +166 -0
- package/dist/src/core/manifest-helpers.d.ts +25 -0
- package/dist/src/core/manifest-helpers.js +96 -0
- package/dist/src/core/manifest-register.d.ts +30 -0
- package/dist/src/core/manifest-register.js +65 -0
- package/dist/src/core/manifest-rules.d.ts +39 -0
- package/dist/src/core/manifest-rules.js +151 -0
- package/dist/src/core/manifest-tokenizers.d.ts +34 -0
- package/dist/src/core/manifest-tokenizers.js +84 -0
- package/dist/src/core/parser-fallback.d.ts +36 -0
- package/dist/src/core/parser-fallback.js +43 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +29 -0
- package/dist/src/core/patch-apply-fuzz.js +70 -0
- package/dist/src/core/patch-apply.d.ts +46 -0
- package/dist/src/core/patch-apply.js +235 -0
- package/dist/src/core/patch-export.d.ts +99 -0
- package/dist/src/core/patch-export.js +314 -0
- package/dist/src/core/patch-files.d.ts +11 -0
- package/dist/src/core/patch-files.js +51 -0
- package/dist/src/core/patch-lint.d.ts +72 -0
- package/dist/src/core/patch-lint.js +403 -0
- package/dist/src/core/patch-lock.d.ts +8 -0
- package/dist/src/core/patch-lock.js +29 -0
- package/dist/src/core/patch-manifest-consistency.d.ts +24 -0
- package/dist/src/core/patch-manifest-consistency.js +135 -0
- package/dist/src/core/patch-manifest-io.d.ts +36 -0
- package/dist/src/core/patch-manifest-io.js +77 -0
- package/dist/src/core/patch-manifest-query.d.ts +48 -0
- package/dist/src/core/patch-manifest-query.js +124 -0
- package/dist/src/core/patch-manifest-validate.d.ts +22 -0
- package/dist/src/core/patch-manifest-validate.js +72 -0
- package/dist/src/core/patch-manifest.d.ts +11 -0
- package/dist/src/core/patch-manifest.js +12 -0
- package/dist/src/core/patch-parse.d.ts +43 -0
- package/dist/src/core/patch-parse.js +143 -0
- package/dist/src/core/patch-transform.d.ts +21 -0
- package/dist/src/core/patch-transform.js +138 -0
- package/dist/src/core/rebase-session.d.ts +47 -0
- package/dist/src/core/rebase-session.js +65 -0
- package/dist/src/core/register-browser-content.d.ts +11 -0
- package/dist/src/core/register-browser-content.js +116 -0
- package/dist/src/core/register-module.d.ts +11 -0
- package/dist/src/core/register-module.js +76 -0
- package/dist/src/core/register-shared-css.d.ts +11 -0
- package/dist/src/core/register-shared-css.js +117 -0
- package/dist/src/core/register-test-manifest.d.ts +18 -0
- package/dist/src/core/register-test-manifest.js +99 -0
- package/dist/src/core/state-file.d.ts +4 -0
- package/dist/src/core/state-file.js +25 -0
- package/dist/src/core/token-coverage.d.ts +12 -0
- package/dist/src/core/token-coverage.js +74 -0
- package/dist/src/core/token-manager.d.ts +55 -0
- package/dist/src/core/token-manager.js +387 -0
- package/dist/src/core/wire-destroy.d.ts +21 -0
- package/dist/src/core/wire-destroy.js +103 -0
- package/dist/src/core/wire-dom-fragment.d.ts +23 -0
- package/dist/src/core/wire-dom-fragment.js +129 -0
- package/dist/src/core/wire-init.d.ts +23 -0
- package/dist/src/core/wire-init.js +201 -0
- package/dist/src/core/wire-subscript.d.ts +20 -0
- package/dist/src/core/wire-subscript.js +134 -0
- package/dist/src/core/wire-targets.d.ts +7 -0
- package/dist/src/core/wire-targets.js +9 -0
- package/dist/src/core/wire-utils.d.ts +88 -0
- package/dist/src/core/wire-utils.js +279 -0
- package/dist/src/errors/base.d.ts +60 -0
- package/dist/src/errors/base.js +87 -0
- package/dist/src/errors/build.d.ts +52 -0
- package/dist/src/errors/build.js +114 -0
- package/dist/src/errors/codes.d.ts +29 -0
- package/dist/src/errors/codes.js +30 -0
- package/dist/src/errors/config.d.ts +31 -0
- package/dist/src/errors/config.js +61 -0
- package/dist/src/errors/download.d.ts +42 -0
- package/dist/src/errors/download.js +95 -0
- package/dist/src/errors/furnace.d.ts +10 -0
- package/dist/src/errors/furnace.js +22 -0
- package/dist/src/errors/git.d.ts +41 -0
- package/dist/src/errors/git.js +99 -0
- package/dist/src/errors/patch.d.ts +10 -0
- package/dist/src/errors/patch.js +26 -0
- package/dist/src/errors/rebase.d.ts +20 -0
- package/dist/src/errors/rebase.js +30 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +21 -0
- package/dist/src/types/cli.d.ts +14 -0
- package/dist/src/types/cli.js +2 -0
- package/dist/src/types/commands/index.d.ts +6 -0
- package/dist/src/types/commands/index.js +6 -0
- package/dist/src/types/commands/options.d.ts +239 -0
- package/dist/src/types/commands/options.js +6 -0
- package/dist/src/types/commands/patches.d.ts +89 -0
- package/dist/src/types/commands/patches.js +6 -0
- package/dist/src/types/commands/project.d.ts +71 -0
- package/dist/src/types/commands/project.js +6 -0
- package/dist/src/types/config.d.ts +101 -0
- package/dist/src/types/config.js +2 -0
- package/dist/src/types/furnace.d.ts +158 -0
- package/dist/src/types/furnace.js +2 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +6 -0
- package/dist/src/utils/errors.d.ts +2 -0
- package/dist/src/utils/errors.js +15 -0
- package/dist/src/utils/fs.d.ts +72 -0
- package/dist/src/utils/fs.js +179 -0
- package/dist/src/utils/logger.d.ts +58 -0
- package/dist/src/utils/logger.js +120 -0
- package/dist/src/utils/options.d.ts +8 -0
- package/dist/src/utils/options.js +16 -0
- package/dist/src/utils/package-root.d.ts +10 -0
- package/dist/src/utils/package-root.js +53 -0
- package/dist/src/utils/parse.d.ts +110 -0
- package/dist/src/utils/parse.js +200 -0
- package/dist/src/utils/paths.d.ts +10 -0
- package/dist/src/utils/paths.js +43 -0
- package/dist/src/utils/platform.d.ts +38 -0
- package/dist/src/utils/platform.js +56 -0
- package/dist/src/utils/process.d.ts +80 -0
- package/dist/src/utils/process.js +188 -0
- package/dist/src/utils/regex.d.ts +24 -0
- package/dist/src/utils/regex.js +40 -0
- package/dist/src/utils/validation.d.ts +133 -0
- package/dist/src/utils/validation.js +250 -0
- package/package.json +106 -0
- package/templates/configs/common.mozconfig +24 -0
- package/templates/configs/darwin.mozconfig +10 -0
- package/templates/configs/linux.mozconfig +12 -0
- package/templates/configs/win32.mozconfig +14 -0
- package/templates/licenses/0BSD.md +14 -0
- package/templates/licenses/EUPL-1.2.md +294 -0
- package/templates/licenses/GPL-2.0-or-later.md +339 -0
- package/templates/licenses/MPL-2.0.md +383 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
4
|
+
import { toError } from '../utils/errors.js';
|
|
5
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
6
|
+
import { warn } from '../utils/logger.js';
|
|
7
|
+
import { isArray, isBoolean, isObject, isString } from '../utils/validation.js';
|
|
8
|
+
import { FIREFORGE_DIR } from './config.js';
|
|
9
|
+
import { quarantineStateFile, withStateFileLock } from './state-file.js';
|
|
10
|
+
/** Name of the furnace configuration file */
|
|
11
|
+
export const FURNACE_CONFIG_FILENAME = 'furnace.json';
|
|
12
|
+
/** Name of the furnace state file */
|
|
13
|
+
export const FURNACE_STATE_FILENAME = 'furnace-state.json';
|
|
14
|
+
/** Name of the components directory */
|
|
15
|
+
export const COMPONENTS_DIR = 'components';
|
|
16
|
+
/** Name of the overrides subdirectory */
|
|
17
|
+
export const OVERRIDES_DIR = 'overrides';
|
|
18
|
+
/** Name of the custom subdirectory */
|
|
19
|
+
export const CUSTOM_DIR = 'custom';
|
|
20
|
+
/**
|
|
21
|
+
* Gets all furnace-related paths based on a root directory.
|
|
22
|
+
* @param root - Root directory of the project
|
|
23
|
+
* @returns All furnace paths
|
|
24
|
+
*/
|
|
25
|
+
export function getFurnacePaths(root) {
|
|
26
|
+
const componentsDir = join(root, COMPONENTS_DIR);
|
|
27
|
+
return {
|
|
28
|
+
furnaceConfig: join(root, FURNACE_CONFIG_FILENAME),
|
|
29
|
+
componentsDir,
|
|
30
|
+
overridesDir: join(componentsDir, OVERRIDES_DIR),
|
|
31
|
+
customDir: join(componentsDir, CUSTOM_DIR),
|
|
32
|
+
furnaceState: join(root, FIREFORGE_DIR, FURNACE_STATE_FILENAME),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a furnace.json exists in the given directory.
|
|
37
|
+
* @param root - Root directory to check
|
|
38
|
+
* @returns True if furnace.json exists
|
|
39
|
+
*/
|
|
40
|
+
export async function furnaceConfigExists(root) {
|
|
41
|
+
const paths = getFurnacePaths(root);
|
|
42
|
+
return pathExists(paths.furnaceConfig);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validates an override component config object.
|
|
46
|
+
* @param data - Raw data to validate
|
|
47
|
+
* @param name - Component name for error messages
|
|
48
|
+
*/
|
|
49
|
+
function parseStringArray(value, fieldName) {
|
|
50
|
+
if (!isArray(value)) {
|
|
51
|
+
throw new FurnaceError(`Furnace config: "${fieldName}" must be an array`);
|
|
52
|
+
}
|
|
53
|
+
const items = [];
|
|
54
|
+
for (const item of value) {
|
|
55
|
+
if (!isString(item)) {
|
|
56
|
+
throw new FurnaceError(`Furnace config: "${fieldName}" array must contain only strings`);
|
|
57
|
+
}
|
|
58
|
+
items.push(item);
|
|
59
|
+
}
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
62
|
+
function parseOverrideConfig(data, name) {
|
|
63
|
+
const validTypes = ['css-only', 'full'];
|
|
64
|
+
if (!isString(data['type']) || !validTypes.includes(data['type'])) {
|
|
65
|
+
throw new FurnaceError(`Furnace config: override "${name}.type" must be one of: ${validTypes.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
if (!isString(data['description'])) {
|
|
68
|
+
throw new FurnaceError(`Furnace config: override "${name}.description" must be a string`);
|
|
69
|
+
}
|
|
70
|
+
if (!isString(data['basePath'])) {
|
|
71
|
+
throw new FurnaceError(`Furnace config: override "${name}.basePath" must be a string`);
|
|
72
|
+
}
|
|
73
|
+
if (data['basePath'].includes('..')) {
|
|
74
|
+
throw new FurnaceError(`Furnace config: override "${name}.basePath" must not contain ".." (path traversal)`);
|
|
75
|
+
}
|
|
76
|
+
if (!isString(data['baseVersion'])) {
|
|
77
|
+
throw new FurnaceError(`Furnace config: override "${name}.baseVersion" must be a string`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
type: data['type'] === 'css-only' ? 'css-only' : 'full',
|
|
81
|
+
description: data['description'],
|
|
82
|
+
basePath: data['basePath'],
|
|
83
|
+
baseVersion: data['baseVersion'],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validates a custom component config object.
|
|
88
|
+
* @param data - Raw data to validate
|
|
89
|
+
* @param name - Component name for error messages
|
|
90
|
+
*/
|
|
91
|
+
function parseCustomConfig(data, name) {
|
|
92
|
+
if (!isString(data['description'])) {
|
|
93
|
+
throw new FurnaceError(`Furnace config: custom "${name}.description" must be a string`);
|
|
94
|
+
}
|
|
95
|
+
if (!isString(data['targetPath'])) {
|
|
96
|
+
throw new FurnaceError(`Furnace config: custom "${name}.targetPath" must be a string`);
|
|
97
|
+
}
|
|
98
|
+
if (data['targetPath'].includes('..')) {
|
|
99
|
+
throw new FurnaceError(`Furnace config: custom "${name}.targetPath" must not contain ".." (path traversal)`);
|
|
100
|
+
}
|
|
101
|
+
if (!isBoolean(data['register'])) {
|
|
102
|
+
throw new FurnaceError(`Furnace config: custom "${name}.register" must be a boolean`);
|
|
103
|
+
}
|
|
104
|
+
if (!isBoolean(data['localized'])) {
|
|
105
|
+
throw new FurnaceError(`Furnace config: custom "${name}.localized" must be a boolean`);
|
|
106
|
+
}
|
|
107
|
+
if (data['composes'] !== undefined) {
|
|
108
|
+
parseStringArray(data['composes'], `${name}.composes`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
description: data['description'],
|
|
112
|
+
targetPath: data['targetPath'],
|
|
113
|
+
register: data['register'],
|
|
114
|
+
localized: data['localized'],
|
|
115
|
+
...(data['composes'] !== undefined
|
|
116
|
+
? { composes: parseStringArray(data['composes'], `${name}.composes`) }
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validates a raw config object and returns a typed FurnaceConfig.
|
|
122
|
+
* @param data - Raw data to validate
|
|
123
|
+
* @returns Validated FurnaceConfig
|
|
124
|
+
* @throws Error if validation fails
|
|
125
|
+
*/
|
|
126
|
+
export function validateFurnaceConfig(data) {
|
|
127
|
+
if (!isObject(data)) {
|
|
128
|
+
throw new FurnaceError('Furnace config must be an object');
|
|
129
|
+
}
|
|
130
|
+
if (data['version'] !== 1) {
|
|
131
|
+
throw new FurnaceError('Furnace config: "version" must be 1');
|
|
132
|
+
}
|
|
133
|
+
if (!isString(data['componentPrefix'])) {
|
|
134
|
+
throw new FurnaceError('Furnace config: "componentPrefix" must be a string');
|
|
135
|
+
}
|
|
136
|
+
// Validate optional tokenPrefix
|
|
137
|
+
if (data['tokenPrefix'] !== undefined && !isString(data['tokenPrefix'])) {
|
|
138
|
+
throw new FurnaceError('Furnace config: "tokenPrefix" must be a string if provided');
|
|
139
|
+
}
|
|
140
|
+
// Validate optional tokenAllowlist
|
|
141
|
+
if (data['tokenAllowlist'] !== undefined) {
|
|
142
|
+
parseStringArray(data['tokenAllowlist'], 'tokenAllowlist');
|
|
143
|
+
}
|
|
144
|
+
const stock = parseStringArray(data['stock'], 'stock');
|
|
145
|
+
// Validate overrides
|
|
146
|
+
if (!isObject(data['overrides'])) {
|
|
147
|
+
throw new FurnaceError('Furnace config: "overrides" must be an object');
|
|
148
|
+
}
|
|
149
|
+
const overrides = {};
|
|
150
|
+
for (const [name, value] of Object.entries(data['overrides'])) {
|
|
151
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
152
|
+
throw new FurnaceError(`Furnace config: override name "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
|
|
153
|
+
}
|
|
154
|
+
if (!isObject(value)) {
|
|
155
|
+
throw new FurnaceError(`Furnace config: override "${name}" must be an object`);
|
|
156
|
+
}
|
|
157
|
+
overrides[name] = parseOverrideConfig(value, name);
|
|
158
|
+
}
|
|
159
|
+
// Validate custom
|
|
160
|
+
if (!isObject(data['custom'])) {
|
|
161
|
+
throw new FurnaceError('Furnace config: "custom" must be an object');
|
|
162
|
+
}
|
|
163
|
+
const custom = {};
|
|
164
|
+
for (const [name, value] of Object.entries(data['custom'])) {
|
|
165
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
166
|
+
throw new FurnaceError(`Furnace config: custom name "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
|
|
167
|
+
}
|
|
168
|
+
if (!isObject(value)) {
|
|
169
|
+
throw new FurnaceError(`Furnace config: custom "${name}" must be an object`);
|
|
170
|
+
}
|
|
171
|
+
custom[name] = parseCustomConfig(value, name);
|
|
172
|
+
}
|
|
173
|
+
const config = {
|
|
174
|
+
version: 1,
|
|
175
|
+
componentPrefix: data['componentPrefix'],
|
|
176
|
+
stock,
|
|
177
|
+
overrides,
|
|
178
|
+
custom,
|
|
179
|
+
};
|
|
180
|
+
if (data['tokenPrefix'] !== undefined) {
|
|
181
|
+
config.tokenPrefix = data['tokenPrefix'];
|
|
182
|
+
}
|
|
183
|
+
if (data['tokenAllowlist'] !== undefined) {
|
|
184
|
+
config.tokenAllowlist = parseStringArray(data['tokenAllowlist'], 'tokenAllowlist');
|
|
185
|
+
}
|
|
186
|
+
return config;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validates a parsed furnace state object and returns a typed FurnaceState.
|
|
190
|
+
* @param data - Parsed JSON state data
|
|
191
|
+
* @returns Validated FurnaceState
|
|
192
|
+
*/
|
|
193
|
+
export function validateFurnaceState(data) {
|
|
194
|
+
const result = sanitizeFurnaceState(data);
|
|
195
|
+
if (result.issues.length > 0) {
|
|
196
|
+
throw new FurnaceError(`Invalid furnace state: ${result.issues.join('; ')}`);
|
|
197
|
+
}
|
|
198
|
+
return result.state;
|
|
199
|
+
}
|
|
200
|
+
function sanitizeFurnaceState(data) {
|
|
201
|
+
if (!isObject(data)) {
|
|
202
|
+
return {
|
|
203
|
+
state: {},
|
|
204
|
+
issues: ['the root value must be a JSON object'],
|
|
205
|
+
recoveredFields: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const state = {};
|
|
209
|
+
const issues = [];
|
|
210
|
+
const recoveredFields = [];
|
|
211
|
+
if (data['lastApply'] !== undefined) {
|
|
212
|
+
if (!isString(data['lastApply'])) {
|
|
213
|
+
issues.push('field "lastApply" must be a string');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
state.lastApply = data['lastApply'];
|
|
217
|
+
recoveredFields.push('lastApply');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (data['appliedChecksums'] !== undefined) {
|
|
221
|
+
if (!isObject(data['appliedChecksums'])) {
|
|
222
|
+
issues.push('field "appliedChecksums" must be an object of string checksum values');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const appliedChecksums = {};
|
|
226
|
+
let hasInvalidChecksum = false;
|
|
227
|
+
for (const [filePath, checksum] of Object.entries(data['appliedChecksums'])) {
|
|
228
|
+
if (!isString(checksum)) {
|
|
229
|
+
hasInvalidChecksum = true;
|
|
230
|
+
issues.push(`appliedChecksums["${filePath}"] must be a string`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
appliedChecksums[filePath] = checksum;
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(appliedChecksums).length > 0 || !hasInvalidChecksum) {
|
|
236
|
+
state.appliedChecksums = appliedChecksums;
|
|
237
|
+
recoveredFields.push('appliedChecksums');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { state, issues, recoveredFields };
|
|
242
|
+
}
|
|
243
|
+
async function recoverInvalidFurnaceState(statePath, result, alreadyLocked = false) {
|
|
244
|
+
const recover = async () => {
|
|
245
|
+
const quarantinedFile = await quarantineStateFile(statePath, 'invalid');
|
|
246
|
+
if (result.recoveredFields.length > 0) {
|
|
247
|
+
await writeJson(statePath, result.state);
|
|
248
|
+
}
|
|
249
|
+
const recoveryMessage = result.recoveredFields.length > 0
|
|
250
|
+
? ` Recovered valid field${result.recoveredFields.length === 1 ? '' : 's'}: ${result.recoveredFields.join(', ')}.`
|
|
251
|
+
: ' No valid furnace state fields could be recovered; using defaults.';
|
|
252
|
+
const quarantineMessage = quarantinedFile
|
|
253
|
+
? ` Quarantined the original file as ${quarantinedFile}.`
|
|
254
|
+
: '';
|
|
255
|
+
warn(`Furnace state file (.fireforge/furnace-state.json) was invalid: ${result.issues.join('; ')}.${recoveryMessage}${quarantineMessage}`);
|
|
256
|
+
return result.state;
|
|
257
|
+
};
|
|
258
|
+
return alreadyLocked ? recover() : withStateFileLock(statePath, recover);
|
|
259
|
+
}
|
|
260
|
+
async function loadFurnaceStateFromPath(statePath, alreadyLocked = false) {
|
|
261
|
+
if (!(await pathExists(statePath))) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const data = await readJson(statePath);
|
|
266
|
+
const result = sanitizeFurnaceState(data);
|
|
267
|
+
if (result.issues.length === 0) {
|
|
268
|
+
return result.state;
|
|
269
|
+
}
|
|
270
|
+
return await recoverInvalidFurnaceState(statePath, result, alreadyLocked);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return await recoverInvalidFurnaceState(statePath, {
|
|
274
|
+
state: {},
|
|
275
|
+
issues: [`the file could not be parsed: ${toError(error).message}`],
|
|
276
|
+
recoveredFields: [],
|
|
277
|
+
}, alreadyLocked);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Loads and validates the furnace.json configuration.
|
|
282
|
+
* @param root - Root directory of the project
|
|
283
|
+
* @returns Validated FurnaceConfig
|
|
284
|
+
* @throws Error if config doesn't exist or is invalid
|
|
285
|
+
*/
|
|
286
|
+
export async function loadFurnaceConfig(root) {
|
|
287
|
+
const paths = getFurnacePaths(root);
|
|
288
|
+
if (!(await pathExists(paths.furnaceConfig))) {
|
|
289
|
+
throw new FurnaceError(`Furnace configuration file not found: ${paths.furnaceConfig}\n\n` +
|
|
290
|
+
'Run "fireforge furnace create" or "fireforge furnace override" to get started.');
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const data = await readJson(paths.furnaceConfig);
|
|
294
|
+
return validateFurnaceConfig(data);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (error instanceof FurnaceError) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
throw new FurnaceError(`Invalid furnace.json at ${paths.furnaceConfig}: ${toError(error).message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Writes a furnace configuration to furnace.json.
|
|
305
|
+
* @param root - Root directory of the project
|
|
306
|
+
* @param config - Configuration to write
|
|
307
|
+
*/
|
|
308
|
+
export async function writeFurnaceConfig(root, config) {
|
|
309
|
+
const paths = getFurnacePaths(root);
|
|
310
|
+
await writeJson(paths.furnaceConfig, config);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Creates a default furnace configuration.
|
|
314
|
+
* @returns A valid empty FurnaceConfig
|
|
315
|
+
*/
|
|
316
|
+
export function createDefaultFurnaceConfig() {
|
|
317
|
+
return {
|
|
318
|
+
version: 1,
|
|
319
|
+
componentPrefix: 'moz-',
|
|
320
|
+
stock: [],
|
|
321
|
+
overrides: {},
|
|
322
|
+
custom: {},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Loads furnace config if it exists, or creates and writes a default config.
|
|
327
|
+
* @param root - Root directory of the project
|
|
328
|
+
* @returns FurnaceConfig (existing or newly created)
|
|
329
|
+
*/
|
|
330
|
+
export async function ensureFurnaceConfig(root) {
|
|
331
|
+
if (await furnaceConfigExists(root)) {
|
|
332
|
+
return loadFurnaceConfig(root);
|
|
333
|
+
}
|
|
334
|
+
const config = createDefaultFurnaceConfig();
|
|
335
|
+
await writeFurnaceConfig(root, config);
|
|
336
|
+
return config;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Loads the furnace state, or returns defaults if it doesn't exist.
|
|
340
|
+
* @param root - Root directory of the project
|
|
341
|
+
* @returns Furnace state
|
|
342
|
+
*/
|
|
343
|
+
export async function loadFurnaceState(root) {
|
|
344
|
+
const paths = getFurnacePaths(root);
|
|
345
|
+
return loadFurnaceStateFromPath(paths.furnaceState);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Saves the furnace state.
|
|
349
|
+
* @param root - Root directory of the project
|
|
350
|
+
* @param state - State to save
|
|
351
|
+
*/
|
|
352
|
+
export async function saveFurnaceState(root, state) {
|
|
353
|
+
const paths = getFurnacePaths(root);
|
|
354
|
+
const validatedState = validateFurnaceState(state);
|
|
355
|
+
await withStateFileLock(paths.furnaceState, async () => {
|
|
356
|
+
await writeJson(paths.furnaceState, validatedState);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Updates furnace state fields transactionally under the state file lock.
|
|
361
|
+
* @param root - Root directory of the project
|
|
362
|
+
* @param updates - Fields to update, or a transactional updater function
|
|
363
|
+
*/
|
|
364
|
+
export async function updateFurnaceState(root, updates) {
|
|
365
|
+
const paths = getFurnacePaths(root);
|
|
366
|
+
await withStateFileLock(paths.furnaceState, async () => {
|
|
367
|
+
const current = await loadFurnaceStateFromPath(paths.furnaceState, true);
|
|
368
|
+
const nextState = typeof updates === 'function' ? updates(current) : { ...current, ...updates };
|
|
369
|
+
await writeJson(paths.furnaceState, validateFurnaceState(nextState));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
//# sourceMappingURL=furnace-config.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/** Path to customElements.js within the engine source tree */
|
|
3
|
+
export const CUSTOM_ELEMENTS_JS = 'toolkit/content/customElements.js';
|
|
4
|
+
/** Path to jar.mn within the engine source tree (toolkit global) */
|
|
5
|
+
export const JAR_MN = 'toolkit/content/jar.mn';
|
|
6
|
+
//# sourceMappingURL=furnace-constants.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based custom element registration updates for customElements.js.
|
|
3
|
+
* Removal logic is in furnace-registration-remove.ts.
|
|
4
|
+
*/
|
|
5
|
+
export { removeCustomElementRegistration } from './furnace-registration-remove.js';
|
|
6
|
+
export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
7
|
+
/**
|
|
8
|
+
* Adds a custom element registration entry to customElements.js.
|
|
9
|
+
*
|
|
10
|
+
* The entry is inserted into the array literal inside the `for...of` loop
|
|
11
|
+
* that registers all custom elements:
|
|
12
|
+
* ```js
|
|
13
|
+
* ["tag", "chrome://global/content/elements/tag.mjs"],
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* New entries are inserted in alphabetical order relative to existing entries.
|
|
17
|
+
* This operation is idempotent — if the tag is already registered the file is
|
|
18
|
+
* left unchanged.
|
|
19
|
+
*
|
|
20
|
+
* @param engineDir - Path to the Firefox engine source root
|
|
21
|
+
* @param tagName - Custom element tag name
|
|
22
|
+
* @param modulePath - chrome:// URI for the module
|
|
23
|
+
*/
|
|
24
|
+
export declare function addCustomElementRegistration(engineDir: string, tagName: string, modulePath: string): Promise<void>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* AST-based custom element registration updates for customElements.js.
|
|
4
|
+
* Removal logic is in furnace-registration-remove.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import MagicString from 'magic-string';
|
|
8
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
9
|
+
import { toError } from '../utils/errors.js';
|
|
10
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
11
|
+
import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
|
|
12
|
+
import { CUSTOM_ELEMENTS_JS } from './furnace-constants.js';
|
|
13
|
+
import { validateRegistrationPlacement, validateTagName } from './furnace-registration-validate.js';
|
|
14
|
+
// Re-export from split modules so existing import sites continue working
|
|
15
|
+
export { removeCustomElementRegistration } from './furnace-registration-remove.js';
|
|
16
|
+
// Re-export constants so existing import sites continue working
|
|
17
|
+
export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Checks whether a `ForOfStatement` is nested inside a
|
|
23
|
+
* `document.addEventListener("DOMContentLoaded", ...)` call by
|
|
24
|
+
* inspecting the ancestor stack.
|
|
25
|
+
*/
|
|
26
|
+
function isInsideDOMContentLoaded(ancestors, content) {
|
|
27
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
28
|
+
const ancestor = ancestors[i];
|
|
29
|
+
if (!ancestor || ancestor.type !== 'CallExpression')
|
|
30
|
+
continue;
|
|
31
|
+
const call = ancestor;
|
|
32
|
+
if (call.callee.type === 'MemberExpression' &&
|
|
33
|
+
call.callee.object.type === 'Identifier' &&
|
|
34
|
+
call.callee.object.name === 'document' &&
|
|
35
|
+
call.callee.property.type === 'Identifier' &&
|
|
36
|
+
call.callee.property.name === 'addEventListener') {
|
|
37
|
+
const firstArg = call.arguments[0];
|
|
38
|
+
if (firstArg &&
|
|
39
|
+
firstArg.type === 'Literal' &&
|
|
40
|
+
firstArg.value === 'DOMContentLoaded') {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Check if "DOMContentLoaded" appears in the call's source (handles edge cases)
|
|
44
|
+
const src = getNodeSource(content, call);
|
|
45
|
+
if (/["']DOMContentLoaded["']/.test(src)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function selectRegistrationTarget(targets, isESModule, tagName) {
|
|
53
|
+
const target = isESModule
|
|
54
|
+
? targets.find((candidate) => candidate.insideDCL)
|
|
55
|
+
: targets.find((candidate) => !candidate.insideDCL);
|
|
56
|
+
if (target) {
|
|
57
|
+
return target;
|
|
58
|
+
}
|
|
59
|
+
if (isESModule) {
|
|
60
|
+
throw new FurnaceError('Could not find DOMContentLoaded block in customElements.js', tagName);
|
|
61
|
+
}
|
|
62
|
+
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);
|
|
63
|
+
}
|
|
64
|
+
function buildRegistrationEntry(referenceEntry, tagName, modulePath) {
|
|
65
|
+
if (!referenceEntry) {
|
|
66
|
+
return ` ["${tagName}", "${modulePath}"],`;
|
|
67
|
+
}
|
|
68
|
+
if (referenceEntry.isMultiLine) {
|
|
69
|
+
const indent = referenceEntry.indent;
|
|
70
|
+
const inner = referenceEntry.innerIndent ?? indent + ' ';
|
|
71
|
+
return `${indent}[\n${inner}"${tagName}",\n${inner}"${modulePath}",\n${indent}],`;
|
|
72
|
+
}
|
|
73
|
+
return `${referenceEntry.indent}["${tagName}", "${modulePath}"],`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* AST-based implementation: parses customElements.js, walks to find the
|
|
77
|
+
* target ForOfStatement array, and inserts the new entry at the correct
|
|
78
|
+
* alphabetical position using magic-string.
|
|
79
|
+
*/
|
|
80
|
+
function addRegistrationAST(content, tagName, modulePath, isESModule) {
|
|
81
|
+
validateTagName(tagName);
|
|
82
|
+
const ast = parseScript(content);
|
|
83
|
+
const ancestors = [];
|
|
84
|
+
// Collect all ForOfStatement nodes with ArrayExpression rights
|
|
85
|
+
const forOfs = [];
|
|
86
|
+
walkAST(ast, {
|
|
87
|
+
enter(node) {
|
|
88
|
+
ancestors.push(node);
|
|
89
|
+
if (node.type === 'ForOfStatement') {
|
|
90
|
+
const forOf = node;
|
|
91
|
+
if (forOf.right.type === 'ArrayExpression') {
|
|
92
|
+
const array = forOf.right;
|
|
93
|
+
forOfs.push({
|
|
94
|
+
array,
|
|
95
|
+
insideDCL: isInsideDOMContentLoaded(ancestors, content),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
leave() {
|
|
101
|
+
ancestors.pop();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
// Select the target array
|
|
105
|
+
const target = selectRegistrationTarget(forOfs, isESModule, tagName);
|
|
106
|
+
const array = target.array;
|
|
107
|
+
// Parse existing entries from the ArrayExpression elements
|
|
108
|
+
const entries = [];
|
|
109
|
+
for (const el of array.elements) {
|
|
110
|
+
if (!el || el.type !== 'ArrayExpression')
|
|
111
|
+
continue;
|
|
112
|
+
const entryArr = el;
|
|
113
|
+
const firstEl = entryArr.elements[0];
|
|
114
|
+
if (!firstEl || firstEl.type !== 'Literal')
|
|
115
|
+
continue;
|
|
116
|
+
const tag = String(firstEl.value);
|
|
117
|
+
// Detect if this entry is multi-line
|
|
118
|
+
const entrySrc = getNodeSource(content, entryArr);
|
|
119
|
+
const isMultiLine = entrySrc.includes('\n');
|
|
120
|
+
const indent = detectIndent(content, entryArr.start);
|
|
121
|
+
let innerIndent;
|
|
122
|
+
if (isMultiLine) {
|
|
123
|
+
const firstElNode = firstEl;
|
|
124
|
+
innerIndent = detectIndent(content, firstElNode.start);
|
|
125
|
+
}
|
|
126
|
+
entries.push({ tag, node: entryArr, isMultiLine, indent, innerIndent });
|
|
127
|
+
}
|
|
128
|
+
// Find alphabetical insertion position
|
|
129
|
+
let insertAfterNode = null;
|
|
130
|
+
let insertBeforeNode = null;
|
|
131
|
+
let referenceEntry;
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (entry.tag > tagName) {
|
|
134
|
+
insertBeforeNode = entry.node;
|
|
135
|
+
if (!referenceEntry)
|
|
136
|
+
referenceEntry = entry;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
insertAfterNode = entry.node;
|
|
140
|
+
referenceEntry = entry;
|
|
141
|
+
}
|
|
142
|
+
// Build new entry string matching detected format
|
|
143
|
+
const newEntry = buildRegistrationEntry(referenceEntry, tagName, modulePath);
|
|
144
|
+
const ms = new MagicString(content);
|
|
145
|
+
// Helper: find the start-of-line position for a given offset
|
|
146
|
+
function lineStart(pos) {
|
|
147
|
+
let i = pos - 1;
|
|
148
|
+
while (i >= 0 && content[i] !== '\n')
|
|
149
|
+
i--;
|
|
150
|
+
return i + 1;
|
|
151
|
+
}
|
|
152
|
+
// Helper: find the end-of-line position (the \n itself) for a given offset
|
|
153
|
+
function lineEnd(pos) {
|
|
154
|
+
let i = pos;
|
|
155
|
+
while (i < content.length && content[i] !== '\n')
|
|
156
|
+
i++;
|
|
157
|
+
return i;
|
|
158
|
+
}
|
|
159
|
+
// Find the insertion position (character offset)
|
|
160
|
+
if (insertBeforeNode) {
|
|
161
|
+
const sol = lineStart(insertBeforeNode.start);
|
|
162
|
+
ms.appendLeft(sol, newEntry + '\n');
|
|
163
|
+
}
|
|
164
|
+
else if (insertAfterNode) {
|
|
165
|
+
const eol = lineEnd(insertAfterNode.end);
|
|
166
|
+
ms.appendRight(eol, '\n' + newEntry);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const eol = lineEnd(array.start);
|
|
170
|
+
ms.appendRight(eol, '\n' + newEntry);
|
|
171
|
+
}
|
|
172
|
+
return ms.toString();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Adds a custom element registration entry to customElements.js.
|
|
176
|
+
*
|
|
177
|
+
* The entry is inserted into the array literal inside the `for...of` loop
|
|
178
|
+
* that registers all custom elements:
|
|
179
|
+
* ```js
|
|
180
|
+
* ["tag", "chrome://global/content/elements/tag.mjs"],
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* New entries are inserted in alphabetical order relative to existing entries.
|
|
184
|
+
* This operation is idempotent — if the tag is already registered the file is
|
|
185
|
+
* left unchanged.
|
|
186
|
+
*
|
|
187
|
+
* @param engineDir - Path to the Firefox engine source root
|
|
188
|
+
* @param tagName - Custom element tag name
|
|
189
|
+
* @param modulePath - chrome:// URI for the module
|
|
190
|
+
*/
|
|
191
|
+
export async function addCustomElementRegistration(engineDir, tagName, modulePath) {
|
|
192
|
+
const filePath = join(engineDir, CUSTOM_ELEMENTS_JS);
|
|
193
|
+
if (!(await pathExists(filePath))) {
|
|
194
|
+
throw new FurnaceError('customElements.js not found in engine', tagName);
|
|
195
|
+
}
|
|
196
|
+
const content = await readText(filePath);
|
|
197
|
+
// Idempotency: already registered (standalone block or array entry).
|
|
198
|
+
if (content.includes(`setElementCreationCallback("${tagName}"`) ||
|
|
199
|
+
content.includes(`["${tagName}",`) ||
|
|
200
|
+
new RegExp(`^\\s*"${tagName}",\\s*$`, 'm').test(content)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const isESModule = modulePath.endsWith('.mjs');
|
|
204
|
+
let nextContent;
|
|
205
|
+
try {
|
|
206
|
+
nextContent = addRegistrationAST(content, tagName, modulePath, isESModule);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (error instanceof FurnaceError) {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
const parserError = toError(error);
|
|
213
|
+
throw new FurnaceError(`Failed to update ${CUSTOM_ELEMENTS_JS} using AST registration parsing: ${parserError.message}`, tagName, parserError);
|
|
214
|
+
}
|
|
215
|
+
validateRegistrationPlacement(nextContent, tagName, isESModule);
|
|
216
|
+
await writeText(filePath, nextContent);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=furnace-registration-ast.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removal of custom element registrations from customElements.js.
|
|
3
|
+
* Supports three removal strategies: standalone callback, single-line array, multi-line array.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Removes a custom element registration from customElements.js.
|
|
7
|
+
*
|
|
8
|
+
* This operation is idempotent — if the tag is not registered or the file does
|
|
9
|
+
* not exist, nothing happens.
|
|
10
|
+
*
|
|
11
|
+
* @param engineDir - Path to the Firefox engine source root
|
|
12
|
+
* @param tagName - Custom element tag name to remove
|
|
13
|
+
*/
|
|
14
|
+
export declare function removeCustomElementRegistration(engineDir: string, tagName: string): Promise<void>;
|