@hominis/fireforge 0.15.1 → 0.15.3

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 (56) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +76 -3
  3. package/dist/src/commands/build.js +41 -3
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
  6. package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
  7. package/dist/src/commands/furnace/chrome-doc.js +168 -0
  8. package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
  9. package/dist/src/commands/furnace/create-mochikit.js +70 -0
  10. package/dist/src/commands/furnace/create-templates.d.ts +53 -0
  11. package/dist/src/commands/furnace/create-templates.js +118 -0
  12. package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
  13. package/dist/src/commands/furnace/create-xpcshell.js +53 -0
  14. package/dist/src/commands/furnace/create.d.ts +17 -0
  15. package/dist/src/commands/furnace/create.js +59 -12
  16. package/dist/src/commands/furnace/index.d.ts +2 -1
  17. package/dist/src/commands/furnace/index.js +20 -2
  18. package/dist/src/commands/lint.d.ts +13 -1
  19. package/dist/src/commands/lint.js +33 -7
  20. package/dist/src/commands/setup.d.ts +1 -1
  21. package/dist/src/commands/setup.js +3 -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 +94 -4
  28. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  29. package/dist/src/core/furnace-config-tokens.d.ts +6 -0
  30. package/dist/src/core/furnace-config-tokens.js +15 -0
  31. package/dist/src/core/furnace-config.js +10 -4
  32. package/dist/src/core/furnace-operation.d.ts +2 -1
  33. package/dist/src/core/furnace-operation.js +13 -7
  34. package/dist/src/core/furnace-registration-ast.d.ts +2 -2
  35. package/dist/src/core/furnace-registration-ast.js +1 -1
  36. package/dist/src/core/furnace-validate-compatibility.js +18 -7
  37. package/dist/src/core/furnace-validate-helpers.d.ts +31 -1
  38. package/dist/src/core/furnace-validate-helpers.js +101 -18
  39. package/dist/src/core/furnace-validate-registration.d.ts +1 -1
  40. package/dist/src/core/furnace-validate-registration.js +1 -1
  41. package/dist/src/core/mach-error-hints.d.ts +29 -0
  42. package/dist/src/core/mach-error-hints.js +43 -0
  43. package/dist/src/core/mach.d.ts +5 -2
  44. package/dist/src/core/mach.js +31 -4
  45. package/dist/src/core/marionette-preflight.d.ts +14 -7
  46. package/dist/src/core/marionette-preflight.js +94 -44
  47. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  48. package/dist/src/core/patch-lint-cross.js +1 -1
  49. package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
  50. package/dist/src/core/patch-lint-diff-tag.js +83 -0
  51. package/dist/src/core/patch-lint.js +29 -9
  52. package/dist/src/types/commands/options.d.ts +25 -0
  53. package/dist/src/types/commands/patches.d.ts +9 -0
  54. package/dist/src/types/config.d.ts +1 -1
  55. package/dist/src/types/furnace.d.ts +13 -2
  56. package/package.json +1 -1
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge furnace chrome-doc create <name>` — scaffolds a top-level
4
+ * chrome document (xhtml + js + css + ftl + jar.mn registrations).
5
+ *
6
+ * Motivation: `furnace create` covers custom elements under
7
+ * `toolkit/content/widgets/`, but top-level chrome documents (the
8
+ * `mybrowser.xhtml`-class entry points a fork adds alongside or instead
9
+ * of `browser.xhtml`) are today hand-authored with error-prone jar.mn +
10
+ * jar.inc.mn + locales/jar.mn glue. The `*` preprocessor flag, the
11
+ * macOS titlebar-button carve-out, the startup-topic observer, and the
12
+ * Fluent linkage each have silent-break failure modes.
13
+ *
14
+ * This command writes the four source files and appends three jar.mn
15
+ * entries under a rollback journal identical in shape to `furnace create`.
16
+ * A SIGINT mid-scaffold restores every touched file; a successful run
17
+ * leaves the tree ready for `fireforge build`.
18
+ */
19
+ import { join } from 'node:path';
20
+ import { loadConfig } from '../../core/config.js';
21
+ import { runFurnaceMutation } from '../../core/furnace-operation.js';
22
+ import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
23
+ import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
24
+ import { InvalidArgumentError } from '../../errors/base.js';
25
+ import { FurnaceError } from '../../errors/furnace.js';
26
+ import { pathExists, readText, writeText } from '../../utils/fs.js';
27
+ import { intro, note, outro } from '../../utils/logger.js';
28
+ import { generateChromeDocCss, generateChromeDocFtl, generateChromeDocJs, generateChromeDocXhtml, jarIncMnEntryForChromeDoc, jarMnEntriesForChromeDoc, localeJarMnEntryForChromeDoc, } from './chrome-doc-templates.js';
29
+ /** Chrome-doc name shape: lowercase ASCII, optional hyphens, no leading digit. */
30
+ const CHROME_DOC_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
31
+ /**
32
+ * Validates a chrome-doc name. Lowercase ASCII, optional hyphens, no
33
+ * leading digit — the name is used verbatim in CSS selectors, jar.mn
34
+ * entries, FTL keys, and file basenames, so anything outside that
35
+ * character set would break at least one downstream consumer.
36
+ * @param name Chrome-doc name (file basename without extension).
37
+ * @throws InvalidArgumentError when the name is unusable.
38
+ */
39
+ function validateChromeDocName(name) {
40
+ if (!name.trim()) {
41
+ throw new InvalidArgumentError('Chrome-doc name is required', 'name');
42
+ }
43
+ if (!CHROME_DOC_NAME_PATTERN.test(name)) {
44
+ throw new InvalidArgumentError('Chrome-doc name must be lowercase ASCII, may contain hyphens, and must not start with a digit (e.g. mybrowser, about-onboarding).', 'name');
45
+ }
46
+ }
47
+ /**
48
+ * Appends a line to a jar.mn-style file when that exact line is not
49
+ * already present. Captures the pre-write contents in the journal so a
50
+ * mid-run interruption restores the file to its original state.
51
+ */
52
+ async function appendJarEntryIfAbsent(filePath, entry, journal) {
53
+ if (!(await pathExists(filePath))) {
54
+ // Target jar.mn doesn't exist in this tree layout. We do NOT create it
55
+ // — a fork that moved the jar file needs the operator to choose a
56
+ // placement. The command surfaces this as a FurnaceError so the user
57
+ // can investigate rather than silently writing to a non-canonical path.
58
+ throw new FurnaceError(`Required jar file ${filePath} does not exist; cannot register chrome-doc entry. Check that the fork's engine layout matches the expected browser/ and locales/ tree.`);
59
+ }
60
+ const existing = await readText(filePath);
61
+ if (existing.includes(entry)) {
62
+ return;
63
+ }
64
+ await snapshotFile(journal, filePath);
65
+ const withEntry = existing.trimEnd() + '\n' + entry + '\n';
66
+ await writeText(filePath, withEntry);
67
+ }
68
+ /**
69
+ * Writes the xhtml/js/css/ftl source files plus the three jar.mn
70
+ * registrations under a rollback journal. Any interruption leaves the
71
+ * tree in its pre-command state.
72
+ */
73
+ async function performChromeDocMutations(args) {
74
+ const journal = createRollbackJournal();
75
+ args.operationContext.registerJournal(journal);
76
+ // XHTML uses an inline XML comment since getLicenseHeader has no XML
77
+ // style — the SPDX convention is a single-line comment at the top.
78
+ const jsHeader = getLicenseHeader(args.license, 'js');
79
+ const cssHeader = getLicenseHeader(args.license, 'css');
80
+ const ftlHeader = getLicenseHeader(args.license, 'hash');
81
+ const written = [];
82
+ try {
83
+ const contentDir = join(args.engineDir, 'browser/base/content');
84
+ const sharedThemeDir = join(args.engineDir, 'browser/themes/shared');
85
+ const localeDir = join(args.engineDir, 'browser/locales/en-US/browser');
86
+ for (const dir of [contentDir, sharedThemeDir, localeDir]) {
87
+ if (!(await pathExists(dir))) {
88
+ recordCreatedDir(journal, dir);
89
+ const { ensureDir } = await import('../../utils/fs.js');
90
+ await ensureDir(dir);
91
+ }
92
+ }
93
+ const xhtmlPath = join(contentDir, `${args.name}.xhtml`);
94
+ if (await pathExists(xhtmlPath)) {
95
+ throw new FurnaceError(`${args.name}.xhtml already exists at ${xhtmlPath}. Remove it or choose a different name.`);
96
+ }
97
+ await snapshotFile(journal, xhtmlPath);
98
+ await writeText(xhtmlPath, generateChromeDocXhtml(args.name, args.withTitlebar, args.license));
99
+ written.push(`browser/base/content/${args.name}.xhtml`);
100
+ const jsPath = join(contentDir, `${args.name}.js`);
101
+ await snapshotFile(journal, jsPath);
102
+ await writeText(jsPath, generateChromeDocJs(args.name, jsHeader));
103
+ written.push(`browser/base/content/${args.name}.js`);
104
+ const cssPath = join(sharedThemeDir, `${args.name}-chrome.css`);
105
+ await snapshotFile(journal, cssPath);
106
+ await writeText(cssPath, generateChromeDocCss(args.name, args.withTitlebar, cssHeader));
107
+ written.push(`browser/themes/shared/${args.name}-chrome.css`);
108
+ const ftlPath = join(localeDir, `${args.name}.ftl`);
109
+ await snapshotFile(journal, ftlPath);
110
+ await writeText(ftlPath, generateChromeDocFtl(args.name, ftlHeader));
111
+ written.push(`browser/locales/en-US/browser/${args.name}.ftl`);
112
+ // jar.mn registrations — XHTML + JS go through the `*` preprocessor
113
+ // for brand substitution, CSS goes through jar.inc.mn, FTL through
114
+ // the locale jar.
115
+ const jarMnPath = join(args.engineDir, 'browser/base/jar.mn');
116
+ for (const entry of jarMnEntriesForChromeDoc(args.name)) {
117
+ await appendJarEntryIfAbsent(jarMnPath, entry, journal);
118
+ }
119
+ written.push('browser/base/jar.mn');
120
+ const jarIncMnPath = join(args.engineDir, 'browser/themes/shared/jar.inc.mn');
121
+ await appendJarEntryIfAbsent(jarIncMnPath, jarIncMnEntryForChromeDoc(args.name), journal);
122
+ written.push('browser/themes/shared/jar.inc.mn');
123
+ const localeJarMnPath = join(args.engineDir, 'browser/locales/jar.mn');
124
+ await appendJarEntryIfAbsent(localeJarMnPath, localeJarMnEntryForChromeDoc(args.name), journal);
125
+ written.push('browser/locales/jar.mn');
126
+ }
127
+ catch (error) {
128
+ await restoreRollbackJournalOrThrow(journal, `Failed to scaffold chrome-doc "${args.name}"`);
129
+ throw error;
130
+ }
131
+ return written;
132
+ }
133
+ /**
134
+ * Runs `furnace chrome-doc create <name>`.
135
+ * @param projectRoot Root directory of the project.
136
+ * @param name Chrome-doc name (e.g. `mybrowser`, `aboutonboarding`).
137
+ * @param options CLI-provided options.
138
+ */
139
+ export async function furnaceChromeDocCreateCommand(projectRoot, name, options = {}) {
140
+ intro('Furnace chrome-doc create');
141
+ validateChromeDocName(name);
142
+ const forgeConfig = await loadConfig(projectRoot);
143
+ const license = forgeConfig.license ?? DEFAULT_LICENSE;
144
+ const engineDir = join(projectRoot, 'engine');
145
+ if (!(await pathExists(engineDir))) {
146
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first to scaffold a chrome-doc.');
147
+ }
148
+ const withTitlebar = options.titlebar ?? true;
149
+ const written = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocMutations({
150
+ name,
151
+ license,
152
+ engineDir,
153
+ withTitlebar,
154
+ operationContext: ctx,
155
+ }));
156
+ note([
157
+ `Chrome document "${name}" scaffolded:`,
158
+ ...written.map((f) => ` engine/${f}`),
159
+ '',
160
+ 'Next steps:',
161
+ ` 1. Edit engine/browser/base/content/${name}.xhtml and fill in the body.`,
162
+ ` 2. Localize strings in engine/browser/locales/en-US/browser/${name}.ftl.`,
163
+ ` 3. Run "fireforge build" to validate packaging (post-build audit will flag`,
164
+ ' any entry whose file does not land in the dist bundle).',
165
+ ].join('\n'), name);
166
+ outro('Chrome document created');
167
+ }
168
+ //# sourceMappingURL=chrome-doc.js.map
@@ -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
@@ -24,3 +24,56 @@ export declare function generateMjsContent(name: string, className: string, desc
24
24
  export declare function generateCssContent(header: string): string;
25
25
  /** Generates the .ftl file content for a custom component. */
26
26
  export declare function generateFtlContent(name: string, header: string): string;
27
+ /** Returns the canonical xpcshell test file basename for a component. */
28
+ export declare function xpcshellTestFileName(name: string): string;
29
+ /**
30
+ * Generates an xpcshell test file for a custom component.
31
+ *
32
+ * xpcshell tests run headless without a `tabbrowser`, so they suit
33
+ * storage/observer/module-loading code in forks that do not mount the
34
+ * upstream browser chrome (and therefore lack `openLinkIn` →
35
+ * `URILoadingHelper`). The scaffold imports the component module via
36
+ * `ChromeUtils.importESModule` and asserts the module resolves — enough
37
+ * to catch registration regressions without touching DOM rendering paths
38
+ * that xpcshell cannot execute.
39
+ */
40
+ export declare function generateXpcshellTestContent(name: string, header: string): string;
41
+ /**
42
+ * Generates an `xpcshell.toml` manifest for a custom component's test
43
+ * directory. Kept minimal — adding prefs, head.js, and support-files is
44
+ * left to the operator because those decisions depend on what the
45
+ * component actually touches (Services.storage, observer topics, etc.).
46
+ */
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;
@@ -81,6 +81,124 @@ export function generateFtlContent(name, header) {
81
81
  return `${header}
82
82
 
83
83
  ## Strings for the ${name} component
84
+ `;
85
+ }
86
+ /** Returns the canonical xpcshell test file basename for a component. */
87
+ export function xpcshellTestFileName(name) {
88
+ return `test_${name.replace(/-/g, '_')}_module_loads.js`;
89
+ }
90
+ /**
91
+ * Generates an xpcshell test file for a custom component.
92
+ *
93
+ * xpcshell tests run headless without a `tabbrowser`, so they suit
94
+ * storage/observer/module-loading code in forks that do not mount the
95
+ * upstream browser chrome (and therefore lack `openLinkIn` →
96
+ * `URILoadingHelper`). The scaffold imports the component module via
97
+ * `ChromeUtils.importESModule` and asserts the module resolves — enough
98
+ * to catch registration regressions without touching DOM rendering paths
99
+ * that xpcshell cannot execute.
100
+ */
101
+ export function generateXpcshellTestContent(name, header) {
102
+ return `${header}
103
+
104
+ "use strict";
105
+
106
+ add_task(async function test_${name.replace(/-/g, '_')}_module_loads() {
107
+ // Module-load smoke check: resolves the ESM at its registered chrome URI.
108
+ // Replace or extend with storage-layer assertions as the component grows
109
+ // (Services.storage, observer topics, JSONFile, etc. are all available
110
+ // here without a tabbrowser).
111
+ const moduleUri = "chrome://global/content/elements/${name}.mjs";
112
+ const module = await ChromeUtils.importESModule(moduleUri);
113
+ Assert.ok(
114
+ module,
115
+ "${name}.mjs should load under xpcshell (storage-layer code path).",
116
+ );
117
+ });
118
+ `;
119
+ }
120
+ /**
121
+ * Generates an `xpcshell.toml` manifest for a custom component's test
122
+ * directory. Kept minimal — adding prefs, head.js, and support-files is
123
+ * left to the operator because those decisions depend on what the
124
+ * component actually touches (Services.storage, observer topics, etc.).
125
+ */
126
+ export function generateXpcshellManifestContent(name, header) {
127
+ return `${header}
128
+
129
+ [DEFAULT]
130
+ head = ""
131
+
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
+
84
202
  `;
85
203
  }
86
204
  //# sourceMappingURL=create-templates.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * xpcshell test-harness scaffolder for `fireforge furnace create --xpcshell`.
3
+ * Extracted from `create.ts` so the command entrypoint stays under the
4
+ * per-file LOC budget and the scaffolder is unit-testable in isolation.
5
+ */
6
+ import { type RollbackJournal } from '../../core/furnace-rollback.js';
7
+ import type { ProjectLicense } from '../../types/config.js';
8
+ /**
9
+ * Scaffolds an xpcshell test harness for a newly created custom component.
10
+ *
11
+ * xpcshell is the appropriate harness for storage-layer code on forks
12
+ * without a `tabbrowser` (no `openLinkIn` → `URILoadingHelper`). Browser
13
+ * chrome mochitests require tabbrowser; xpcshell does not, so storage,
14
+ * observers, and ESM-loading logic can be covered headless.
15
+ *
16
+ * Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest
17
+ * into `engine/browser/base/content/test/<binary-name>-xpcshell/
18
+ * <component-name>/`. moz.build registration is intentionally left to the
19
+ * operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
20
+ * deliberate choice about which moz.build should own it, and an
21
+ * auto-insertion that guessed wrong would be worse than a note.
22
+ */
23
+ export declare function scaffoldXpcshellTestFiles(componentName: string, license: ProjectLicense, forgeConfig: {
24
+ binaryName: string;
25
+ }, paths: {
26
+ engine: string;
27
+ }, journal?: RollbackJournal): Promise<string[]>;
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * xpcshell test-harness scaffolder for `fireforge furnace create --xpcshell`.
4
+ * Extracted from `create.ts` so the command entrypoint stays under the
5
+ * per-file LOC budget and the scaffolder is unit-testable in isolation.
6
+ */
7
+ import { join } from 'node:path';
8
+ import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
9
+ import { getLicenseHeader } from '../../core/license-headers.js';
10
+ import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
11
+ import { warn } from '../../utils/logger.js';
12
+ import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellTestFileName, } from './create-templates.js';
13
+ /**
14
+ * Scaffolds an xpcshell test harness for a newly created custom component.
15
+ *
16
+ * xpcshell is the appropriate harness for storage-layer code on forks
17
+ * without a `tabbrowser` (no `openLinkIn` → `URILoadingHelper`). Browser
18
+ * chrome mochitests require tabbrowser; xpcshell does not, so storage,
19
+ * observers, and ESM-loading logic can be covered headless.
20
+ *
21
+ * Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest
22
+ * into `engine/browser/base/content/test/<binary-name>-xpcshell/
23
+ * <component-name>/`. moz.build registration is intentionally left to the
24
+ * operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
25
+ * deliberate choice about which moz.build should own it, and an
26
+ * auto-insertion that guessed wrong would be worse than a note.
27
+ */
28
+ export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal) {
29
+ const parentDirName = `${forgeConfig.binaryName}-xpcshell`;
30
+ const testDir = join(paths.engine, 'browser/base/content/test', parentDirName, componentName);
31
+ if (journal && !(await pathExists(testDir))) {
32
+ recordCreatedDir(journal, testDir);
33
+ }
34
+ await ensureDir(testDir);
35
+ const jsHeader = getLicenseHeader(license, 'js');
36
+ const hashHeader = getLicenseHeader(license, 'hash');
37
+ const testFiles = [];
38
+ const testFileName = xpcshellTestFileName(componentName);
39
+ const testFilePath = join(testDir, testFileName);
40
+ if (journal)
41
+ await snapshotFile(journal, testFilePath);
42
+ await writeText(testFilePath, generateXpcshellTestContent(componentName, jsHeader));
43
+ testFiles.push(testFileName);
44
+ const manifestPath = join(testDir, 'xpcshell.toml');
45
+ if (journal)
46
+ await snapshotFile(journal, manifestPath);
47
+ await writeText(manifestPath, generateXpcshellManifestContent(componentName, hashHeader));
48
+ testFiles.push('xpcshell.toml');
49
+ warn(`xpcshell scaffold written under browser/base/content/test/${parentDirName}/${componentName}/. ` +
50
+ 'Add the directory to XPCSHELL_TESTS_MANIFESTS in the nearest moz.build to run it via "fireforge test".');
51
+ return testFiles;
52
+ }
53
+ //# sourceMappingURL=create-xpcshell.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