@hominis/fireforge 0.30.1 → 0.31.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 (141) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +5 -15
  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 +36 -0
  10. package/dist/src/commands/export.js +47 -112
  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 +1 -1
  20. package/dist/src/commands/lint-per-patch.js +110 -81
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +96 -84
  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-scan.js +8 -1
  42. package/dist/src/commands/rebase/summary.d.ts +1 -5
  43. package/dist/src/commands/rebase/summary.js +1 -1
  44. package/dist/src/commands/status-output.js +77 -68
  45. package/dist/src/commands/test-diagnose.d.ts +23 -0
  46. package/dist/src/commands/test-diagnose.js +210 -0
  47. package/dist/src/commands/test-run.d.ts +58 -0
  48. package/dist/src/commands/test-run.js +88 -0
  49. package/dist/src/commands/test.js +169 -257
  50. package/dist/src/commands/token.js +15 -1
  51. package/dist/src/commands/wire.js +109 -78
  52. package/dist/src/core/build-audit.d.ts +1 -1
  53. package/dist/src/core/build-audit.js +2 -46
  54. package/dist/src/core/build-baseline-types.d.ts +38 -0
  55. package/dist/src/core/build-baseline-types.js +10 -0
  56. package/dist/src/core/build-baseline.d.ts +1 -31
  57. package/dist/src/core/build-prepare.d.ts +1 -1
  58. package/dist/src/core/build-prepare.js +2 -45
  59. package/dist/src/core/config-paths.d.ts +0 -8
  60. package/dist/src/core/config-paths.js +4 -4
  61. package/dist/src/core/config-state.d.ts +0 -6
  62. package/dist/src/core/config-state.js +1 -1
  63. package/dist/src/core/config-validate-patch-policy.js +12 -13
  64. package/dist/src/core/config-validate.js +48 -28
  65. package/dist/src/core/engine-changes.d.ts +24 -0
  66. package/dist/src/core/engine-changes.js +64 -0
  67. package/dist/src/core/firefox-cache.d.ts +0 -5
  68. package/dist/src/core/firefox-cache.js +1 -1
  69. package/dist/src/core/firefox-download.d.ts +0 -6
  70. package/dist/src/core/firefox-download.js +1 -1
  71. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  72. package/dist/src/core/furnace-apply-helpers.js +11 -20
  73. package/dist/src/core/furnace-apply.d.ts +1 -1
  74. package/dist/src/core/furnace-apply.js +1 -1
  75. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  76. package/dist/src/core/furnace-checksum-utils.js +15 -0
  77. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  78. package/dist/src/core/furnace-config-validate.js +133 -0
  79. package/dist/src/core/furnace-config.d.ts +4 -32
  80. package/dist/src/core/furnace-config.js +15 -111
  81. package/dist/src/core/furnace-constants.d.ts +0 -10
  82. package/dist/src/core/furnace-constants.js +2 -2
  83. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  84. package/dist/src/core/furnace-css-fragments.js +243 -0
  85. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  86. package/dist/src/core/furnace-jsconfig.js +171 -0
  87. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  88. package/dist/src/core/furnace-validate-helpers.js +40 -1
  89. package/dist/src/core/furnace-validate-registration.js +16 -1
  90. package/dist/src/core/furnace-validate.js +54 -2
  91. package/dist/src/core/git-file-ops.d.ts +0 -12
  92. package/dist/src/core/git-file-ops.js +2 -2
  93. package/dist/src/core/lint-cache.d.ts +0 -13
  94. package/dist/src/core/lint-cache.js +5 -5
  95. package/dist/src/core/mach.d.ts +5 -1
  96. package/dist/src/core/mach.js +6 -2
  97. package/dist/src/core/manifest-register.d.ts +5 -16
  98. package/dist/src/core/manifest-register.js +3 -1
  99. package/dist/src/core/patch-lint-checkjs.js +53 -7
  100. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  101. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  102. package/dist/src/core/patch-lint-observer.js +168 -0
  103. package/dist/src/core/patch-lint.js +132 -125
  104. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  105. package/dist/src/core/patch-manifest-io.js +44 -2
  106. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  107. package/dist/src/core/patch-manifest-validate.js +1 -1
  108. package/dist/src/core/patch-manifest.d.ts +1 -1
  109. package/dist/src/core/patch-manifest.js +1 -1
  110. package/dist/src/core/patch-policy.d.ts +0 -4
  111. package/dist/src/core/patch-policy.js +10 -4
  112. package/dist/src/core/register-browser-content.d.ts +1 -1
  113. package/dist/src/core/register-module.d.ts +1 -1
  114. package/dist/src/core/register-result.d.ts +21 -0
  115. package/dist/src/core/register-result.js +9 -0
  116. package/dist/src/core/register-shared-css.d.ts +1 -1
  117. package/dist/src/core/register-test-manifest.d.ts +1 -1
  118. package/dist/src/core/test-harness-crash.d.ts +61 -0
  119. package/dist/src/core/test-harness-crash.js +140 -0
  120. package/dist/src/core/test-stale-check.d.ts +1 -1
  121. package/dist/src/core/test-stale-check.js +2 -46
  122. package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
  123. package/dist/src/core/test-xpcshell-retry.js +4 -2
  124. package/dist/src/core/token-dark-mode.js +14 -26
  125. package/dist/src/core/token-manager.d.ts +4 -0
  126. package/dist/src/core/token-manager.js +70 -16
  127. package/dist/src/core/typecheck-shim.d.ts +0 -21
  128. package/dist/src/core/typecheck-shim.js +26 -4
  129. package/dist/src/core/wire-utils.js +37 -44
  130. package/dist/src/types/commands/index.d.ts +1 -1
  131. package/dist/src/types/commands/options.d.ts +105 -0
  132. package/dist/src/types/furnace.d.ts +12 -1
  133. package/dist/src/utils/elapsed.d.ts +0 -2
  134. package/dist/src/utils/elapsed.js +1 -1
  135. package/dist/src/utils/fs.d.ts +0 -5
  136. package/dist/src/utils/fs.js +1 -1
  137. package/dist/src/utils/regex.d.ts +0 -6
  138. package/dist/src/utils/regex.js +3 -3
  139. package/dist/src/utils/validation.d.ts +0 -8
  140. package/dist/src/utils/validation.js +2 -2
  141. 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) {
@@ -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
@@ -1,16 +1,8 @@
1
1
  import type { FurnaceConfig, FurnaceState } from '../types/furnace.js';
2
2
  import { detectComposesCycles } from './furnace-graph-utils.js';
3
3
  export { detectComposesCycles };
4
- /** Name of the furnace configuration file */
5
- export declare const FURNACE_CONFIG_FILENAME = "furnace.json";
6
- /** Name of the furnace state file */
7
- export declare const FURNACE_STATE_FILENAME = "furnace-state.json";
8
- /** Name of the components directory */
9
- export declare const COMPONENTS_DIR = "components";
10
- /** Name of the overrides subdirectory */
11
- export declare const OVERRIDES_DIR = "overrides";
12
- /** Name of the custom subdirectory */
13
- export declare const CUSTOM_DIR = "custom";
4
+ /** Directory name for shared CSS fragments within components/ */
5
+ export declare const SHARED_FRAGMENTS_DIR = "shared";
14
6
  /**
15
7
  * Paths for furnace-related files and directories.
16
8
  */
@@ -23,6 +15,8 @@ interface FurnacePaths {
23
15
  overridesDir: string;
24
16
  /** Path to components/custom directory */
25
17
  customDir: string;
18
+ /** Path to components/shared directory (CSS fragments) */
19
+ sharedDir: string;
26
20
  /** Path to .fireforge/furnace-state.json */
27
21
  furnaceState: string;
28
22
  }
@@ -38,22 +32,6 @@ export declare function getFurnacePaths(root: string): FurnacePaths;
38
32
  * @returns True if furnace.json exists
39
33
  */
40
34
  export declare function furnaceConfigExists(root: string): Promise<boolean>;
41
- /**
42
- * Migrates a furnace config from an older schema version to the current one.
43
- * Returns the data unchanged if it is already at the current version.
44
- *
45
- * When a future version 2 is introduced, add a `case 1:` that transforms
46
- * v1 data into v2 shape and falls through to validation. The pattern is:
47
- *
48
- * ```
49
- * case 1:
50
- * data = migrateV1ToV2(data);
51
- * // fallthrough
52
- * case 2:
53
- * break;
54
- * ```
55
- */
56
- export declare function migrateFurnaceConfig(data: Record<string, unknown>): Record<string, unknown>;
57
35
  /**
58
36
  * Validates a raw config object and returns a typed FurnaceConfig.
59
37
  * @param data - Raw data to validate
@@ -61,12 +39,6 @@ export declare function migrateFurnaceConfig(data: Record<string, unknown>): Rec
61
39
  * @throws Error if validation fails
62
40
  */
63
41
  export declare function validateFurnaceConfig(data: unknown): FurnaceConfig;
64
- /**
65
- * Validates a parsed furnace state object and returns a typed FurnaceState.
66
- * @param data - Parsed JSON state data
67
- * @returns Validated FurnaceState
68
- */
69
- export declare function validateFurnaceState(data: unknown): FurnaceState;
70
42
  /**
71
43
  * Loads and validates the furnace.json configuration.
72
44
  * @param root - Root directory of the project