@hominis/fireforge 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +4 -2
  3. package/dist/src/commands/config.js +16 -5
  4. package/dist/src/commands/download.js +22 -4
  5. package/dist/src/commands/export-all.js +50 -9
  6. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +11 -1
  7. package/dist/src/commands/furnace/chrome-doc-templates.js +12 -2
  8. package/dist/src/commands/furnace/create.js +21 -3
  9. package/dist/src/commands/furnace/index.js +1 -0
  10. package/dist/src/commands/furnace/init.js +76 -2
  11. package/dist/src/commands/furnace/preview.js +15 -2
  12. package/dist/src/commands/lint.js +16 -1
  13. package/dist/src/commands/rebase/patch-loop.js +19 -0
  14. package/dist/src/commands/status.js +17 -5
  15. package/dist/src/commands/wire.js +47 -8
  16. package/dist/src/core/build-baseline.d.ts +14 -0
  17. package/dist/src/core/build-baseline.js +61 -1
  18. package/dist/src/core/config-mutate.d.ts +1 -1
  19. package/dist/src/core/config-mutate.js +23 -1
  20. package/dist/src/core/config.d.ts +17 -0
  21. package/dist/src/core/config.js +35 -0
  22. package/dist/src/core/firefox.d.ts +16 -2
  23. package/dist/src/core/firefox.js +7 -2
  24. package/dist/src/core/furnace-config.d.ts +23 -0
  25. package/dist/src/core/furnace-config.js +38 -0
  26. package/dist/src/core/mach-error-hints.js +23 -0
  27. package/dist/src/core/patch-lint.js +43 -20
  28. package/dist/src/core/patch-parse.d.ts +18 -7
  29. package/dist/src/core/patch-parse.js +24 -2
  30. package/dist/src/core/patch-transform.js +4 -1
  31. package/dist/src/core/test-stale-check.js +46 -1
  32. package/dist/src/core/token-manager.js +57 -4
  33. package/dist/src/core/token-scaffold.d.ts +36 -0
  34. package/dist/src/core/token-scaffold.js +74 -0
  35. package/dist/src/types/commands/options.d.ts +10 -0
  36. package/package.json +1 -1
@@ -23,6 +23,9 @@
23
23
  * plugin, etc.) can legitimately have a fresh `dist/` with no
24
24
  * FireForge-recorded baseline update.
25
25
  */
26
+ import { createHash } from 'node:crypto';
27
+ import { readFile } from 'node:fs/promises';
28
+ import { join } from 'node:path';
26
29
  import { toError } from '../utils/errors.js';
27
30
  import { verbose } from '../utils/logger.js';
28
31
  import { isPackageablePath } from './build-audit.js';
@@ -90,7 +93,32 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
90
93
  return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
91
94
  }
92
95
  const changed = await collectChangedEnginePaths(engineDir, baseline);
93
- const packageable = changed.filter((path) => isPackageablePath(path)).sort();
96
+ let packageable = changed.filter((path) => isPackageablePath(path)).sort();
97
+ // Content-hash comparison: when the baseline carries a fingerprint set,
98
+ // fold each candidate path through a live re-hash and drop paths whose
99
+ // current content matches the baseline. Pre-0.16.0 baselines have no
100
+ // `packageableFingerprints` field; those fall through and the
101
+ // path-only comparison behaves as before (every workdir-dirty
102
+ // packageable path is reported as stale). The concrete motivating
103
+ // case: a project with imported patches + Furnace-applied components
104
+ // always has a persistent workdir diff against HEAD. Before the
105
+ // fingerprint layer, `git diff --name-only HEAD` returned that diff
106
+ // on every build, so the stale check fired immediately after a
107
+ // successful build even though nothing had actually changed. The
108
+ // fingerprints capture "these files had this content when the build
109
+ // ran"; a path stays stale only when its live hash diverges.
110
+ const fingerprints = baseline.packageableFingerprints;
111
+ if (fingerprints) {
112
+ const staleAfterHashCheck = [];
113
+ for (const path of packageable) {
114
+ const recorded = fingerprints[path];
115
+ const live = await hashEngineFile(engineDir, path);
116
+ if (recorded === undefined || live === undefined || recorded !== live) {
117
+ staleAfterHashCheck.push(path);
118
+ }
119
+ }
120
+ packageable = staleAfterHashCheck;
121
+ }
94
122
  if (packageable.length === 0) {
95
123
  return { stale: false, changedPaths: [], truncated: 0, baseline };
96
124
  }
@@ -98,6 +126,23 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
98
126
  const truncated = Math.max(0, packageable.length - head.length);
99
127
  return { stale: true, changedPaths: head, truncated, baseline };
100
128
  }
129
+ /**
130
+ * Reads a file under the engine directory and returns a hex-encoded
131
+ * SHA-256 of its contents, matching the hash the baseline writer
132
+ * produces. Returns `undefined` on any IO error (missing file,
133
+ * permission denied, etc.) so the caller can treat the path as still
134
+ * stale rather than crashing the preflight.
135
+ */
136
+ async function hashEngineFile(engineDir, relPath) {
137
+ try {
138
+ const buffer = await readFile(join(engineDir, relPath));
139
+ return createHash('sha256').update(buffer).digest('hex');
140
+ }
141
+ catch (error) {
142
+ verbose(`Stale-build preflight: could not hash ${relPath} for baseline comparison — ${toError(error).message}`);
143
+ return undefined;
144
+ }
145
+ }
101
146
  /**
102
147
  * Formats a human-readable warning body from a {@link StaleBuildResult}.
103
148
  * Kept separate from the probe so test code can assert on the structured
@@ -93,8 +93,56 @@ async function assertTokenCategoryExists(engineDir, tokensCssPath, category) {
93
93
  }
94
94
  }
95
95
  }
96
- throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}. ` +
97
- 'Available categories are defined by /* =... category ...= */ comment headers.');
96
+ const discoveredCategories = discoverCategoryHeaders(lines);
97
+ const available = discoveredCategories.length > 0
98
+ ? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
99
+ : 'The file currently has no category headers. Add one by hand near the top of the :root { … } block — the format is "/* = My Category = */" — or run "fireforge furnace init --force" to re-scaffold the default seed set.';
100
+ throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
101
+ `${available}\n\n` +
102
+ 'Categories are declared by comment headers. Single-line shape: /* = My Category = */. ' +
103
+ 'Multi-line shape: /* =============\\n * My Category\\n * ============= */.');
104
+ }
105
+ /**
106
+ * Scans a tokens CSS file for category header comments and returns the
107
+ * category names in document order. Used to enrich the "category not
108
+ * found" error body with concrete alternatives the operator can copy.
109
+ *
110
+ * Mirrors the shapes `findCategorySection` already recognises:
111
+ * - Single-line: `/* = Foo = *\/`
112
+ * - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
113
+ * closing `*\/`.
114
+ *
115
+ * This helper exists as a pure inspector; it never throws on malformed
116
+ * headers and silently skips shapes it cannot parse.
117
+ */
118
+ function discoverCategoryHeaders(lines) {
119
+ const categories = new Set();
120
+ const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const line = lines[i] ?? '';
123
+ const singleMatch = singleLinePattern.exec(line);
124
+ if (singleMatch?.[1]) {
125
+ const extracted = singleMatch[1].trim();
126
+ if (extracted.length > 0)
127
+ categories.add(extracted);
128
+ continue;
129
+ }
130
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
131
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
132
+ const blockLine = lines[j] ?? '';
133
+ if (/\*\//.test(blockLine))
134
+ break;
135
+ const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
136
+ if (trimmed.length === 0)
137
+ continue;
138
+ if (/^=+$/.test(trimmed))
139
+ continue;
140
+ categories.add(trimmed);
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ return [...categories];
98
146
  }
99
147
  /**
100
148
  * Validates token-add inputs without mutating files.
@@ -187,8 +235,13 @@ function findCategorySection(lines, category, tokensCssPath) {
187
235
  }
188
236
  }
189
237
  if (categoryLine === -1) {
190
- throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}. ` +
191
- 'Available categories are defined by /* =... category ...= */ comment headers.');
238
+ const discoveredCategories = discoverCategoryHeaders(lines);
239
+ const available = discoveredCategories.length > 0
240
+ ? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
241
+ : 'The file currently has no category headers.';
242
+ throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
243
+ `${available}\n\n` +
244
+ 'Add a header by hand inside the :root block (format: "/* = My Category = */") or re-run "fireforge furnace init --force" to re-seed the default categories.');
192
245
  }
193
246
  // Find the end of this category section (next section header or closing })
194
247
  // Handles both single-line (/* =...= */) and multi-line (/* ===...) section delimiters
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Scaffolds the default tokens CSS file consumed by `fireforge token add`.
3
+ *
4
+ * Before 0.16.0 `fireforge furnace init` wrote `furnace.json` but not the
5
+ * tokens CSS — every project's first `fireforge token add` hit
6
+ * `Token CSS file not found: browser/themes/shared/<binaryName>-tokens.css`.
7
+ * The 0.16.0 init now calls into this module to write a canonical
8
+ * `:root { … }` shell with a seed set of category headers that
9
+ * `assertTokenCategoryExists` recognises, and registers the tokens CSS
10
+ * path in `patchLint.rawColorAllowlist` so the first token that's an
11
+ * actual color value does not instantly fail `fireforge lint`.
12
+ */
13
+ import type { ProjectLicense } from '../types/config.js';
14
+ /**
15
+ * The set of categories seeded by the default scaffold. `token add
16
+ * --category <name>` accepts any of these without further setup; an
17
+ * operator who needs another category only has to add a matching
18
+ * `/* = My Category = *\/` header inside the `:root` block by hand.
19
+ *
20
+ * The names intentionally mirror the vocabulary used in Firefox's own
21
+ * token files (Colors — Canvas, Spacing, …) so operators coming from
22
+ * upstream don't have to relearn a fork-specific taxonomy.
23
+ */
24
+ export declare const DEFAULT_TOKEN_CATEGORIES: readonly string[];
25
+ /**
26
+ * Generates the default tokens CSS body. Extracted from the init
27
+ * command so tests can assert on the generated shape without running
28
+ * the interactive scaffold flow.
29
+ *
30
+ * @param binaryName - `fireforge.json` `binaryName` used in the
31
+ * rendered file banner so operators can identify the fork on-sight.
32
+ * @param license - Project license; piped through `getLicenseHeader`
33
+ * so the scaffold is SPDX-marked and survives `fireforge lint`'s
34
+ * license-header checks without operator intervention.
35
+ */
36
+ export declare function generateDefaultTokensCss(binaryName: string, license: ProjectLicense): string;
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Scaffolds the default tokens CSS file consumed by `fireforge token add`.
4
+ *
5
+ * Before 0.16.0 `fireforge furnace init` wrote `furnace.json` but not the
6
+ * tokens CSS — every project's first `fireforge token add` hit
7
+ * `Token CSS file not found: browser/themes/shared/<binaryName>-tokens.css`.
8
+ * The 0.16.0 init now calls into this module to write a canonical
9
+ * `:root { … }` shell with a seed set of category headers that
10
+ * `assertTokenCategoryExists` recognises, and registers the tokens CSS
11
+ * path in `patchLint.rawColorAllowlist` so the first token that's an
12
+ * actual color value does not instantly fail `fireforge lint`.
13
+ */
14
+ import { getLicenseHeader } from './license-headers.js';
15
+ /**
16
+ * The set of categories seeded by the default scaffold. `token add
17
+ * --category <name>` accepts any of these without further setup; an
18
+ * operator who needs another category only has to add a matching
19
+ * `/* = My Category = *\/` header inside the `:root` block by hand.
20
+ *
21
+ * The names intentionally mirror the vocabulary used in Firefox's own
22
+ * token files (Colors — Canvas, Spacing, …) so operators coming from
23
+ * upstream don't have to relearn a fork-specific taxonomy.
24
+ */
25
+ export const DEFAULT_TOKEN_CATEGORIES = [
26
+ 'Colors — General',
27
+ 'Colors — Canvas',
28
+ 'Colors — Experiment',
29
+ 'Spacing',
30
+ ];
31
+ /**
32
+ * Generates the default tokens CSS body. Extracted from the init
33
+ * command so tests can assert on the generated shape without running
34
+ * the interactive scaffold flow.
35
+ *
36
+ * @param binaryName - `fireforge.json` `binaryName` used in the
37
+ * rendered file banner so operators can identify the fork on-sight.
38
+ * @param license - Project license; piped through `getLicenseHeader`
39
+ * so the scaffold is SPDX-marked and survives `fireforge lint`'s
40
+ * license-header checks without operator intervention.
41
+ */
42
+ export function generateDefaultTokensCss(binaryName, license) {
43
+ const header = getLicenseHeader(license, 'css');
44
+ const categoryBlocks = DEFAULT_TOKEN_CATEGORIES.map((category) => ` /* = ${category} = */\n /* (add design tokens for "${category}" here; use \`fireforge token add --category "${category}" …\`) */`).join('\n\n');
45
+ return `${header}
46
+
47
+ /*
48
+ * Design tokens for ${binaryName}.
49
+ *
50
+ * Scaffolded by \`fireforge furnace init\`. Add new tokens with
51
+ * \`fireforge token add --category '<name>' -- <token-name> <value>\`
52
+ * — the command appends into the matching \`/* = <name> = *\\/\` block
53
+ * below and keeps the per-category ordering stable.
54
+ *
55
+ * Raw color literals inside this file are expected — the whole point
56
+ * of the tokens layer is that every other CSS file consumes \`var(…)\`
57
+ * instead of literal colors. \`fireforge furnace init\` adds this
58
+ * file's path to \`patchLint.rawColorAllowlist\` in fireforge.json so
59
+ * \`fireforge lint\` stops flagging the literals here.
60
+ */
61
+
62
+ :root {
63
+ ${categoryBlocks}
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ :root {
68
+ /* Dark-mode overrides land here. Use \`fireforge token add --mode override --dark-value <v>\`
69
+ to have a token's dark value placed inside this block. */
70
+ }
71
+ }
72
+ `;
73
+ }
74
+ //# sourceMappingURL=token-scaffold.js.map
@@ -372,6 +372,16 @@ export interface FurnaceCreateOptions {
372
372
  * dry-run faithfully previews the real command's outcome.
373
373
  */
374
374
  dryRun?: boolean;
375
+ /**
376
+ * Bypass the configured `componentPrefix` check for the supplied name.
377
+ * Without this flag, a name that does not start with the prefix is
378
+ * rejected before any files are written, so a prefix-mismatched
379
+ * component cannot leave behind a half-scaffolded state. Pass this
380
+ * flag only when you know the prefix mismatch is intentional — e.g.
381
+ * creating an experimental component whose name intentionally breaks
382
+ * the fork's convention.
383
+ */
384
+ allowPrefixMismatch?: boolean;
375
385
  }
376
386
  /**
377
387
  * Options for the wire command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",