@hominis/fireforge 0.15.2 → 0.15.4

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 (41) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +55 -1
  3. package/dist/src/commands/build.js +29 -2
  4. package/dist/src/commands/doctor-furnace.js +8 -1
  5. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
  6. package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
  7. package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
  8. package/dist/src/commands/furnace/chrome-doc.js +168 -0
  9. package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
  10. package/dist/src/commands/furnace/create-mochikit.js +70 -0
  11. package/dist/src/commands/furnace/create-templates.d.ts +32 -0
  12. package/dist/src/commands/furnace/create-templates.js +69 -0
  13. package/dist/src/commands/furnace/create.d.ts +17 -0
  14. package/dist/src/commands/furnace/create.js +54 -16
  15. package/dist/src/commands/furnace/index.d.ts +2 -1
  16. package/dist/src/commands/furnace/index.js +20 -3
  17. package/dist/src/commands/lint.d.ts +13 -1
  18. package/dist/src/commands/lint.js +33 -7
  19. package/dist/src/commands/wire.js +59 -6
  20. package/dist/src/core/browser-wire.d.ts +8 -0
  21. package/dist/src/core/browser-wire.js +2 -2
  22. package/dist/src/core/build-audit.d.ts +46 -0
  23. package/dist/src/core/build-audit.js +251 -0
  24. package/dist/src/core/build-baseline.d.ts +59 -0
  25. package/dist/src/core/build-baseline.js +83 -0
  26. package/dist/src/core/build-prepare.d.ts +20 -1
  27. package/dist/src/core/build-prepare.js +89 -4
  28. package/dist/src/core/furnace-operation.d.ts +2 -1
  29. package/dist/src/core/furnace-operation.js +13 -7
  30. package/dist/src/core/mach-error-hints.d.ts +29 -0
  31. package/dist/src/core/mach-error-hints.js +43 -0
  32. package/dist/src/core/mach.d.ts +5 -2
  33. package/dist/src/core/mach.js +31 -4
  34. package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
  35. package/dist/src/core/patch-lint-diff-tag.js +83 -0
  36. package/dist/src/core/wire-dom-fragment.d.ts +16 -4
  37. package/dist/src/core/wire-dom-fragment.js +32 -17
  38. package/dist/src/types/commands/options.d.ts +22 -0
  39. package/dist/src/types/commands/patches.d.ts +9 -0
  40. package/dist/src/types/furnace.d.ts +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MochiKit (chrome://mochikit) test-harness scaffolder for
3
+ * `fireforge furnace create --test-style=mochikit`.
4
+ *
5
+ * Motivation: browser-chrome mochitests require a `tabbrowser` to exist in
6
+ * the top-level chrome document. Forks with a bespoke chrome document
7
+ * (e.g. `mybrowser.xhtml`) that deliberately omits tabbrowser cannot run
8
+ * browser-chrome tests today. MochiKit tests load the component module
9
+ * directly via `chrome://global/` and assert against `customElements`, so
10
+ * they work against any fork that registers the upstream toolkit test
11
+ * manifest tree — including those without a tabbrowser.
12
+ */
13
+ import { type RollbackJournal } from '../../core/furnace-rollback.js';
14
+ import type { ProjectLicense } from '../../types/config.js';
15
+ /**
16
+ * Scaffolds a MochiKit test for a newly created custom component under
17
+ * `engine/toolkit/content/tests/widgets/`. Mirrors the layout stock
18
+ * Firefox widgets (moz-button, moz-toggle, etc.) use, so an operator who
19
+ * already added the `widgets/` tree to their test-manifest registration
20
+ * picks the new test up automatically.
21
+ *
22
+ * Appends a per-test entry to the existing `chrome.toml` when present,
23
+ * writes a fresh `[DEFAULT]`-headed one otherwise. The caller is still
24
+ * responsible for ensuring the `toolkit/content/tests/widgets/chrome.toml`
25
+ * path is registered somewhere in the moz.build tree; most forks inherit
26
+ * this from upstream via `TEST_HARNESS_FILES += [...]`.
27
+ */
28
+ export declare function scaffoldMochikitTestFiles(componentName: string, license: ProjectLicense, paths: {
29
+ engine: string;
30
+ }, journal?: RollbackJournal): Promise<string[]>;
@@ -0,0 +1,70 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * MochiKit (chrome://mochikit) test-harness scaffolder for
4
+ * `fireforge furnace create --test-style=mochikit`.
5
+ *
6
+ * Motivation: browser-chrome mochitests require a `tabbrowser` to exist in
7
+ * the top-level chrome document. Forks with a bespoke chrome document
8
+ * (e.g. `mybrowser.xhtml`) that deliberately omits tabbrowser cannot run
9
+ * browser-chrome tests today. MochiKit tests load the component module
10
+ * directly via `chrome://global/` and assert against `customElements`, so
11
+ * they work against any fork that registers the upstream toolkit test
12
+ * manifest tree — including those without a tabbrowser.
13
+ */
14
+ import { join } from 'node:path';
15
+ import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
16
+ import { getLicenseHeader } from '../../core/license-headers.js';
17
+ import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
18
+ import { warn } from '../../utils/logger.js';
19
+ import { generateMochikitChromeTomlEntry, generateMochikitChromeTomlSkeleton, generateMochikitTestContent, mochikitTestFileName, } from './create-templates.js';
20
+ /**
21
+ * Scaffolds a MochiKit test for a newly created custom component under
22
+ * `engine/toolkit/content/tests/widgets/`. Mirrors the layout stock
23
+ * Firefox widgets (moz-button, moz-toggle, etc.) use, so an operator who
24
+ * already added the `widgets/` tree to their test-manifest registration
25
+ * picks the new test up automatically.
26
+ *
27
+ * Appends a per-test entry to the existing `chrome.toml` when present,
28
+ * writes a fresh `[DEFAULT]`-headed one otherwise. The caller is still
29
+ * responsible for ensuring the `toolkit/content/tests/widgets/chrome.toml`
30
+ * path is registered somewhere in the moz.build tree; most forks inherit
31
+ * this from upstream via `TEST_HARNESS_FILES += [...]`.
32
+ */
33
+ export async function scaffoldMochikitTestFiles(componentName, license, paths, journal) {
34
+ const testDir = join(paths.engine, 'toolkit/content/tests/widgets');
35
+ if (journal && !(await pathExists(testDir))) {
36
+ recordCreatedDir(journal, testDir);
37
+ }
38
+ await ensureDir(testDir);
39
+ const hashHeader = getLicenseHeader(license, 'hash');
40
+ const writtenFiles = [];
41
+ const testFileName = mochikitTestFileName(componentName);
42
+ const testFilePath = join(testDir, testFileName);
43
+ if (journal)
44
+ await snapshotFile(journal, testFilePath);
45
+ await writeText(testFilePath, generateMochikitTestContent(componentName));
46
+ writtenFiles.push(testFileName);
47
+ // chrome.toml — append entry if the file already exists, otherwise write
48
+ // a fresh skeleton + entry. Idempotency: if the entry is already present
49
+ // the manifest is left untouched so re-runs don't double-register.
50
+ const manifestPath = join(testDir, 'chrome.toml');
51
+ const entry = generateMochikitChromeTomlEntry(componentName);
52
+ if (await pathExists(manifestPath)) {
53
+ const existing = await readText(manifestPath);
54
+ if (!existing.includes(`["${testFileName}"]`)) {
55
+ if (journal)
56
+ await snapshotFile(journal, manifestPath);
57
+ await writeText(manifestPath, existing.trimEnd() + '\n\n' + entry);
58
+ }
59
+ }
60
+ else {
61
+ if (journal)
62
+ await snapshotFile(journal, manifestPath);
63
+ await writeText(manifestPath, generateMochikitChromeTomlSkeleton(hashHeader) + entry);
64
+ writtenFiles.push('chrome.toml');
65
+ }
66
+ warn(`MochiKit scaffold written under toolkit/content/tests/widgets/. ` +
67
+ 'Ensure `toolkit/content/tests/widgets/chrome.toml` is reachable from an existing test-harness registration (upstream TEST_HARNESS_FILES entries handle this by default).');
68
+ return writtenFiles;
69
+ }
70
+ //# sourceMappingURL=create-mochikit.js.map
@@ -45,3 +45,35 @@ export declare function generateXpcshellTestContent(name: string, header: string
45
45
  * component actually touches (Services.storage, observer topics, etc.).
46
46
  */
47
47
  export declare function generateXpcshellManifestContent(name: string, header: string): string;
48
+ /** Returns the canonical mochikit test file basename for a component. */
49
+ export declare function mochikitTestFileName(name: string): string;
50
+ /**
51
+ * Generates a MochiKit (chrome://mochikit) test for a custom component.
52
+ *
53
+ * MochiKit tests load the component module directly via the global chrome
54
+ * URI and assert that `customElements.get(<tag>)` returns a constructor.
55
+ * They run on forks whose top-level chrome document lacks a `tabbrowser`
56
+ * (the class of bug that forces `--xpcshell` for storage code) because
57
+ * they do not traverse `URILoadingHelper.openLinkIn`.
58
+ *
59
+ * The scaffold here is a smoke test — the component is defined and the
60
+ * constructor is a function. Real UI assertions (render output, l10n
61
+ * wiring, keyboard interactions) are intentionally left out because they
62
+ * depend on the component's shape; operators can extend the test using
63
+ * the same SimpleTest APIs upstream toolkit widgets (moz-button, etc.)
64
+ * rely on.
65
+ */
66
+ export declare function generateMochikitTestContent(name: string): string;
67
+ /**
68
+ * Generates the `chrome.toml` entry block to append for a newly scaffolded
69
+ * mochikit test. When the manifest already exists the caller appends this
70
+ * snippet; when absent, the caller writes a file that starts with a
71
+ * `[DEFAULT]` stanza followed by this block.
72
+ */
73
+ export declare function generateMochikitChromeTomlEntry(name: string): string;
74
+ /**
75
+ * Generates the minimal `chrome.toml` used when the file does not yet
76
+ * exist in the tree. Keeps the `[DEFAULT]` stanza empty so each scaffold
77
+ * adds its own per-test entry, matching the stock Firefox convention.
78
+ */
79
+ export declare function generateMochikitChromeTomlSkeleton(header: string): string;
@@ -130,6 +130,75 @@ export function generateXpcshellManifestContent(name, header) {
130
130
  head = ""
131
131
 
132
132
  ["${xpcshellTestFileName(name)}"]
133
+ `;
134
+ }
135
+ /** Returns the canonical mochikit test file basename for a component. */
136
+ export function mochikitTestFileName(name) {
137
+ return `test_${name}.html`;
138
+ }
139
+ /**
140
+ * Generates a MochiKit (chrome://mochikit) test for a custom component.
141
+ *
142
+ * MochiKit tests load the component module directly via the global chrome
143
+ * URI and assert that `customElements.get(<tag>)` returns a constructor.
144
+ * They run on forks whose top-level chrome document lacks a `tabbrowser`
145
+ * (the class of bug that forces `--xpcshell` for storage code) because
146
+ * they do not traverse `URILoadingHelper.openLinkIn`.
147
+ *
148
+ * The scaffold here is a smoke test — the component is defined and the
149
+ * constructor is a function. Real UI assertions (render output, l10n
150
+ * wiring, keyboard interactions) are intentionally left out because they
151
+ * depend on the component's shape; operators can extend the test using
152
+ * the same SimpleTest APIs upstream toolkit widgets (moz-button, etc.)
153
+ * rely on.
154
+ */
155
+ export function generateMochikitTestContent(name) {
156
+ return `<!DOCTYPE html>
157
+ <html>
158
+ <head>
159
+ <meta charset="utf-8" />
160
+ <title>Test the ${name} custom element</title>
161
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
162
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
163
+ </head>
164
+ <body>
165
+ <p id="display"></p>
166
+ <div id="content" style="display: none"></div>
167
+ <pre id="test"></pre>
168
+ <script type="module">
169
+ import "chrome://global/content/elements/${name}.mjs";
170
+
171
+ SimpleTest.waitForExplicitFinish();
172
+
173
+ add_task(async function test_${name.replace(/-/g, '_')}_defined() {
174
+ const ctor = await customElements.whenDefined("${name}");
175
+ ok(ctor, "${name} custom element should be defined");
176
+ is(typeof ctor, "function", "Constructor should be a function");
177
+ });
178
+ </script>
179
+ </body>
180
+ </html>
181
+ `;
182
+ }
183
+ /**
184
+ * Generates the `chrome.toml` entry block to append for a newly scaffolded
185
+ * mochikit test. When the manifest already exists the caller appends this
186
+ * snippet; when absent, the caller writes a file that starts with a
187
+ * `[DEFAULT]` stanza followed by this block.
188
+ */
189
+ export function generateMochikitChromeTomlEntry(name) {
190
+ return `["${mochikitTestFileName(name)}"]\n`;
191
+ }
192
+ /**
193
+ * Generates the minimal `chrome.toml` used when the file does not yet
194
+ * exist in the tree. Keeps the `[DEFAULT]` stanza empty so each scaffold
195
+ * adds its own per-test entry, matching the stock Firefox convention.
196
+ */
197
+ export function generateMochikitChromeTomlSkeleton(header) {
198
+ return `${header}
199
+
200
+ [DEFAULT]
201
+
133
202
  `;
134
203
  }
135
204
  //# sourceMappingURL=create-templates.js.map
@@ -1,4 +1,21 @@
1
1
  import type { FurnaceCreateOptions } from '../../types/commands/index.js';
2
+ /** Resolved test-harness selection for a `furnace create` run. */
3
+ export type ResolvedTestStyle = 'mochikit' | 'browser-chrome' | 'xpcshell' | 'none';
4
+ /**
5
+ * Collapses `--with-tests`, `--xpcshell`, and `--test-style` into the single
6
+ * scaffold dispatch used inside the mutation phase.
7
+ *
8
+ * Backwards-compat invariants:
9
+ * - `--xpcshell` alone is equivalent to `--test-style=xpcshell`.
10
+ * - `--with-tests` alone (no `--test-style`) now defaults to `mochikit`
11
+ * (previously it defaulted to browser-chrome; the dogfooding pass
12
+ * flagged browser-chrome as unrunnable against non-tabbrowser chrome).
13
+ * Operators who need the old behavior can pass
14
+ * `--with-tests --test-style=browser-chrome`.
15
+ * - `--xpcshell --with-tests` is rejected as ambiguous.
16
+ * @throws InvalidArgumentError when flags conflict.
17
+ */
18
+ export declare function resolveTestStyle(options: FurnaceCreateOptions): ResolvedTestStyle;
2
19
  /**
3
20
  * Runs the furnace create command to scaffold a new custom component.
4
21
  * @param projectRoot - Root directory of the project
@@ -15,6 +15,7 @@ import { FurnaceError } from '../../errors/furnace.js';
15
15
  import { toError } from '../../utils/errors.js';
16
16
  import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
17
17
  import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
18
+ import { scaffoldMochikitTestFiles } from './create-mochikit.js';
18
19
  import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
19
20
  import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
20
21
  async function loadAuthoringFurnaceConfig(projectRoot) {
@@ -224,6 +225,35 @@ async function writeComponentFiles(componentDir, componentName, className, descr
224
225
  }
225
226
  return files;
226
227
  }
228
+ /**
229
+ * Collapses `--with-tests`, `--xpcshell`, and `--test-style` into the single
230
+ * scaffold dispatch used inside the mutation phase.
231
+ *
232
+ * Backwards-compat invariants:
233
+ * - `--xpcshell` alone is equivalent to `--test-style=xpcshell`.
234
+ * - `--with-tests` alone (no `--test-style`) now defaults to `mochikit`
235
+ * (previously it defaulted to browser-chrome; the dogfooding pass
236
+ * flagged browser-chrome as unrunnable against non-tabbrowser chrome).
237
+ * Operators who need the old behavior can pass
238
+ * `--with-tests --test-style=browser-chrome`.
239
+ * - `--xpcshell --with-tests` is rejected as ambiguous.
240
+ * @throws InvalidArgumentError when flags conflict.
241
+ */
242
+ export function resolveTestStyle(options) {
243
+ const xpcshellFlag = options.xpcshell ?? false;
244
+ const withTests = options.withTests ?? false;
245
+ const explicit = options.testStyle;
246
+ if (xpcshellFlag && explicit && explicit !== 'xpcshell') {
247
+ throw new InvalidArgumentError(`--xpcshell cannot be combined with --test-style=${explicit}; choose one.`, 'testStyle');
248
+ }
249
+ if (explicit)
250
+ return explicit;
251
+ if (xpcshellFlag)
252
+ return 'xpcshell';
253
+ if (withTests)
254
+ return 'mochikit';
255
+ return 'none';
256
+ }
227
257
  /**
228
258
  * Performs the transactional mutation phase of furnace create. All file
229
259
  * writes and the config update are recorded in a rollback journal so a
@@ -259,14 +289,18 @@ async function performCreateMutations(args) {
259
289
  args.config.custom[args.componentName] = customEntry;
260
290
  await snapshotFile(journal, args.furnacePaths.furnaceConfig);
261
291
  await writeFurnaceConfig(args.projectRoot, args.config);
262
- if (args.withTests) {
292
+ if (args.testStyle === 'browser-chrome') {
263
293
  const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
264
294
  testFiles.push(...scafFiles);
265
295
  }
266
- if (args.xpcshellTests) {
296
+ else if (args.testStyle === 'xpcshell') {
267
297
  const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
268
298
  testFiles.push(...xpcshellFiles);
269
299
  }
300
+ else if (args.testStyle === 'mochikit') {
301
+ const mochikitFiles = await scaffoldMochikitTestFiles(args.componentName, args.license, args.paths, journal);
302
+ testFiles.push(...mochikitFiles);
303
+ }
270
304
  }
271
305
  catch (error) {
272
306
  try {
@@ -400,15 +434,13 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
400
434
  return;
401
435
  }
402
436
  const { localized, register } = featureSelection;
403
- // --with-tests writes files under engine/browser/base/content/test/ and
404
- // registers them in moz.build. --xpcshell is the equivalent for forks
405
- // without a tabbrowser (storage-layer code). Guard against a missing
406
- // engine now rather than letting the scaffolders fabricate a partial
407
- // engine tree with ensureDir.
408
- const withTests = options.withTests ?? false;
409
- const xpcshellTests = options.xpcshell ?? false;
410
- if ((withTests || xpcshellTests) && !(await pathExists(paths.engine))) {
411
- throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests or --xpcshell.', componentName);
437
+ // Collapse --with-tests / --xpcshell / --test-style into the single
438
+ // scaffold selection used by the mutation phase. The resolver validates
439
+ // incompatible combinations up-front so a bad flag set never strands a
440
+ // partial mutation behind.
441
+ const testStyle = resolveTestStyle(options);
442
+ if (testStyle !== 'none' && !(await pathExists(paths.engine))) {
443
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests, --xpcshell, or --test-style.', componentName);
412
444
  }
413
445
  // --- Generate component files ---
414
446
  const className = tagNameToClassName(componentName);
@@ -444,8 +476,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
444
476
  forgeConfig,
445
477
  paths,
446
478
  license,
447
- withTests,
448
- xpcshellTests,
479
+ testStyle,
449
480
  ftlChromeSubPath,
450
481
  operationContext: ctx,
451
482
  }));
@@ -453,9 +484,16 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
453
484
  let noteParts = `Files created in components/custom/${componentName}/:\n` +
454
485
  files.map((f) => ` ${f}`).join('\n');
455
486
  if (testFiles.length > 0) {
456
- const testRoot = xpcshellTests
457
- ? `engine/browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${componentName}/`
458
- : `engine/browser/base/content/test/${forgeConfig.binaryName}/`;
487
+ let testRoot;
488
+ if (testStyle === 'xpcshell') {
489
+ testRoot = `engine/browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${componentName}/`;
490
+ }
491
+ else if (testStyle === 'mochikit') {
492
+ testRoot = 'engine/toolkit/content/tests/widgets/';
493
+ }
494
+ else {
495
+ testRoot = `engine/browser/base/content/test/${forgeConfig.binaryName}/`;
496
+ }
459
497
  noteParts += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
460
498
  }
461
499
  noteParts +=
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandContext } from '../../types/cli.js';
3
3
  import { furnaceApplyCommand } from './apply.js';
4
+ import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
4
5
  import { furnaceCreateCommand } from './create.js';
5
6
  import { furnaceDeployCommand } from './deploy.js';
6
7
  import { furnaceDiffCommand } from './diff.js';
@@ -15,6 +16,6 @@ import { furnaceScanCommand } from './scan.js';
15
16
  import { furnaceStatusCommand } from './status.js';
16
17
  import { furnaceSyncCommand } from './sync.js';
17
18
  import { furnaceValidateCommand } from './validate.js';
18
- export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
19
+ export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceChromeDocCreateCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
19
20
  /** Registers the furnace command on the CLI program. */
20
21
  export declare function registerFurnace(program: Command, context: CommandContext): void;
@@ -2,6 +2,7 @@
2
2
  import { Option } from 'commander';
3
3
  import { pickDefined } from '../../utils/options.js';
4
4
  import { furnaceApplyCommand } from './apply.js';
5
+ import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
5
6
  import { furnaceCreateCommand } from './create.js';
6
7
  import { furnaceDeployCommand } from './deploy.js';
7
8
  import { furnaceDiffCommand } from './diff.js';
@@ -16,7 +17,7 @@ import { furnaceScanCommand } from './scan.js';
16
17
  import { furnaceStatusCommand } from './status.js';
17
18
  import { furnaceSyncCommand } from './sync.js';
18
19
  import { furnaceValidateCommand } from './validate.js';
19
- export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
20
+ export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceChromeDocCreateCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
20
21
  /**
21
22
  * Registers Furnace commands for querying component state: status, scan,
22
23
  * and action commands like apply, deploy, and create.
@@ -71,12 +72,28 @@ function registerFurnaceInfoCommands(furnace, context) {
71
72
  .option('-d, --description <desc>', 'Component description')
72
73
  .option('--localized', 'Include Fluent l10n support')
73
74
  .option('--no-register', 'Skip customElements.js registration')
74
- .option('--with-tests', 'Scaffold Mochitest directory and register in moz.build')
75
- .option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser)')
75
+ .option('--with-tests', 'Scaffold a test harness (defaults to MochiKit; see --test-style)')
76
+ .option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser); equivalent to --test-style=xpcshell')
77
+ .option('--test-style <style>', "Override the harness written by --with-tests: mochikit (default, runs against non-tabbrowser chrome), browser-chrome (today's scaffold, needs tabbrowser), or xpcshell (headless)", (value) => {
78
+ if (value !== 'mochikit' && value !== 'browser-chrome' && value !== 'xpcshell') {
79
+ throw new Error(`--test-style must be one of: mochikit, browser-chrome, xpcshell. Got: "${value}".`);
80
+ }
81
+ return value;
82
+ })
76
83
  .option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
77
84
  .action(withErrorHandling(async (name, options) => {
78
85
  await furnaceCreateCommand(getProjectRoot(), name, options);
79
86
  }));
87
+ const chromeDoc = furnace
88
+ .command('chrome-doc')
89
+ .description('Scaffold top-level chrome documents (xhtml + js + css + ftl + jar.mn)');
90
+ chromeDoc
91
+ .command('create <name>')
92
+ .description('Scaffold a new top-level chrome document')
93
+ .option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
94
+ .action(withErrorHandling(async (name, options) => {
95
+ await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
96
+ }));
80
97
  }
81
98
  /**
82
99
  * Registers Furnace commands for authoring, inspection, and maintenance:
@@ -1,10 +1,22 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandContext } from '../types/cli.js';
3
+ /** Options controlling how the lint command filters and tags its output. */
4
+ export interface LintCommandOptions {
5
+ /**
6
+ * When set, tag each issue as `introduced` or `cumulative` based on
7
+ * whether its file changed since this git revision (e.g. `HEAD`, a
8
+ * branch name, or a SHA). Issues are not filtered — the full set still
9
+ * prints and the exit code is unchanged — but a diff-scoped summary
10
+ * makes it trivial to see which errors the current task introduced.
11
+ */
12
+ since?: string;
13
+ }
3
14
  /**
4
15
  * Runs the lint command to check engine changes against patch quality rules.
5
16
  * @param projectRoot - Root directory of the project
6
17
  * @param files - Optional file/directory paths to lint (relative to engine/)
18
+ * @param options - Additional lint options such as `--since` diff-scoping
7
19
  */
8
- export declare function lintCommand(projectRoot: string, files: string[]): Promise<void>;
20
+ export declare function lintCommand(projectRoot: string, files: string[], options?: LintCommandOptions): Promise<void>;
9
21
  /** Registers the lint command on the CLI program. */
10
22
  export declare function registerLint(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -7,6 +7,7 @@ import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
7
7
  import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } from '../core/git-status.js';
8
8
  import { extractAffectedFiles } from '../core/patch-apply.js';
9
9
  import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
10
+ import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
10
11
  import { GeneralError } from '../errors/base.js';
11
12
  import { pathExists } from '../utils/fs.js';
12
13
  import { info, intro, outro, success, warn } from '../utils/logger.js';
@@ -14,8 +15,9 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
14
15
  * Runs the lint command to check engine changes against patch quality rules.
15
16
  * @param projectRoot - Root directory of the project
16
17
  * @param files - Optional file/directory paths to lint (relative to engine/)
18
+ * @param options - Additional lint options such as `--since` diff-scoping
17
19
  */
18
- export async function lintCommand(projectRoot, files) {
20
+ export async function lintCommand(projectRoot, files, options = {}) {
19
21
  intro('FireForge Lint');
20
22
  const paths = getProjectPaths(projectRoot);
21
23
  if (!(await pathExists(paths.engine))) {
@@ -105,19 +107,38 @@ export async function lintCommand(projectRoot, files) {
105
107
  outro('Lint passed');
106
108
  return;
107
109
  }
110
+ // Diff-scoping: tag each issue as introduced-in-current-task vs
111
+ // cumulative-pre-existing-drift. Never filters — full set still prints
112
+ // and exit code semantics are unchanged — but the per-line prefix and
113
+ // summary make triage trivial on a large patch series.
114
+ const sinceActive = Boolean(options.since);
115
+ if (options.since) {
116
+ const diffFiles = await collectDiffFilePaths(paths.engine, options.since);
117
+ tagLintIssues(issues, diffFiles);
118
+ }
108
119
  const errors = issues.filter((i) => i.severity === 'error');
109
120
  const warnings = issues.filter((i) => i.severity === 'warning');
110
121
  const notices = issues.filter((i) => i.severity === 'notice');
122
+ const tagPrefix = (issue) => sinceActive && issue.tag ? `[${issue.tag}] ` : '';
111
123
  for (const issue of notices) {
112
- info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
124
+ info(`${tagPrefix(issue)}NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
113
125
  }
114
126
  for (const issue of warnings) {
115
- warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
127
+ warn(`${tagPrefix(issue)}[${issue.check}] ${issue.file}: ${issue.message}`);
116
128
  }
117
129
  for (const issue of errors) {
118
- warn(`ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
130
+ warn(`${tagPrefix(issue)}ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
131
+ }
132
+ if (sinceActive) {
133
+ const introducedErrors = errors.filter((i) => i.tag === 'introduced').length;
134
+ const introducedWarnings = warnings.filter((i) => i.tag === 'introduced').length;
135
+ const cumulativeErrors = errors.length - introducedErrors;
136
+ const cumulativeWarnings = warnings.length - introducedWarnings;
137
+ info(`\nLint: ${introducedErrors} introduced error(s), ${introducedWarnings} introduced warning(s); ${cumulativeErrors} cumulative error(s), ${cumulativeWarnings} cumulative warning(s)`);
138
+ }
139
+ else {
140
+ info(`\nLint: ${errors.length} error(s), ${warnings.length} warning(s)`);
119
141
  }
120
- info(`\nLint: ${errors.length} error(s), ${warnings.length} warning(s)`);
121
142
  if (errors.length > 0) {
122
143
  outro('Lint failed');
123
144
  throw new GeneralError(`Patch lint found ${errors.length} error(s). Fix these before exporting.`);
@@ -129,8 +150,13 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
129
150
  program
130
151
  .command('lint [paths...]')
131
152
  .description('Lint engine changes against patch quality rules')
132
- .action(withErrorHandling(async (paths) => {
133
- await lintCommand(getProjectRoot(), paths);
153
+ .option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
154
+ .action(withErrorHandling(async (paths, options) => {
155
+ const lintOptions = {};
156
+ if (options.since !== undefined) {
157
+ lintOptions.since = options.since;
158
+ }
159
+ await lintCommand(getProjectRoot(), paths, lintOptions);
134
160
  }));
135
161
  }
136
162
  //# sourceMappingURL=lint.js.map