@hominis/fireforge 0.30.1 → 0.32.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 (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +9 -16
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +46 -1
  10. package/dist/src/commands/export.js +52 -113
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +3 -1
  20. package/dist/src/commands/lint-per-patch.js +265 -74
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +193 -88
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-files.js +4 -1
  42. package/dist/src/commands/re-export-scan.js +8 -1
  43. package/dist/src/commands/re-export.js +8 -1
  44. package/dist/src/commands/rebase/summary.d.ts +1 -5
  45. package/dist/src/commands/rebase/summary.js +1 -1
  46. package/dist/src/commands/status-output.js +77 -68
  47. package/dist/src/commands/test-diagnose.d.ts +23 -0
  48. package/dist/src/commands/test-diagnose.js +210 -0
  49. package/dist/src/commands/test-run.d.ts +68 -0
  50. package/dist/src/commands/test-run.js +97 -0
  51. package/dist/src/commands/test.js +214 -263
  52. package/dist/src/commands/token.js +15 -1
  53. package/dist/src/commands/wire.js +109 -78
  54. package/dist/src/core/build-audit.d.ts +1 -1
  55. package/dist/src/core/build-audit.js +2 -46
  56. package/dist/src/core/build-baseline-types.d.ts +38 -0
  57. package/dist/src/core/build-baseline-types.js +10 -0
  58. package/dist/src/core/build-baseline.d.ts +1 -31
  59. package/dist/src/core/build-prepare.d.ts +1 -1
  60. package/dist/src/core/build-prepare.js +2 -45
  61. package/dist/src/core/config-paths.d.ts +0 -8
  62. package/dist/src/core/config-paths.js +4 -4
  63. package/dist/src/core/config-state.d.ts +0 -6
  64. package/dist/src/core/config-state.js +1 -1
  65. package/dist/src/core/config-validate-patch-policy.js +12 -13
  66. package/dist/src/core/config-validate.js +74 -28
  67. package/dist/src/core/engine-changes.d.ts +24 -0
  68. package/dist/src/core/engine-changes.js +64 -0
  69. package/dist/src/core/firefox-cache.d.ts +0 -5
  70. package/dist/src/core/firefox-cache.js +1 -1
  71. package/dist/src/core/firefox-download.d.ts +0 -6
  72. package/dist/src/core/firefox-download.js +1 -1
  73. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  74. package/dist/src/core/furnace-apply-helpers.js +11 -20
  75. package/dist/src/core/furnace-apply.d.ts +1 -1
  76. package/dist/src/core/furnace-apply.js +1 -1
  77. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  78. package/dist/src/core/furnace-checksum-utils.js +15 -0
  79. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  80. package/dist/src/core/furnace-config-validate.js +133 -0
  81. package/dist/src/core/furnace-config.d.ts +4 -32
  82. package/dist/src/core/furnace-config.js +15 -111
  83. package/dist/src/core/furnace-constants.d.ts +0 -10
  84. package/dist/src/core/furnace-constants.js +2 -2
  85. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  86. package/dist/src/core/furnace-css-fragments.js +243 -0
  87. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  88. package/dist/src/core/furnace-jsconfig.js +191 -0
  89. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  90. package/dist/src/core/furnace-validate-helpers.js +40 -1
  91. package/dist/src/core/furnace-validate-registration.js +16 -1
  92. package/dist/src/core/furnace-validate.js +54 -2
  93. package/dist/src/core/git-base.d.ts +15 -0
  94. package/dist/src/core/git-base.js +32 -0
  95. package/dist/src/core/git-diff.d.ts +8 -0
  96. package/dist/src/core/git-diff.js +224 -59
  97. package/dist/src/core/git-file-ops.d.ts +39 -12
  98. package/dist/src/core/git-file-ops.js +84 -3
  99. package/dist/src/core/lint-cache.d.ts +0 -13
  100. package/dist/src/core/lint-cache.js +5 -5
  101. package/dist/src/core/mach.d.ts +22 -1
  102. package/dist/src/core/mach.js +27 -2
  103. package/dist/src/core/manifest-register.d.ts +5 -16
  104. package/dist/src/core/manifest-register.js +3 -1
  105. package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
  106. package/dist/src/core/patch-lint-checkjs.js +263 -71
  107. package/dist/src/core/patch-lint-css.d.ts +23 -0
  108. package/dist/src/core/patch-lint-css.js +172 -0
  109. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  110. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  111. package/dist/src/core/patch-lint-observer.js +168 -0
  112. package/dist/src/core/patch-lint.d.ts +34 -11
  113. package/dist/src/core/patch-lint.js +24 -161
  114. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  115. package/dist/src/core/patch-manifest-io.js +44 -2
  116. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  117. package/dist/src/core/patch-manifest-validate.js +1 -1
  118. package/dist/src/core/patch-manifest.d.ts +1 -1
  119. package/dist/src/core/patch-manifest.js +1 -1
  120. package/dist/src/core/patch-policy.d.ts +0 -4
  121. package/dist/src/core/patch-policy.js +10 -4
  122. package/dist/src/core/register-browser-content.d.ts +1 -1
  123. package/dist/src/core/register-module.d.ts +1 -1
  124. package/dist/src/core/register-result.d.ts +21 -0
  125. package/dist/src/core/register-result.js +9 -0
  126. package/dist/src/core/register-shared-css.d.ts +1 -1
  127. package/dist/src/core/register-test-manifest.d.ts +1 -1
  128. package/dist/src/core/test-harness-crash.d.ts +61 -0
  129. package/dist/src/core/test-harness-crash.js +140 -0
  130. package/dist/src/core/test-stale-check.d.ts +1 -1
  131. package/dist/src/core/test-stale-check.js +2 -46
  132. package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
  133. package/dist/src/core/test-xpcshell-retry.js +10 -3
  134. package/dist/src/core/token-dark-mode.js +14 -26
  135. package/dist/src/core/token-manager.d.ts +4 -0
  136. package/dist/src/core/token-manager.js +70 -16
  137. package/dist/src/core/typecheck-shim.d.ts +3 -22
  138. package/dist/src/core/typecheck-shim.js +69 -7
  139. package/dist/src/core/wire-utils.js +37 -44
  140. package/dist/src/types/commands/index.d.ts +1 -1
  141. package/dist/src/types/commands/options.d.ts +122 -0
  142. package/dist/src/types/config.d.ts +11 -2
  143. package/dist/src/types/furnace.d.ts +12 -1
  144. package/dist/src/utils/elapsed.d.ts +0 -2
  145. package/dist/src/utils/elapsed.js +1 -1
  146. package/dist/src/utils/fs.d.ts +0 -5
  147. package/dist/src/utils/fs.js +1 -1
  148. package/dist/src/utils/regex.d.ts +0 -6
  149. package/dist/src/utils/regex.js +3 -3
  150. package/dist/src/utils/validation.d.ts +0 -8
  151. package/dist/src/utils/validation.js +2 -2
  152. package/package.json +6 -4
@@ -10,20 +10,13 @@ import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LIC
10
10
  import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
11
11
  import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
12
12
  /**
13
- * Validates a raw config object and returns a typed FireForgeConfig.
14
- * @param data - Raw data to validate
15
- * @returns Validated FireForgeConfig
16
- * @throws Error if validation fails
13
+ * Parses and validates the four required identity fields (`name`,
14
+ * `vendor`, `appId`, `binaryName`): all non-empty strings, with
15
+ * `binaryName` additionally barred from path separators/traversal and
16
+ * `appId` required to be a reverse-domain identifier.
17
17
  */
18
- export function validateConfig(data) {
19
- let rec;
20
- try {
21
- rec = parseObject(data, 'Config');
22
- }
23
- catch {
24
- throw new ConfigError('Config must be an object');
25
- }
26
- // Required string fields. Empty strings would technically pass the
18
+ function parseIdentityFields(rec) {
19
+ // Empty strings would technically pass the
27
20
  // typeof-check below but are never valid for any of these identifier
28
21
  // fields — rejecting them here prevents downstream code (Firefox build,
29
22
  // launcher binary lookup, AppID assertions) from failing with confusing
@@ -54,7 +47,14 @@ export function validateConfig(data) {
54
47
  if (!isValidAppId(appId)) {
55
48
  throw new ConfigError('Config field "appId" must be a valid reverse-domain identifier (e.g., "org.example.browser")');
56
49
  }
57
- // Firefox config
50
+ return { name, vendor, appId, binaryName };
51
+ }
52
+ /**
53
+ * Parses and validates the required `firefox` block: version shape,
54
+ * product allowlist, product/version cross-compatibility, and the
55
+ * optional sha256 digest (normalized to lowercase).
56
+ */
57
+ function parseFirefoxBlock(rec) {
58
58
  let firefoxRec;
59
59
  try {
60
60
  firefoxRec = rec.object('firefox');
@@ -80,19 +80,14 @@ export function validateConfig(data) {
80
80
  if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
81
81
  throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
82
82
  }
83
- // Optional configs
84
- const config = {
85
- name,
86
- vendor,
87
- appId,
88
- binaryName,
89
- firefox: {
90
- version: firefoxVersion,
91
- product: firefoxProduct,
92
- ...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
93
- },
83
+ return {
84
+ version: firefoxVersion,
85
+ product: firefoxProduct,
86
+ ...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
94
87
  };
95
- // Build
88
+ }
89
+ /** Parses the optional `build` block (currently just `build.jobs`). */
90
+ function parseBuildBlock(rec, config) {
96
91
  const buildRec = optionalConfigObject(rec, 'build');
97
92
  if (buildRec) {
98
93
  config.build = {};
@@ -104,7 +99,9 @@ export function validateConfig(data) {
104
99
  config.build.jobs = jobs;
105
100
  }
106
101
  }
107
- // Wire
102
+ }
103
+ /** Parses the optional `wire` block (currently just `wire.subscriptDir`). */
104
+ function parseWireBlock(rec, config) {
108
105
  const wireRec = optionalConfigObject(rec, 'wire');
109
106
  if (wireRec) {
110
107
  config.wire = {};
@@ -116,7 +113,9 @@ export function validateConfig(data) {
116
113
  config.wire.subscriptDir = subscriptDir;
117
114
  }
118
115
  }
119
- // License
116
+ }
117
+ /** Parses the optional `license` field against the supported-license list. */
118
+ function parseLicenseField(rec, config) {
120
119
  const licenseRaw = rec.raw('license');
121
120
  if (licenseRaw !== undefined) {
122
121
  if (typeof licenseRaw !== 'string') {
@@ -127,6 +126,27 @@ export function validateConfig(data) {
127
126
  }
128
127
  config.license = licenseRaw;
129
128
  }
129
+ }
130
+ /**
131
+ * Validates a raw config object and returns a typed FireForgeConfig.
132
+ * @param data - Raw data to validate
133
+ * @returns Validated FireForgeConfig
134
+ * @throws Error if validation fails
135
+ */
136
+ export function validateConfig(data) {
137
+ let rec;
138
+ try {
139
+ rec = parseObject(data, 'Config');
140
+ }
141
+ catch {
142
+ throw new ConfigError('Config must be an object');
143
+ }
144
+ const identity = parseIdentityFields(rec);
145
+ const firefox = parseFirefoxBlock(rec);
146
+ const config = { ...identity, firefox };
147
+ parseBuildBlock(rec, config);
148
+ parseWireBlock(rec, config);
149
+ parseLicenseField(rec, config);
130
150
  // Marker comment — appended to lines FireForge writes into upstream files.
131
151
  const markerComment = parseMarkerComment(rec.raw('markerComment'));
132
152
  if (markerComment !== undefined) {
@@ -223,6 +243,28 @@ const PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS = [
223
243
  'noUnusedLocals',
224
244
  'noUnusedParameters',
225
245
  ];
246
+ /**
247
+ * Validates the reviewed `paths` mapping: an object of pattern → string[]
248
+ * targets, each pattern carrying at most one `*`. Lets patch-owned modules
249
+ * be typed from their real sources without an ambient stub shim.
250
+ */
251
+ function parseCheckJsPathsMapping(raw) {
252
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
253
+ throw new ConfigError('Config field "patchLint.checkJsCompilerOptions.paths" must be a plain object');
254
+ }
255
+ const rec = raw;
256
+ const out = {};
257
+ for (const [pattern, targets] of Object.entries(rec)) {
258
+ if ((pattern.match(/\*/g) ?? []).length > 1) {
259
+ throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths" key "${pattern}" may contain at most one "*"`);
260
+ }
261
+ if (!Array.isArray(targets) || targets.some((t) => typeof t !== 'string')) {
262
+ throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths.${pattern}" must be an array of strings`);
263
+ }
264
+ out[pattern] = targets;
265
+ }
266
+ return out;
267
+ }
226
268
  function parsePatchLintCheckJsCompilerOptions(raw) {
227
269
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
228
270
  throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" must be a plain object');
@@ -231,6 +273,10 @@ function parsePatchLintCheckJsCompilerOptions(raw) {
231
273
  const allowed = new Set(PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS);
232
274
  const out = {};
233
275
  for (const key of Object.keys(rec)) {
276
+ if (key === 'paths') {
277
+ out.paths = parseCheckJsPathsMapping(rec[key]);
278
+ continue;
279
+ }
234
280
  if (!allowed.has(key)) {
235
281
  throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions" has unknown key "${key}"`);
236
282
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared collector for "what changed in the engine tree since the last
3
+ * successful build". Three preflight/audit paths previously carried their
4
+ * own copy of this logic (`build-audit`, `build-prepare`,
5
+ * `test-stale-check`), differing only in the verbose-log label; they all
6
+ * call this module now.
7
+ */
8
+ import type { BuildBaseline } from './build-baseline-types.js';
9
+ /**
10
+ * Collects engine-relative paths changed since the baseline's HEAD SHA,
11
+ * plus any workdir modifications (tracked and untracked). Defensive — git
12
+ * failures surface as verbose lines and return the files collected so far,
13
+ * so an empty result means "no drift we can prove" rather than "no drift
14
+ * occurred". When the baseline is missing or the engine has no HEAD yet,
15
+ * falls back to workdir-only collection. The result is sorted.
16
+ *
17
+ * @param engineDir - Path to the engine directory
18
+ * @param baseline - Last-build baseline, when one exists
19
+ * @param contextLabel - Prefix for verbose-log lines (e.g. `Audit`,
20
+ * `Auto-configure`, `Stale-build preflight`) so operators can attribute
21
+ * the probe that emitted them
22
+ * @returns Sorted engine-relative POSIX paths
23
+ */
24
+ export declare function collectChangedEnginePaths(engineDir: string, baseline: BuildBaseline | undefined, contextLabel: string): Promise<string[]>;
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Shared collector for "what changed in the engine tree since the last
4
+ * successful build". Three preflight/audit paths previously carried their
5
+ * own copy of this logic (`build-audit`, `build-prepare`,
6
+ * `test-stale-check`), differing only in the verbose-log label; they all
7
+ * call this module now.
8
+ */
9
+ import { toError } from '../utils/errors.js';
10
+ import { verbose } from '../utils/logger.js';
11
+ import { hasChanges, isMissingHeadError } from './git.js';
12
+ import { git } from './git-base.js';
13
+ import { getUntrackedFiles } from './git-status.js';
14
+ /**
15
+ * Collects engine-relative paths changed since the baseline's HEAD SHA,
16
+ * plus any workdir modifications (tracked and untracked). Defensive — git
17
+ * failures surface as verbose lines and return the files collected so far,
18
+ * so an empty result means "no drift we can prove" rather than "no drift
19
+ * occurred". When the baseline is missing or the engine has no HEAD yet,
20
+ * falls back to workdir-only collection. The result is sorted.
21
+ *
22
+ * @param engineDir - Path to the engine directory
23
+ * @param baseline - Last-build baseline, when one exists
24
+ * @param contextLabel - Prefix for verbose-log lines (e.g. `Audit`,
25
+ * `Auto-configure`, `Stale-build preflight`) so operators can attribute
26
+ * the probe that emitted them
27
+ * @returns Sorted engine-relative POSIX paths
28
+ */
29
+ export async function collectChangedEnginePaths(engineDir, baseline, contextLabel) {
30
+ const collected = new Set();
31
+ if (baseline?.engineHeadSha) {
32
+ try {
33
+ const output = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
34
+ for (const line of output.split('\n')) {
35
+ const trimmed = line.trim();
36
+ if (trimmed)
37
+ collected.add(trimmed);
38
+ }
39
+ }
40
+ catch (error) {
41
+ if (!isMissingHeadError(error)) {
42
+ verbose(`${contextLabel}: could not diff engine against baseline — ${toError(error).message}`);
43
+ }
44
+ }
45
+ }
46
+ try {
47
+ if (await hasChanges(engineDir)) {
48
+ const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
49
+ for (const line of worktreeDiff.split('\n')) {
50
+ const trimmed = line.trim();
51
+ if (trimmed)
52
+ collected.add(trimmed);
53
+ }
54
+ for (const untracked of await getUntrackedFiles(engineDir)) {
55
+ collected.add(untracked);
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ verbose(`${contextLabel}: could not enumerate workdir changes — ${toError(error).message}`);
61
+ }
62
+ return [...collected].sort();
63
+ }
64
+ //# sourceMappingURL=engine-changes.js.map
@@ -3,11 +3,6 @@
3
3
  */
4
4
  import type { ResolvedArchive } from './firefox-archive.js';
5
5
  import type { ProgressCallback } from './firefox-download.js';
6
- /**
7
- * Computes the SHA-256 hex digest of a file.
8
- * @param filePath - Path to the file
9
- */
10
- export declare function sha256File(filePath: string): Promise<string>;
11
6
  /**
12
7
  * Ensures a valid cached archive exists, downloading it if needed.
13
8
  * @param archive - Resolved archive descriptor
@@ -18,7 +18,7 @@ import { downloadFile } from './firefox-download.js';
18
18
  * Computes the SHA-256 hex digest of a file.
19
19
  * @param filePath - Path to the file
20
20
  */
21
- export async function sha256File(filePath) {
21
+ async function sha256File(filePath) {
22
22
  const hash = createHash('sha256');
23
23
  const stream = createReadStream(filePath);
24
24
  await pipeline(stream, hash);
@@ -5,12 +5,6 @@
5
5
  * Progress callback for download operations.
6
6
  */
7
7
  export type ProgressCallback = (downloaded: number, total: number) => void;
8
- /**
9
- * Fetches a URL with timeout and bounded retry for transient failures.
10
- *
11
- * Non-retryable errors (e.g. 404) are thrown immediately.
12
- */
13
- export declare function fetchWithRetry(url: string): Promise<Response>;
14
8
  /**
15
9
  * Downloads a file from a URL with progress tracking, timeout, and retry.
16
10
  * @param url - URL to download
@@ -22,7 +22,7 @@ const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
22
22
  *
23
23
  * Non-retryable errors (e.g. 404) are thrown immediately.
24
24
  */
25
- export async function fetchWithRetry(url) {
25
+ async function fetchWithRetry(url) {
26
26
  let lastError;
27
27
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
28
28
  const controller = new AbortController();
@@ -29,13 +29,6 @@ export declare function getOverrideEngineTargetPath(engineDir: string, config: O
29
29
  export declare function restoreOverrideFileToBaseline(engineDir: string, enginePath: string, journal: RollbackJournal): Promise<'restored' | 'removed' | 'noop'>;
30
30
  /** Computes stable checksums for the source files that define a component. */
31
31
  export declare function computeComponentChecksums(componentDir: string): Promise<Record<string, string>>;
32
- /**
33
- * Returns the filenames present in `previous` that are absent from `current`
34
- * — i.e. files we know we deployed last time but the workspace has since
35
- * deleted. The order of returned names is intentionally stable
36
- * (sorted alphabetically) so test snapshots and CLI output are deterministic.
37
- */
38
- export declare function diffDeletedFiles(previous: Record<string, string>, current: Record<string, string>): string[];
39
32
  /**
40
33
  * Removes engine copies of files that the developer has deleted from a custom
41
34
  * component's workspace since the last apply. `.ftl` files live under the
@@ -110,4 +103,4 @@ export declare function applyOverrideComponent(engineDir: string, name: string,
110
103
  affectedPaths: string[];
111
104
  actions?: DryRunAction[];
112
105
  }>;
113
- export { extractComponentChecksums, prefixChecksums } from './furnace-checksum-utils.js';
106
+ export { diffDeletedFiles, extractComponentChecksums, prefixChecksums, } from './furnace-checksum-utils.js';
@@ -8,6 +8,7 @@ import { copyFile, ensureDir, pathExists, readText, removeFile } from '../utils/
8
8
  import { verbose } from '../utils/logger.js';
9
9
  import { applyCustomFtlFile, describeLocaleFtlJarMnRegistration, removeCustomFtlJarMnEntry, } from './furnace-apply-ftl.js';
10
10
  import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
11
+ import { deployFileWithFragments, describeFragmentExpansion, SHARED_FRAGMENTS_DIR, } from './furnace-css-fragments.js';
11
12
  import { addCustomElementRegistration, addJarMnEntries, validateCustomElementRegistration, validateJarMnInsertionForFiles, } from './furnace-registration.js';
12
13
  import { recordCreatedDir, snapshotFile } from './furnace-rollback.js';
13
14
  import { checkRegistrationConsistency } from './furnace-validate-registration.js';
@@ -97,21 +98,6 @@ export async function computeComponentChecksums(componentDir) {
97
98
  }
98
99
  return checksums;
99
100
  }
100
- /**
101
- * Returns the filenames present in `previous` that are absent from `current`
102
- * — i.e. files we know we deployed last time but the workspace has since
103
- * deleted. The order of returned names is intentionally stable
104
- * (sorted alphabetically) so test snapshots and CLI output are deterministic.
105
- */
106
- export function diffDeletedFiles(previous, current) {
107
- const deleted = [];
108
- for (const key of Object.keys(previous)) {
109
- if (!(key in current)) {
110
- deleted.push(key);
111
- }
112
- }
113
- return deleted.sort();
114
- }
115
101
  /**
116
102
  * Removes engine copies of files that the developer has deleted from a custom
117
103
  * component's workspace since the last apply. `.ftl` files live under the
@@ -300,12 +286,13 @@ async function buildCustomDryRunActions(name, componentDir, engineDir, config, t
300
286
  continue;
301
287
  if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.css'))
302
288
  continue;
289
+ const fragmentNote = await describeFragmentExpansion(join(componentDir, entry.name));
303
290
  actions.push({
304
291
  component: name,
305
- action: 'copy',
292
+ action: fragmentNote ? 'expand-fragments' : 'copy',
306
293
  source: join(componentDir, entry.name),
307
294
  target: join(targetDir, entry.name),
308
- description: `Copy ${entry.name} to ${config.targetPath}`,
295
+ description: `Copy ${entry.name} to ${config.targetPath}${fragmentNote}`,
309
296
  });
310
297
  }
311
298
  // Per-component .ftl handling is skipped when the component opts into a
@@ -401,11 +388,15 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
401
388
  await snapshotFile(rollbackJournal, dest);
402
389
  }
403
390
  }
404
- // Copy phase (parallel — independent file writes to different paths)
391
+ // Copy phase (parallel — independent file writes to different paths).
392
+ // CSS files carrying @fireforge-include directives are written as their
393
+ // fragment-expanded form (field report D2); the workspace source keeps
394
+ // only the directive, so shared CSS stays single-sourced.
395
+ const sharedDir = join(componentDir, '..', '..', SHARED_FRAGMENTS_DIR);
405
396
  await Promise.all(filesToCopy.map(async (entry) => {
406
397
  const src = join(componentDir, entry.name);
407
398
  const dest = join(targetDir, entry.name);
408
- await copyFile(src, dest);
399
+ await deployFileWithFragments(src, dest, sharedDir);
409
400
  affectedPaths.push(relative(engineDir, dest));
410
401
  copiedFileNames.push(entry.name);
411
402
  }));
@@ -499,5 +490,5 @@ export async function applyOverrideComponent(engineDir, name, componentDir, conf
499
490
  }
500
491
  return { affectedPaths };
501
492
  }
502
- export { extractComponentChecksums, prefixChecksums } from './furnace-checksum-utils.js';
493
+ export { diffDeletedFiles, extractComponentChecksums, prefixChecksums, } from './furnace-checksum-utils.js';
503
494
  //# sourceMappingURL=furnace-apply-helpers.js.map
@@ -1,7 +1,7 @@
1
1
  import type { ApplyResult, DryRunAction } from '../types/furnace.js';
2
2
  import { type FurnaceOperationContext } from './furnace-operation.js';
3
3
  import { type RollbackJournal } from './furnace-rollback.js';
4
- export { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, } from './furnace-apply-helpers.js';
4
+ export { computeComponentChecksums, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, } from './furnace-apply-helpers.js';
5
5
  /**
6
6
  * Applies all override and custom components to the engine source tree.
7
7
  *
@@ -14,7 +14,7 @@ import { recordFurnaceRollbackFailure } from './furnace-operation.js';
14
14
  import { addJarMnEntries, removeCustomElementRegistration, removeJarMnEntries, } from './furnace-registration.js';
15
15
  import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from './furnace-rollback.js';
16
16
  import { runPostApplyConsistencyChecks } from './furnace-validate-registration.js';
17
- export { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, } from './furnace-apply-helpers.js';
17
+ export { computeComponentChecksums, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, } from './furnace-apply-helpers.js';
18
18
  function addMissingComponentError(result, name, directoryPath) {
19
19
  result.errors.push({
20
20
  name,
@@ -2,3 +2,10 @@
2
2
  export declare function extractComponentChecksums(allChecksums: Record<string, string> | undefined, type: string, name: string): Record<string, string>;
3
3
  /** Prefixes component checksums so they can be stored in the flattened state format. */
4
4
  export declare function prefixChecksums(checksums: Record<string, string>, type: string, name: string): Record<string, string>;
5
+ /**
6
+ * Returns the filenames present in `previous` that are absent from `current`
7
+ * — i.e. files we know we deployed last time but the workspace has since
8
+ * deleted. The order of returned names is intentionally stable
9
+ * (sorted alphabetically) so test snapshots and CLI output are deterministic.
10
+ */
11
+ export declare function diffDeletedFiles(previous: Record<string, string>, current: Record<string, string>): string[];
@@ -21,4 +21,19 @@ export function prefixChecksums(checksums, type, name) {
21
21
  }
22
22
  return result;
23
23
  }
24
+ /**
25
+ * Returns the filenames present in `previous` that are absent from `current`
26
+ * — i.e. files we know we deployed last time but the workspace has since
27
+ * deleted. The order of returned names is intentionally stable
28
+ * (sorted alphabetically) so test snapshots and CLI output are deterministic.
29
+ */
30
+ export function diffDeletedFiles(previous, current) {
31
+ const deleted = [];
32
+ for (const key of Object.keys(previous)) {
33
+ if (!(key in current)) {
34
+ deleted.push(key);
35
+ }
36
+ }
37
+ return deleted.sort();
38
+ }
24
39
  //# sourceMappingURL=furnace-checksum-utils.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Section validators for `validateFurnaceConfig`, split out of
3
+ * `furnace-config.ts` to keep that module inside the per-file line
4
+ * budget. Each helper owns one shape: the override entry, the stock
5
+ * list, the named-component maps, and the optional-field tail.
6
+ */
7
+ import type { FurnaceConfig, OverrideComponentConfig } from '../types/furnace.js';
8
+ /**
9
+ * Validates an override component config object.
10
+ * @param data - Raw data to validate
11
+ * @param name - Component name for error messages
12
+ */
13
+ export declare function parseOverrideConfig(data: Record<string, unknown>, name: string): OverrideComponentConfig;
14
+ /**
15
+ * Parses and validates the `stock` component list: lowercase identifiers,
16
+ * no duplicates.
17
+ */
18
+ export declare function parseStockList(raw: unknown): string[];
19
+ /**
20
+ * Parses one of the named-component maps (`overrides` / `custom`): the
21
+ * map must be an object, every key a lowercase identifier, every value an
22
+ * object handed to the kind-specific parser.
23
+ */
24
+ export declare function parseNamedComponentMap<T>(raw: unknown, kind: 'override' | 'custom', key: 'overrides' | 'custom', parse: (value: Record<string, unknown>, name: string) => T): Record<string, T>;
25
+ /**
26
+ * Copies the validated optional fields (token settings, platform
27
+ * prefixes, ftl/jsconfig/scan paths) from the migrated raw config onto
28
+ * the typed config, re-validating the path-shaped ones against
29
+ * traversal.
30
+ */
31
+ export declare function applyOptionalFurnaceFields(migrated: Record<string, unknown>, config: FurnaceConfig): void;
@@ -0,0 +1,133 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Section validators for `validateFurnaceConfig`, split out of
4
+ * `furnace-config.ts` to keep that module inside the per-file line
5
+ * budget. Each helper owns one shape: the override entry, the stock
6
+ * list, the named-component maps, and the optional-field tail.
7
+ */
8
+ import { FurnaceError } from '../errors/furnace.js';
9
+ import { isObject, isString } from '../utils/validation.js';
10
+ import { parseStringArray } from './furnace-config-array-utils.js';
11
+ /**
12
+ * Validates an override component config object.
13
+ * @param data - Raw data to validate
14
+ * @param name - Component name for error messages
15
+ */
16
+ export function parseOverrideConfig(data, name) {
17
+ const validTypes = ['css-only', 'full'];
18
+ if (!isString(data['type']) || !validTypes.includes(data['type'])) {
19
+ throw new FurnaceError(`Furnace config: override "${name}.type" must be one of: ${validTypes.join(', ')}`);
20
+ }
21
+ if (!isString(data['description'])) {
22
+ throw new FurnaceError(`Furnace config: override "${name}.description" must be a string`);
23
+ }
24
+ if (!isString(data['basePath'])) {
25
+ throw new FurnaceError(`Furnace config: override "${name}.basePath" must be a string`);
26
+ }
27
+ if (data['basePath'].includes('..')) {
28
+ throw new FurnaceError(`Furnace config: override "${name}.basePath" must not contain ".." (path traversal)`);
29
+ }
30
+ if (!isString(data['baseVersion'])) {
31
+ throw new FurnaceError(`Furnace config: override "${name}.baseVersion" must be a string`);
32
+ }
33
+ return {
34
+ type: data['type'] === 'css-only' ? 'css-only' : 'full',
35
+ description: data['description'],
36
+ basePath: data['basePath'],
37
+ baseVersion: data['baseVersion'],
38
+ ...(isString(data['baseCommit']) ? { baseCommit: data['baseCommit'] } : {}),
39
+ };
40
+ }
41
+ /**
42
+ * Parses and validates the `stock` component list: lowercase identifiers,
43
+ * no duplicates.
44
+ */
45
+ export function parseStockList(raw) {
46
+ const stock = parseStringArray(raw, 'stock');
47
+ const stockSet = new Set();
48
+ for (const name of stock) {
49
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
50
+ throw new FurnaceError(`Furnace config: stock entry "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
51
+ }
52
+ if (stockSet.has(name)) {
53
+ throw new FurnaceError(`Furnace config: duplicate stock entry "${name}"`);
54
+ }
55
+ stockSet.add(name);
56
+ }
57
+ return stock;
58
+ }
59
+ /**
60
+ * Parses one of the named-component maps (`overrides` / `custom`): the
61
+ * map must be an object, every key a lowercase identifier, every value an
62
+ * object handed to the kind-specific parser.
63
+ */
64
+ export function parseNamedComponentMap(raw, kind, key, parse) {
65
+ if (!isObject(raw)) {
66
+ throw new FurnaceError(`Furnace config: "${key}" must be an object`);
67
+ }
68
+ const out = {};
69
+ for (const [name, value] of Object.entries(raw)) {
70
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
71
+ throw new FurnaceError(`Furnace config: ${kind} name "${name}" must match /^[a-z][a-z0-9-]*$/ (lowercase, no path separators)`);
72
+ }
73
+ if (!isObject(value)) {
74
+ throw new FurnaceError(`Furnace config: ${kind} "${name}" must be an object`);
75
+ }
76
+ out[name] = parse(value, name);
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * Copies the validated optional fields (token settings, platform
82
+ * prefixes, ftl/jsconfig/scan paths) from the migrated raw config onto
83
+ * the typed config, re-validating the path-shaped ones against
84
+ * traversal.
85
+ */
86
+ export function applyOptionalFurnaceFields(migrated, config) {
87
+ if (migrated['tokenPrefix'] !== undefined && isString(migrated['tokenPrefix'])) {
88
+ config.tokenPrefix = migrated['tokenPrefix'];
89
+ }
90
+ if (migrated['tokenAllowlist'] !== undefined) {
91
+ config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
92
+ }
93
+ if (migrated['platformPrefixes'] !== undefined) {
94
+ config.platformPrefixes = parseStringArray(migrated['platformPrefixes'], 'platformPrefixes');
95
+ }
96
+ if (migrated['runtimeVariables'] !== undefined) {
97
+ config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
98
+ }
99
+ if (migrated['tokenHostDocuments'] !== undefined) {
100
+ const docs = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
101
+ config.tokenHostDocuments = docs;
102
+ }
103
+ // Validate optional ftlBasePath
104
+ if (migrated['ftlBasePath'] !== undefined) {
105
+ if (!isString(migrated['ftlBasePath'])) {
106
+ throw new FurnaceError('Furnace config: "ftlBasePath" must be a string if provided');
107
+ }
108
+ if (migrated['ftlBasePath'].includes('..')) {
109
+ throw new FurnaceError('Furnace config: "ftlBasePath" must not contain ".." (path traversal)');
110
+ }
111
+ config.ftlBasePath = migrated['ftlBasePath'];
112
+ }
113
+ // Validate optional typecheckJsconfig — consumer-owned jsconfig whose
114
+ // chrome-elements `paths` entries Furnace maintains on deploy.
115
+ if (migrated['typecheckJsconfig'] !== undefined) {
116
+ const jsconfigPath = migrated['typecheckJsconfig'];
117
+ if (!isString(jsconfigPath) || jsconfigPath.includes('..')) {
118
+ throw new FurnaceError('Furnace config: "typecheckJsconfig" must be a string without ".." (path traversal)');
119
+ }
120
+ config.typecheckJsconfig = jsconfigPath;
121
+ }
122
+ // Validate optional scanPaths
123
+ if (migrated['scanPaths'] !== undefined) {
124
+ const paths = parseStringArray(migrated['scanPaths'], 'scanPaths');
125
+ for (const p of paths) {
126
+ if (p.includes('..')) {
127
+ throw new FurnaceError('Furnace config: "scanPaths" entries must not contain ".." (path traversal)');
128
+ }
129
+ }
130
+ config.scanPaths = paths;
131
+ }
132
+ }
133
+ //# sourceMappingURL=furnace-config-validate.js.map