@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.
Files changed (316) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +294 -0
  3. package/README.md +435 -0
  4. package/dist/bin/fireforge.d.ts +10 -0
  5. package/dist/bin/fireforge.js +29 -0
  6. package/dist/src/cli.d.ts +33 -0
  7. package/dist/src/cli.js +180 -0
  8. package/dist/src/commands/bootstrap.d.ts +9 -0
  9. package/dist/src/commands/bootstrap.js +73 -0
  10. package/dist/src/commands/build.d.ts +11 -0
  11. package/dist/src/commands/build.js +102 -0
  12. package/dist/src/commands/config.d.ts +13 -0
  13. package/dist/src/commands/config.js +135 -0
  14. package/dist/src/commands/discard.d.ts +12 -0
  15. package/dist/src/commands/discard.js +84 -0
  16. package/dist/src/commands/doctor.d.ts +18 -0
  17. package/dist/src/commands/doctor.js +356 -0
  18. package/dist/src/commands/download.d.ts +11 -0
  19. package/dist/src/commands/download.js +127 -0
  20. package/dist/src/commands/export-all.d.ts +11 -0
  21. package/dist/src/commands/export-all.js +122 -0
  22. package/dist/src/commands/export-shared.d.ts +48 -0
  23. package/dist/src/commands/export-shared.js +208 -0
  24. package/dist/src/commands/export.d.ts +13 -0
  25. package/dist/src/commands/export.js +178 -0
  26. package/dist/src/commands/furnace/apply.d.ts +7 -0
  27. package/dist/src/commands/furnace/apply.js +80 -0
  28. package/dist/src/commands/furnace/create.d.ts +8 -0
  29. package/dist/src/commands/furnace/create.js +377 -0
  30. package/dist/src/commands/furnace/deploy.d.ts +8 -0
  31. package/dist/src/commands/furnace/deploy.js +338 -0
  32. package/dist/src/commands/furnace/diff.d.ts +7 -0
  33. package/dist/src/commands/furnace/diff.js +119 -0
  34. package/dist/src/commands/furnace/index.d.ts +16 -0
  35. package/dist/src/commands/furnace/index.js +121 -0
  36. package/dist/src/commands/furnace/list.d.ts +5 -0
  37. package/dist/src/commands/furnace/list.js +65 -0
  38. package/dist/src/commands/furnace/override.d.ts +8 -0
  39. package/dist/src/commands/furnace/override.js +188 -0
  40. package/dist/src/commands/furnace/preview.d.ts +7 -0
  41. package/dist/src/commands/furnace/preview.js +96 -0
  42. package/dist/src/commands/furnace/remove.d.ts +8 -0
  43. package/dist/src/commands/furnace/remove.js +159 -0
  44. package/dist/src/commands/furnace/scan.d.ts +5 -0
  45. package/dist/src/commands/furnace/scan.js +112 -0
  46. package/dist/src/commands/furnace/status.d.ts +7 -0
  47. package/dist/src/commands/furnace/status.js +137 -0
  48. package/dist/src/commands/furnace/validate.d.ts +6 -0
  49. package/dist/src/commands/furnace/validate.js +91 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +7 -0
  51. package/dist/src/commands/furnace/validation-output.js +22 -0
  52. package/dist/src/commands/import.d.ts +11 -0
  53. package/dist/src/commands/import.js +241 -0
  54. package/dist/src/commands/lint.d.ts +10 -0
  55. package/dist/src/commands/lint.js +118 -0
  56. package/dist/src/commands/package.d.ts +11 -0
  57. package/dist/src/commands/package.js +80 -0
  58. package/dist/src/commands/re-export.d.ts +12 -0
  59. package/dist/src/commands/re-export.js +242 -0
  60. package/dist/src/commands/rebase/abort.d.ts +7 -0
  61. package/dist/src/commands/rebase/abort.js +49 -0
  62. package/dist/src/commands/rebase/confirm.d.ts +18 -0
  63. package/dist/src/commands/rebase/confirm.js +33 -0
  64. package/dist/src/commands/rebase/continue.d.ts +7 -0
  65. package/dist/src/commands/rebase/continue.js +81 -0
  66. package/dist/src/commands/rebase/index.d.ts +22 -0
  67. package/dist/src/commands/rebase/index.js +127 -0
  68. package/dist/src/commands/rebase/patch-loop.d.ts +9 -0
  69. package/dist/src/commands/rebase/patch-loop.js +135 -0
  70. package/dist/src/commands/rebase/summary.d.ts +12 -0
  71. package/dist/src/commands/rebase/summary.js +43 -0
  72. package/dist/src/commands/rebase.d.ts +4 -0
  73. package/dist/src/commands/rebase.js +6 -0
  74. package/dist/src/commands/register.d.ts +13 -0
  75. package/dist/src/commands/register.js +67 -0
  76. package/dist/src/commands/reset.d.ts +11 -0
  77. package/dist/src/commands/reset.js +83 -0
  78. package/dist/src/commands/resolve.d.ts +9 -0
  79. package/dist/src/commands/resolve.js +124 -0
  80. package/dist/src/commands/run.d.ts +9 -0
  81. package/dist/src/commands/run.js +91 -0
  82. package/dist/src/commands/setup-support.d.ts +23 -0
  83. package/dist/src/commands/setup-support.js +310 -0
  84. package/dist/src/commands/setup.d.ts +11 -0
  85. package/dist/src/commands/setup.js +94 -0
  86. package/dist/src/commands/status.d.ts +11 -0
  87. package/dist/src/commands/status.js +268 -0
  88. package/dist/src/commands/test.d.ts +12 -0
  89. package/dist/src/commands/test.js +182 -0
  90. package/dist/src/commands/token-coverage.d.ts +5 -0
  91. package/dist/src/commands/token-coverage.js +57 -0
  92. package/dist/src/commands/token.d.ts +14 -0
  93. package/dist/src/commands/token.js +121 -0
  94. package/dist/src/commands/watch.d.ts +9 -0
  95. package/dist/src/commands/watch.js +112 -0
  96. package/dist/src/commands/wire.d.ts +13 -0
  97. package/dist/src/commands/wire.js +149 -0
  98. package/dist/src/core/ast-utils.d.ts +47 -0
  99. package/dist/src/core/ast-utils.js +57 -0
  100. package/dist/src/core/brand-validation.d.ts +7 -0
  101. package/dist/src/core/brand-validation.js +15 -0
  102. package/dist/src/core/branding.d.ts +49 -0
  103. package/dist/src/core/branding.js +229 -0
  104. package/dist/src/core/browser-wire.d.ts +40 -0
  105. package/dist/src/core/browser-wire.js +66 -0
  106. package/dist/src/core/build-prepare.d.ts +25 -0
  107. package/dist/src/core/build-prepare.js +93 -0
  108. package/dist/src/core/config-mutate.d.ts +15 -0
  109. package/dist/src/core/config-mutate.js +51 -0
  110. package/dist/src/core/config-paths.d.ts +28 -0
  111. package/dist/src/core/config-paths.js +65 -0
  112. package/dist/src/core/config-state.d.ts +28 -0
  113. package/dist/src/core/config-state.js +152 -0
  114. package/dist/src/core/config-validate.d.ts +11 -0
  115. package/dist/src/core/config-validate.js +141 -0
  116. package/dist/src/core/config.d.ts +39 -0
  117. package/dist/src/core/config.js +70 -0
  118. package/dist/src/core/file-lock.d.ts +11 -0
  119. package/dist/src/core/file-lock.js +80 -0
  120. package/dist/src/core/firefox-archive.d.ts +40 -0
  121. package/dist/src/core/firefox-archive.js +63 -0
  122. package/dist/src/core/firefox-cache.d.ts +23 -0
  123. package/dist/src/core/firefox-cache.js +134 -0
  124. package/dist/src/core/firefox-download.d.ts +21 -0
  125. package/dist/src/core/firefox-download.js +129 -0
  126. package/dist/src/core/firefox-extract.d.ts +21 -0
  127. package/dist/src/core/firefox-extract.js +53 -0
  128. package/dist/src/core/firefox.d.ts +34 -0
  129. package/dist/src/core/firefox.js +78 -0
  130. package/dist/src/core/furnace-apply-helpers.d.ts +21 -0
  131. package/dist/src/core/furnace-apply-helpers.js +244 -0
  132. package/dist/src/core/furnace-apply.d.ts +16 -0
  133. package/dist/src/core/furnace-apply.js +147 -0
  134. package/dist/src/core/furnace-config.d.ts +94 -0
  135. package/dist/src/core/furnace-config.js +372 -0
  136. package/dist/src/core/furnace-constants.d.ts +4 -0
  137. package/dist/src/core/furnace-constants.js +6 -0
  138. package/dist/src/core/furnace-registration-ast.d.ts +24 -0
  139. package/dist/src/core/furnace-registration-ast.js +218 -0
  140. package/dist/src/core/furnace-registration-remove.d.ts +14 -0
  141. package/dist/src/core/furnace-registration-remove.js +89 -0
  142. package/dist/src/core/furnace-registration-validate.d.ts +20 -0
  143. package/dist/src/core/furnace-registration-validate.js +40 -0
  144. package/dist/src/core/furnace-registration.d.ts +29 -0
  145. package/dist/src/core/furnace-registration.js +96 -0
  146. package/dist/src/core/furnace-rollback.d.ts +20 -0
  147. package/dist/src/core/furnace-rollback.js +66 -0
  148. package/dist/src/core/furnace-scanner.d.ts +40 -0
  149. package/dist/src/core/furnace-scanner.js +143 -0
  150. package/dist/src/core/furnace-stories.d.ts +37 -0
  151. package/dist/src/core/furnace-stories.js +185 -0
  152. package/dist/src/core/furnace-validate-accessibility.d.ts +6 -0
  153. package/dist/src/core/furnace-validate-accessibility.js +32 -0
  154. package/dist/src/core/furnace-validate-checks.d.ts +4 -0
  155. package/dist/src/core/furnace-validate-checks.js +7 -0
  156. package/dist/src/core/furnace-validate-compatibility.d.ts +6 -0
  157. package/dist/src/core/furnace-validate-compatibility.js +57 -0
  158. package/dist/src/core/furnace-validate-helpers.d.ts +28 -0
  159. package/dist/src/core/furnace-validate-helpers.js +129 -0
  160. package/dist/src/core/furnace-validate-registration.d.ts +37 -0
  161. package/dist/src/core/furnace-validate-registration.js +220 -0
  162. package/dist/src/core/furnace-validate-structure.d.ts +6 -0
  163. package/dist/src/core/furnace-validate-structure.js +66 -0
  164. package/dist/src/core/furnace-validate.d.ts +16 -0
  165. package/dist/src/core/furnace-validate.js +103 -0
  166. package/dist/src/core/git-base.d.ts +47 -0
  167. package/dist/src/core/git-base.js +50 -0
  168. package/dist/src/core/git-diff.d.ts +63 -0
  169. package/dist/src/core/git-diff.js +246 -0
  170. package/dist/src/core/git-file-ops.d.ts +65 -0
  171. package/dist/src/core/git-file-ops.js +141 -0
  172. package/dist/src/core/git-status.d.ts +65 -0
  173. package/dist/src/core/git-status.js +163 -0
  174. package/dist/src/core/git.d.ts +113 -0
  175. package/dist/src/core/git.js +363 -0
  176. package/dist/src/core/license-headers.d.ts +36 -0
  177. package/dist/src/core/license-headers.js +83 -0
  178. package/dist/src/core/mach-build-artifacts.d.ts +29 -0
  179. package/dist/src/core/mach-build-artifacts.js +117 -0
  180. package/dist/src/core/mach-mozconfig.d.ts +17 -0
  181. package/dist/src/core/mach-mozconfig.js +50 -0
  182. package/dist/src/core/mach-python.d.ts +16 -0
  183. package/dist/src/core/mach-python.js +126 -0
  184. package/dist/src/core/mach.d.ts +106 -0
  185. package/dist/src/core/mach.js +166 -0
  186. package/dist/src/core/manifest-helpers.d.ts +25 -0
  187. package/dist/src/core/manifest-helpers.js +96 -0
  188. package/dist/src/core/manifest-register.d.ts +30 -0
  189. package/dist/src/core/manifest-register.js +65 -0
  190. package/dist/src/core/manifest-rules.d.ts +39 -0
  191. package/dist/src/core/manifest-rules.js +151 -0
  192. package/dist/src/core/manifest-tokenizers.d.ts +34 -0
  193. package/dist/src/core/manifest-tokenizers.js +84 -0
  194. package/dist/src/core/parser-fallback.d.ts +36 -0
  195. package/dist/src/core/parser-fallback.js +43 -0
  196. package/dist/src/core/patch-apply-fuzz.d.ts +29 -0
  197. package/dist/src/core/patch-apply-fuzz.js +70 -0
  198. package/dist/src/core/patch-apply.d.ts +46 -0
  199. package/dist/src/core/patch-apply.js +235 -0
  200. package/dist/src/core/patch-export.d.ts +99 -0
  201. package/dist/src/core/patch-export.js +314 -0
  202. package/dist/src/core/patch-files.d.ts +11 -0
  203. package/dist/src/core/patch-files.js +51 -0
  204. package/dist/src/core/patch-lint.d.ts +72 -0
  205. package/dist/src/core/patch-lint.js +403 -0
  206. package/dist/src/core/patch-lock.d.ts +8 -0
  207. package/dist/src/core/patch-lock.js +29 -0
  208. package/dist/src/core/patch-manifest-consistency.d.ts +24 -0
  209. package/dist/src/core/patch-manifest-consistency.js +135 -0
  210. package/dist/src/core/patch-manifest-io.d.ts +36 -0
  211. package/dist/src/core/patch-manifest-io.js +77 -0
  212. package/dist/src/core/patch-manifest-query.d.ts +48 -0
  213. package/dist/src/core/patch-manifest-query.js +124 -0
  214. package/dist/src/core/patch-manifest-validate.d.ts +22 -0
  215. package/dist/src/core/patch-manifest-validate.js +72 -0
  216. package/dist/src/core/patch-manifest.d.ts +11 -0
  217. package/dist/src/core/patch-manifest.js +12 -0
  218. package/dist/src/core/patch-parse.d.ts +43 -0
  219. package/dist/src/core/patch-parse.js +143 -0
  220. package/dist/src/core/patch-transform.d.ts +21 -0
  221. package/dist/src/core/patch-transform.js +138 -0
  222. package/dist/src/core/rebase-session.d.ts +47 -0
  223. package/dist/src/core/rebase-session.js +65 -0
  224. package/dist/src/core/register-browser-content.d.ts +11 -0
  225. package/dist/src/core/register-browser-content.js +116 -0
  226. package/dist/src/core/register-module.d.ts +11 -0
  227. package/dist/src/core/register-module.js +76 -0
  228. package/dist/src/core/register-shared-css.d.ts +11 -0
  229. package/dist/src/core/register-shared-css.js +117 -0
  230. package/dist/src/core/register-test-manifest.d.ts +18 -0
  231. package/dist/src/core/register-test-manifest.js +99 -0
  232. package/dist/src/core/state-file.d.ts +4 -0
  233. package/dist/src/core/state-file.js +25 -0
  234. package/dist/src/core/token-coverage.d.ts +12 -0
  235. package/dist/src/core/token-coverage.js +74 -0
  236. package/dist/src/core/token-manager.d.ts +55 -0
  237. package/dist/src/core/token-manager.js +387 -0
  238. package/dist/src/core/wire-destroy.d.ts +21 -0
  239. package/dist/src/core/wire-destroy.js +103 -0
  240. package/dist/src/core/wire-dom-fragment.d.ts +23 -0
  241. package/dist/src/core/wire-dom-fragment.js +129 -0
  242. package/dist/src/core/wire-init.d.ts +23 -0
  243. package/dist/src/core/wire-init.js +201 -0
  244. package/dist/src/core/wire-subscript.d.ts +20 -0
  245. package/dist/src/core/wire-subscript.js +134 -0
  246. package/dist/src/core/wire-targets.d.ts +7 -0
  247. package/dist/src/core/wire-targets.js +9 -0
  248. package/dist/src/core/wire-utils.d.ts +88 -0
  249. package/dist/src/core/wire-utils.js +279 -0
  250. package/dist/src/errors/base.d.ts +60 -0
  251. package/dist/src/errors/base.js +87 -0
  252. package/dist/src/errors/build.d.ts +52 -0
  253. package/dist/src/errors/build.js +114 -0
  254. package/dist/src/errors/codes.d.ts +29 -0
  255. package/dist/src/errors/codes.js +30 -0
  256. package/dist/src/errors/config.d.ts +31 -0
  257. package/dist/src/errors/config.js +61 -0
  258. package/dist/src/errors/download.d.ts +42 -0
  259. package/dist/src/errors/download.js +95 -0
  260. package/dist/src/errors/furnace.d.ts +10 -0
  261. package/dist/src/errors/furnace.js +22 -0
  262. package/dist/src/errors/git.d.ts +41 -0
  263. package/dist/src/errors/git.js +99 -0
  264. package/dist/src/errors/patch.d.ts +10 -0
  265. package/dist/src/errors/patch.js +26 -0
  266. package/dist/src/errors/rebase.d.ts +20 -0
  267. package/dist/src/errors/rebase.js +30 -0
  268. package/dist/src/index.d.ts +21 -0
  269. package/dist/src/index.js +21 -0
  270. package/dist/src/types/cli.d.ts +14 -0
  271. package/dist/src/types/cli.js +2 -0
  272. package/dist/src/types/commands/index.d.ts +6 -0
  273. package/dist/src/types/commands/index.js +6 -0
  274. package/dist/src/types/commands/options.d.ts +239 -0
  275. package/dist/src/types/commands/options.js +6 -0
  276. package/dist/src/types/commands/patches.d.ts +89 -0
  277. package/dist/src/types/commands/patches.js +6 -0
  278. package/dist/src/types/commands/project.d.ts +71 -0
  279. package/dist/src/types/commands/project.js +6 -0
  280. package/dist/src/types/config.d.ts +101 -0
  281. package/dist/src/types/config.js +2 -0
  282. package/dist/src/types/furnace.d.ts +158 -0
  283. package/dist/src/types/furnace.js +2 -0
  284. package/dist/src/types/index.d.ts +6 -0
  285. package/dist/src/types/index.js +6 -0
  286. package/dist/src/utils/errors.d.ts +2 -0
  287. package/dist/src/utils/errors.js +15 -0
  288. package/dist/src/utils/fs.d.ts +72 -0
  289. package/dist/src/utils/fs.js +179 -0
  290. package/dist/src/utils/logger.d.ts +58 -0
  291. package/dist/src/utils/logger.js +120 -0
  292. package/dist/src/utils/options.d.ts +8 -0
  293. package/dist/src/utils/options.js +16 -0
  294. package/dist/src/utils/package-root.d.ts +10 -0
  295. package/dist/src/utils/package-root.js +53 -0
  296. package/dist/src/utils/parse.d.ts +110 -0
  297. package/dist/src/utils/parse.js +200 -0
  298. package/dist/src/utils/paths.d.ts +10 -0
  299. package/dist/src/utils/paths.js +43 -0
  300. package/dist/src/utils/platform.d.ts +38 -0
  301. package/dist/src/utils/platform.js +56 -0
  302. package/dist/src/utils/process.d.ts +80 -0
  303. package/dist/src/utils/process.js +188 -0
  304. package/dist/src/utils/regex.d.ts +24 -0
  305. package/dist/src/utils/regex.js +40 -0
  306. package/dist/src/utils/validation.d.ts +133 -0
  307. package/dist/src/utils/validation.js +250 -0
  308. package/package.json +106 -0
  309. package/templates/configs/common.mozconfig +24 -0
  310. package/templates/configs/darwin.mozconfig +10 -0
  311. package/templates/configs/linux.mozconfig +12 -0
  312. package/templates/configs/win32.mozconfig +14 -0
  313. package/templates/licenses/0BSD.md +14 -0
  314. package/templates/licenses/EUPL-1.2.md +294 -0
  315. package/templates/licenses/GPL-2.0-or-later.md +339 -0
  316. 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,4 @@
1
+ /** Path to customElements.js within the engine source tree */
2
+ export declare const CUSTOM_ELEMENTS_JS = "toolkit/content/customElements.js";
3
+ /** Path to jar.mn within the engine source tree (toolkit global) */
4
+ export declare const JAR_MN = "toolkit/content/jar.mn";
@@ -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>;