@hominis/fireforge 0.15.6 → 0.15.8

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 (64) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +158 -15
  3. package/dist/src/commands/build.js +60 -3
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +17 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +18 -0
  6. package/dist/src/commands/furnace/chrome-doc-tests.d.ts +23 -0
  7. package/dist/src/commands/furnace/chrome-doc-tests.js +120 -0
  8. package/dist/src/commands/furnace/chrome-doc.d.ts +11 -0
  9. package/dist/src/commands/furnace/chrome-doc.js +37 -4
  10. package/dist/src/commands/furnace/create-dry-run.d.ts +38 -0
  11. package/dist/src/commands/furnace/create-dry-run.js +100 -0
  12. package/dist/src/commands/furnace/create-features.d.ts +24 -0
  13. package/dist/src/commands/furnace/create-features.js +56 -0
  14. package/dist/src/commands/furnace/create-templates.d.ts +9 -5
  15. package/dist/src/commands/furnace/create-templates.js +28 -6
  16. package/dist/src/commands/furnace/create.js +62 -63
  17. package/dist/src/commands/furnace/index.js +4 -1
  18. package/dist/src/commands/lint.d.ts +17 -2
  19. package/dist/src/commands/lint.js +25 -2
  20. package/dist/src/commands/register.d.ts +1 -1
  21. package/dist/src/commands/register.js +30 -7
  22. package/dist/src/commands/run.d.ts +15 -1
  23. package/dist/src/commands/run.js +202 -7
  24. package/dist/src/commands/test.js +113 -3
  25. package/dist/src/core/build-audit-registration.d.ts +80 -0
  26. package/dist/src/core/build-audit-registration.js +187 -0
  27. package/dist/src/core/build-audit-transforms.d.ts +23 -0
  28. package/dist/src/core/build-audit-transforms.js +94 -0
  29. package/dist/src/core/build-audit.js +107 -7
  30. package/dist/src/core/furnace-apply-ftl.d.ts +5 -3
  31. package/dist/src/core/furnace-apply-ftl.js +6 -2
  32. package/dist/src/core/furnace-apply-helpers.js +14 -4
  33. package/dist/src/core/furnace-config-custom.d.ts +14 -0
  34. package/dist/src/core/furnace-config-custom.js +64 -0
  35. package/dist/src/core/furnace-config.js +2 -39
  36. package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
  37. package/dist/src/core/furnace-validate-accessibility.js +17 -3
  38. package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
  39. package/dist/src/core/furnace-validate-helpers.js +19 -0
  40. package/dist/src/core/furnace-validate-registration.d.ts +6 -4
  41. package/dist/src/core/furnace-validate-registration.js +66 -6
  42. package/dist/src/core/furnace-validate-structure.js +6 -2
  43. package/dist/src/core/furnace-validate.js +6 -3
  44. package/dist/src/core/mach-build-artifacts.d.ts +44 -0
  45. package/dist/src/core/mach-build-artifacts.js +104 -3
  46. package/dist/src/core/mach.d.ts +27 -1
  47. package/dist/src/core/mach.js +26 -2
  48. package/dist/src/core/shared-ftl.d.ts +28 -0
  49. package/dist/src/core/shared-ftl.js +42 -0
  50. package/dist/src/core/smoke-patterns.d.ts +45 -0
  51. package/dist/src/core/smoke-patterns.js +100 -0
  52. package/dist/src/core/test-stale-check.d.ts +42 -0
  53. package/dist/src/core/test-stale-check.js +114 -0
  54. package/dist/src/core/xpcshell-appdir.d.ts +143 -0
  55. package/dist/src/core/xpcshell-appdir.js +273 -0
  56. package/dist/src/errors/codes.d.ts +13 -0
  57. package/dist/src/errors/codes.js +13 -0
  58. package/dist/src/errors/run.d.ts +16 -0
  59. package/dist/src/errors/run.js +22 -0
  60. package/dist/src/types/commands/options.d.ts +64 -0
  61. package/dist/src/types/furnace.d.ts +39 -0
  62. package/dist/src/utils/process.d.ts +63 -0
  63. package/dist/src/utils/process.js +122 -0
  64. package/package.json +1 -1
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Packaging-verification test templates for
4
+ * `fireforge furnace chrome-doc create --with-tests`.
5
+ *
6
+ * Motivation: an operator who scaffolds a top-level chrome document wants
7
+ * to know "did the file actually land in the packaged bundle?" before
8
+ * relying on it at runtime. Both natural test harnesses have gaps for
9
+ * this question:
10
+ *
11
+ * - xpcshell's `chrome://browser/*` URI registration lags what the real
12
+ * browser loads even with `firefox-appdir = "browser"` set, so
13
+ * `NetUtil.asyncFetch("chrome://browser/content/<name>.xhtml")` can
14
+ * fail with `NS_ERROR_FILE_NOT_FOUND` against a file that IS
15
+ * correctly packaged (the motivating case).
16
+ * - Browser-chrome mochitests require a `tabbrowser`, which a
17
+ * fork-authored chrome doc that replaces `browser.xhtml` deliberately
18
+ * does not carry (the URILoadingHelper crash path).
19
+ *
20
+ * This scaffold threads the needle by probing the filesystem directly:
21
+ * `Services.dirsvc.get("XCurProcD", Ci.nsIFile)` returns the current
22
+ * process directory (the browser app dir when `firefox-appdir = "browser"`),
23
+ * and the packaged chrome layout for a jar.mn entry
24
+ * `content/browser/<name>.xhtml` is stable across platforms at
25
+ * `<AppDir>/chrome/browser/content/browser/<name>.xhtml` on an unpacked
26
+ * tree (the default for `mach build` without `MOZ_CHROME_MULTILOCALE`
27
+ * / omnijar). A tree that packs omni.ja would need a different probe;
28
+ * the scaffold notes that out-of-scope case explicitly rather than
29
+ * silently producing a test that fails on packed builds.
30
+ */
31
+ /**
32
+ * Returns the canonical xpcshell test basename for a chrome doc packaging
33
+ * check (`test_<name>_packaging.js`). Hyphens in `<name>` are preserved —
34
+ * xpcshell permits them in file basenames even though the derived task
35
+ * function names replace them with underscores.
36
+ */
37
+ export function chromeDocPackagingTestFileName(name) {
38
+ return `test_${name}_packaging.js`;
39
+ }
40
+ /**
41
+ * Emits an xpcshell test that verifies the scaffolded chrome doc's
42
+ * `.xhtml`, `-chrome.css`, and `.ftl` are all present under the packaged
43
+ * app directory. Each assertion carries the exact probed path in its
44
+ * failure message so an operator reading a red CI run knows which
45
+ * jar.mn entry or build step dropped the file.
46
+ */
47
+ export function generateChromeDocPackagingTest(name, header) {
48
+ const taskSuffix = name.replace(/-/g, '_');
49
+ return `${header}
50
+
51
+ "use strict";
52
+
53
+ // Packaging verification for the "${name}" chrome document.
54
+ //
55
+ // Scope: reads the packaged tree under XCurProcD (the browser app dir
56
+ // when firefox-appdir = "browser") and asserts that the three
57
+ // scaffolded files landed where the jar.mn / jar.inc.mn / locales/jar.mn
58
+ // entries promised. Does NOT go through chrome:// URI resolution —
59
+ // xpcshell's chrome manifest set lags the real browser's even with
60
+ // firefox-appdir = "browser", so NetUtil.asyncFetch on
61
+ // chrome://browser/content/${name}.xhtml can fail with
62
+ // NS_ERROR_FILE_NOT_FOUND against a file that IS packaged.
63
+ //
64
+ // Out of scope: builds that pack omni.ja (MOZ_CHROME_MULTILOCALE, some
65
+ // release configs). The probe below assumes an unpacked tree, which is
66
+ // what "mach build" produces by default. A packed build would need to
67
+ // unzip omni.ja to verify the same files.
68
+
69
+ add_task(async function test_${taskSuffix}_files_packaged() {
70
+ const appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
71
+
72
+ function probe(segments, description) {
73
+ const file = appDir.clone();
74
+ for (const segment of segments) {
75
+ file.append(segment);
76
+ }
77
+ Assert.ok(
78
+ file.exists(),
79
+ description + " missing at " + file.path +
80
+ ' — run "fireforge build --ui" and retry. If the file is present under ' +
81
+ "obj-*/dist/ but absent from this path, the xpcshell harness is probing " +
82
+ "a stale build tree; the post-build audit should flag the same miss.",
83
+ );
84
+ Assert.greater(
85
+ file.fileSize,
86
+ 0,
87
+ description + " is zero-length at " + file.path +
88
+ " — packaging copied an empty file, check the source template.",
89
+ );
90
+ }
91
+
92
+ probe(
93
+ ["chrome", "browser", "content", "browser", "${name}.xhtml"],
94
+ "${name}.xhtml",
95
+ );
96
+ probe(
97
+ ["chrome", "browser", "skin", "classic", "browser", "${name}-chrome.css"],
98
+ "${name}-chrome.css",
99
+ );
100
+ });
101
+ `;
102
+ }
103
+ /**
104
+ * Emits the `xpcshell.toml` manifest for the packaging test directory.
105
+ * Sets `firefox-appdir = "browser"` so XCurProcD resolves to the browser
106
+ * subdir rather than the generic gecko runtime dir — without that, the
107
+ * path probe walks the wrong tree on every fork whose app directory is
108
+ * not the default.
109
+ */
110
+ export function generateChromeDocPackagingManifest(name, header) {
111
+ return `${header}
112
+
113
+ [DEFAULT]
114
+ head = ""
115
+ firefox-appdir = "browser"
116
+
117
+ ["${chromeDocPackagingTestFileName(name)}"]
118
+ `;
119
+ }
120
+ //# sourceMappingURL=chrome-doc-tests.js.map
@@ -24,6 +24,17 @@ export interface FurnaceChromeDocCreateOptions {
24
24
  * `--no-titlebar`.
25
25
  */
26
26
  titlebar?: boolean;
27
+ /**
28
+ * Scaffold an xpcshell packaging-verification test alongside the chrome
29
+ * document. The generated test probes `XCurProcD/chrome/browser/...` on
30
+ * disk rather than going through `chrome://` URI resolution — that
31
+ * bypasses xpcshell's limited browser-chrome manifest set (the
32
+ * motivating failure mode where `NetUtil.asyncFetch` returns
33
+ * `NS_ERROR_FILE_NOT_FOUND` against a file that IS packaged).
34
+ * Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator
35
+ * because the owning moz.build depends on the fork's layout.
36
+ */
37
+ withTests?: boolean;
27
38
  }
28
39
  /**
29
40
  * Runs `furnace chrome-doc create <name>`.
@@ -26,6 +26,7 @@ import { FurnaceError } from '../../errors/furnace.js';
26
26
  import { pathExists, readText, writeText } from '../../utils/fs.js';
27
27
  import { intro, note, outro } from '../../utils/logger.js';
28
28
  import { generateChromeDocCss, generateChromeDocFtl, generateChromeDocJs, generateChromeDocXhtml, jarIncMnEntryForChromeDoc, jarMnEntriesForChromeDoc, localeJarMnEntryForChromeDoc, } from './chrome-doc-templates.js';
29
+ import { chromeDocPackagingTestFileName, generateChromeDocPackagingManifest, generateChromeDocPackagingTest, } from './chrome-doc-tests.js';
29
30
  /** Chrome-doc name shape: lowercase ASCII, optional hyphens, no leading digit. */
30
31
  const CHROME_DOC_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
31
32
  /**
@@ -123,6 +124,28 @@ async function performChromeDocMutations(args) {
123
124
  const localeJarMnPath = join(args.engineDir, 'browser/locales/jar.mn');
124
125
  await appendJarEntryIfAbsent(localeJarMnPath, localeJarMnEntryForChromeDoc(args.name), journal);
125
126
  written.push('browser/locales/jar.mn');
127
+ // --with-tests scaffolds an xpcshell packaging verification. All writes
128
+ // go through the same rollback journal so a SIGINT here restores the
129
+ // source files and jar.mn edits above alongside the test scaffold.
130
+ if (args.withTests) {
131
+ const testParentDir = `${args.binaryName}-xpcshell`;
132
+ const testDir = join(args.engineDir, 'browser/base/content/test', testParentDir, args.name);
133
+ const { ensureDir } = await import('../../utils/fs.js');
134
+ if (!(await pathExists(testDir))) {
135
+ recordCreatedDir(journal, testDir);
136
+ }
137
+ await ensureDir(testDir);
138
+ const hashHeader = getLicenseHeader(args.license, 'hash');
139
+ const testFileName = chromeDocPackagingTestFileName(args.name);
140
+ const testFilePath = join(testDir, testFileName);
141
+ await snapshotFile(journal, testFilePath);
142
+ await writeText(testFilePath, generateChromeDocPackagingTest(args.name, jsHeader));
143
+ written.push(`browser/base/content/test/${testParentDir}/${args.name}/${testFileName}`);
144
+ const manifestPath = join(testDir, 'xpcshell.toml');
145
+ await snapshotFile(journal, manifestPath);
146
+ await writeText(manifestPath, generateChromeDocPackagingManifest(args.name, hashHeader));
147
+ written.push(`browser/base/content/test/${testParentDir}/${args.name}/xpcshell.toml`);
148
+ }
126
149
  }
127
150
  catch (error) {
128
151
  await restoreRollbackJournalOrThrow(journal, `Failed to scaffold chrome-doc "${args.name}"`);
@@ -146,22 +169,32 @@ export async function furnaceChromeDocCreateCommand(projectRoot, name, options =
146
169
  throw new FurnaceError('Engine directory not found. Run "fireforge download" first to scaffold a chrome-doc.');
147
170
  }
148
171
  const withTitlebar = options.titlebar ?? true;
172
+ const withTests = options.withTests ?? false;
149
173
  const written = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocMutations({
150
174
  name,
151
175
  license,
152
176
  engineDir,
153
177
  withTitlebar,
178
+ withTests,
179
+ binaryName: forgeConfig.binaryName,
154
180
  operationContext: ctx,
155
181
  }));
182
+ const nextSteps = [
183
+ ` 1. Edit engine/browser/base/content/${name}.xhtml and fill in the body.`,
184
+ ` 2. Localize strings in engine/browser/locales/en-US/browser/${name}.ftl.`,
185
+ ` 3. Run "fireforge build" to validate packaging (post-build audit will flag`,
186
+ ' any entry whose file does not land in the dist bundle).',
187
+ ];
188
+ if (withTests) {
189
+ nextSteps.push(` 4. Register the xpcshell test directory in the nearest moz.build under`, ` XPCSHELL_TESTS_MANIFESTS, then run "fireforge test browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${name}/xpcshell.toml".`);
190
+ }
191
+ nextSteps.push('', 'Platform-module compatibility: this chrome document carries the', ` data-furnace-chrome-doc="${name}" sentinel on its root element. Upstream`, ' platform modules (DevToolsStartup, PageActions, SessionStore, …) observe', ' "browser-delayed-startup-finished" and walk INTO the window assuming the', ' browser.xhtml DOM; use the sentinel attribute as a guard in fork-side', ' patches to those modules. See README "Platform module compatibility".');
156
192
  note([
157
193
  `Chrome document "${name}" scaffolded:`,
158
194
  ...written.map((f) => ` engine/${f}`),
159
195
  '',
160
196
  '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).',
197
+ ...nextSteps,
165
198
  ].join('\n'), name);
166
199
  outro('Chrome document created');
167
200
  }
@@ -0,0 +1,38 @@
1
+ import type { ResolvedTestStyle } from './create.js';
2
+ export interface DryRunPlanInput {
3
+ componentName: string;
4
+ localized: boolean;
5
+ register: boolean;
6
+ composes: string[] | undefined;
7
+ /**
8
+ * Feature-scoped Fluent bundle the component participates in (the same
9
+ * value that will be written to `furnace.json`'s `sharedFtl`). When set,
10
+ * the component's own `.ftl` is NOT scaffolded and the plan preview
11
+ * reflects that. Omit the key for the default per-component scaffold.
12
+ */
13
+ sharedFtl?: string;
14
+ testStyle: ResolvedTestStyle;
15
+ description: string;
16
+ binaryName: string;
17
+ }
18
+ /**
19
+ * Builds the success-note body printed after `furnace create` has applied
20
+ * its mutations. Lives beside the dry-run formatter so the two renderings
21
+ * stay in lock-step when the scaffolded layout changes.
22
+ */
23
+ export declare function formatSuccessNote(args: {
24
+ componentName: string;
25
+ files: string[];
26
+ testFiles: string[];
27
+ testStyle: ResolvedTestStyle;
28
+ binaryName: string;
29
+ }): string;
30
+ /**
31
+ * Builds the planned component + test file list for a dry-run preview.
32
+ *
33
+ * Mirrors the order `writeComponentFiles` and the test-style scaffolders
34
+ * would produce so the dry-run output matches what a real run prints on
35
+ * success. The component directory path is rendered relative to
36
+ * `components/custom/` to match the wording of the real success note.
37
+ */
38
+ export declare function formatDryRunPlan(args: DryRunPlanInput): string;
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Dry-run plan formatter for `furnace create`.
4
+ *
5
+ * Lives outside `create.ts` so the authoring command stays under the
6
+ * per-file LOC budget. The formatter is pure — all inputs are already
7
+ * resolved by the command's validation phase — so it can be exercised
8
+ * independently of the mutation plumbing.
9
+ */
10
+ /**
11
+ * Builds the test-section fragment of the dry-run plan for a given
12
+ * harness choice. Kept separate from the top-level formatter so the
13
+ * switch over `testStyle` does not push the caller over the per-function
14
+ * complexity budget.
15
+ */
16
+ function formatTestSection(args) {
17
+ const { testStyle, componentName, binaryName } = args;
18
+ if (testStyle === 'none')
19
+ return '';
20
+ const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
21
+ const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
22
+ ? strippedName.slice(binaryName.length + 1)
23
+ : strippedName;
24
+ const underscored = withoutBinaryPrefix.replace(/-/g, '_');
25
+ if (testStyle === 'browser-chrome') {
26
+ const testRoot = `engine/browser/base/content/test/${binaryName}/`;
27
+ return (`\n\nWould create test files in ${testRoot}:\n` +
28
+ ` browser.toml\n head.js\n browser_${binaryName}_${underscored}.js` +
29
+ `\n\nWould register ${binaryName}/browser.toml in engine/browser/base/moz.build`);
30
+ }
31
+ if (testStyle === 'xpcshell') {
32
+ const testRoot = `engine/browser/base/content/test/${binaryName}-xpcshell/${componentName}/`;
33
+ return `\n\nWould create xpcshell test files in ${testRoot}`;
34
+ }
35
+ // testStyle === 'mochikit' (last remaining branch in ResolvedTestStyle).
36
+ const testRoot = 'engine/toolkit/content/tests/widgets/';
37
+ return `\n\nWould create mochikit test file in ${testRoot}`;
38
+ }
39
+ /**
40
+ * Builds the success-note body printed after `furnace create` has applied
41
+ * its mutations. Lives beside the dry-run formatter so the two renderings
42
+ * stay in lock-step when the scaffolded layout changes.
43
+ */
44
+ export function formatSuccessNote(args) {
45
+ const { componentName, files, testFiles, testStyle, binaryName } = args;
46
+ let note = `Files created in components/custom/${componentName}/:\n` +
47
+ files.map((f) => ` ${f}`).join('\n');
48
+ if (testFiles.length > 0) {
49
+ let testRoot;
50
+ if (testStyle === 'xpcshell') {
51
+ testRoot = `engine/browser/base/content/test/${binaryName}-xpcshell/${componentName}/`;
52
+ }
53
+ else if (testStyle === 'mochikit') {
54
+ testRoot = 'engine/toolkit/content/tests/widgets/';
55
+ }
56
+ else {
57
+ testRoot = `engine/browser/base/content/test/${binaryName}/`;
58
+ }
59
+ note += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
60
+ }
61
+ note +=
62
+ '\n\n' +
63
+ 'Next steps:\n' +
64
+ ` 1. Edit component files in components/custom/${componentName}/\n` +
65
+ ' 2. Run "fireforge furnace preview" to see it\n' +
66
+ ' 3. Run "fireforge build" to apply and build';
67
+ return note;
68
+ }
69
+ /**
70
+ * Builds the planned component + test file list for a dry-run preview.
71
+ *
72
+ * Mirrors the order `writeComponentFiles` and the test-style scaffolders
73
+ * would produce so the dry-run output matches what a real run prints on
74
+ * success. The component directory path is rendered relative to
75
+ * `components/custom/` to match the wording of the real success note.
76
+ */
77
+ export function formatDryRunPlan(args) {
78
+ const { componentName, localized, register, composes, sharedFtl, testStyle, description, binaryName, } = args;
79
+ const componentFiles = [`${componentName}.mjs`, `${componentName}.css`];
80
+ // A per-component .ftl is scaffolded only when the component does NOT
81
+ // opt into a shared feature-scoped bundle. Mirrors writeComponentFiles.
82
+ if (localized && !sharedFtl)
83
+ componentFiles.push(`${componentName}.ftl`);
84
+ let plan = `Would create files in components/custom/${componentName}/:\n` +
85
+ componentFiles.map((f) => ` ${f}`).join('\n');
86
+ plan += formatTestSection({ testStyle, componentName, binaryName });
87
+ plan += `\n\nWould add custom entry to furnace.json:`;
88
+ plan += `\n name: ${componentName}`;
89
+ plan += `\n description: ${description || '(empty)'}`;
90
+ plan += `\n register: ${register}`;
91
+ plan += `\n localized: ${localized}`;
92
+ if (composes && composes.length > 0) {
93
+ plan += `\n composes: ${composes.join(', ')}`;
94
+ }
95
+ if (sharedFtl) {
96
+ plan += `\n sharedFtl: ${sharedFtl}`;
97
+ }
98
+ return plan;
99
+ }
100
+ //# sourceMappingURL=create-dry-run.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Feature-flag resolution for `furnace create`. Extracted from
3
+ * `create.ts` so the authoring command stays under the per-file LOC
4
+ * budget — the flag resolver has grown with each new opt-in (`--xpcshell`,
5
+ * `--test-style`, `--shared-ftl`).
6
+ */
7
+ import type { FurnaceCreateOptions } from '../../types/commands/index.js';
8
+ /**
9
+ * Resolves the localized and registration feature flags for a new component.
10
+ *
11
+ * `--shared-ftl` implies `localized` and short-circuits the interactive
12
+ * prompt so the operator is not asked to flip a flag we are about to
13
+ * enforce. `--no-localized` combined with `--shared-ftl` is rejected
14
+ * fast-fail; the cross-field check in furnace-config would catch it
15
+ * too, but later and without a clear command-line message.
16
+ *
17
+ * @param isInteractive - Whether interactive prompts are available
18
+ * @param options - CLI-provided feature flags
19
+ * @returns Final feature selections, or null when creation is cancelled
20
+ */
21
+ export declare function resolveCreateFeatures(isInteractive: boolean, options: FurnaceCreateOptions): Promise<{
22
+ localized: boolean;
23
+ register: boolean;
24
+ } | null>;
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Feature-flag resolution for `furnace create`. Extracted from
4
+ * `create.ts` so the authoring command stays under the per-file LOC
5
+ * budget — the flag resolver has grown with each new opt-in (`--xpcshell`,
6
+ * `--test-style`, `--shared-ftl`).
7
+ */
8
+ import { multiselect } from '@clack/prompts';
9
+ import { InvalidArgumentError } from '../../errors/base.js';
10
+ import { cancel, isCancel } from '../../utils/logger.js';
11
+ /**
12
+ * Resolves the localized and registration feature flags for a new component.
13
+ *
14
+ * `--shared-ftl` implies `localized` and short-circuits the interactive
15
+ * prompt so the operator is not asked to flip a flag we are about to
16
+ * enforce. `--no-localized` combined with `--shared-ftl` is rejected
17
+ * fast-fail; the cross-field check in furnace-config would catch it
18
+ * too, but later and without a clear command-line message.
19
+ *
20
+ * @param isInteractive - Whether interactive prompts are available
21
+ * @param options - CLI-provided feature flags
22
+ * @returns Final feature selections, or null when creation is cancelled
23
+ */
24
+ export async function resolveCreateFeatures(isInteractive, options) {
25
+ let localized = options.localized ?? false;
26
+ let register = options.register ?? true;
27
+ if (options.sharedFtl !== undefined) {
28
+ if (options.localized === false) {
29
+ throw new InvalidArgumentError('--shared-ftl requires localization. Remove --no-localized or drop --shared-ftl.', 'sharedFtl');
30
+ }
31
+ localized = true;
32
+ }
33
+ const featuresPromptSuppressed = options.sharedFtl !== undefined;
34
+ if (isInteractive &&
35
+ options.localized === undefined &&
36
+ options.register === undefined &&
37
+ !featuresPromptSuppressed) {
38
+ const features = await multiselect({
39
+ message: 'Component features:',
40
+ options: [
41
+ { value: 'localized', label: 'Fluent localization (data-l10n-id)' },
42
+ { value: 'register', label: 'Register in customElements.js' },
43
+ ],
44
+ initialValues: ['register'],
45
+ });
46
+ if (isCancel(features)) {
47
+ cancel('Create cancelled');
48
+ return null;
49
+ }
50
+ const selected = features;
51
+ localized = selected.includes('localized');
52
+ register = selected.includes('register');
53
+ }
54
+ return { localized, register };
55
+ }
56
+ //# sourceMappingURL=create-features.js.map
@@ -14,12 +14,16 @@
14
14
  * per-instance shadow-DOM Fluent attachment via `l10n.connectRoot`. We mirror
15
15
  * that pattern here so `--localized` produces functional code.
16
16
  *
17
- * The FTL path mirrors the locale jar.mn entry that `furnace apply` writes:
18
- * `<ftlChromeSubPath>/<name>.ftl`. For the default `toolkit/global` tree this
19
- * yields `toolkit/global/<name>.ftl`, which matches the URI upstream toolkit
20
- * widgets ship.
17
+ * Path resolution precedence (when `localized` is true):
18
+ * 1. `sharedFtl` used verbatim. The caller has resolved it from
19
+ * `--shared-ftl` / `furnace.json`; this template does no rewriting.
20
+ * Use this when the component participates in a feature-scoped
21
+ * bundle that another component owns.
22
+ * 2. `<ftlChromeSubPath>/<name>.ftl` — the default per-component path,
23
+ * matching the locale jar.mn entry that `furnace apply` writes.
24
+ * 3. `<name>.ftl` — fallback when no chrome sub-path was resolvable.
21
25
  */
22
- export declare function generateMjsContent(name: string, className: string, description: string, localized: boolean, header: string, ftlChromeSubPath: string | undefined): string;
26
+ export declare function generateMjsContent(name: string, className: string, description: string, localized: boolean, header: string, ftlChromeSubPath: string | undefined, sharedFtl: string | undefined): string;
23
27
  /** Generates the .css file content for a custom component. */
24
28
  export declare function generateCssContent(header: string): string;
25
29
  /** Generates the .ftl file content for a custom component. */
@@ -15,13 +15,21 @@
15
15
  * per-instance shadow-DOM Fluent attachment via `l10n.connectRoot`. We mirror
16
16
  * that pattern here so `--localized` produces functional code.
17
17
  *
18
- * The FTL path mirrors the locale jar.mn entry that `furnace apply` writes:
19
- * `<ftlChromeSubPath>/<name>.ftl`. For the default `toolkit/global` tree this
20
- * yields `toolkit/global/<name>.ftl`, which matches the URI upstream toolkit
21
- * widgets ship.
18
+ * Path resolution precedence (when `localized` is true):
19
+ * 1. `sharedFtl` used verbatim. The caller has resolved it from
20
+ * `--shared-ftl` / `furnace.json`; this template does no rewriting.
21
+ * Use this when the component participates in a feature-scoped
22
+ * bundle that another component owns.
23
+ * 2. `<ftlChromeSubPath>/<name>.ftl` — the default per-component path,
24
+ * matching the locale jar.mn entry that `furnace apply` writes.
25
+ * 3. `<name>.ftl` — fallback when no chrome sub-path was resolvable.
22
26
  */
23
- export function generateMjsContent(name, className, description, localized, header, ftlChromeSubPath) {
24
- const ftlPath = ftlChromeSubPath !== undefined ? `${ftlChromeSubPath}/${name}.ftl` : `${name}.ftl`;
27
+ export function generateMjsContent(name, className, description, localized, header, ftlChromeSubPath, sharedFtl) {
28
+ const ftlPath = sharedFtl !== undefined
29
+ ? sharedFtl
30
+ : ftlChromeSubPath !== undefined
31
+ ? `${ftlChromeSubPath}/${name}.ftl`
32
+ : `${name}.ftl`;
25
33
  const ftlModulePreamble = localized
26
34
  ? `
27
35
  window.MozXULElement?.insertFTLIfNeeded("${ftlPath}");
@@ -103,6 +111,20 @@ export function generateXpcshellTestContent(name, header) {
103
111
 
104
112
  "use strict";
105
113
 
114
+ // Chrome-URI access from xpcshell:
115
+ // Toolkit chrome (chrome://global/*) IS registered and resolvable from
116
+ // this harness — that is what the smoke assertion below uses.
117
+ //
118
+ // Browser chrome (chrome://browser/*) is NOT registered unless the
119
+ // xpcshell.toml sets firefox-appdir = "browser" AND the built app bundle
120
+ // has landed every packaged chrome manifest. Even then, the set of
121
+ // manifests xpcshell loads lags what the real browser loads, so
122
+ // NetUtil.asyncFetch("chrome://browser/content/…") can still fail with
123
+ // NS_ERROR_FILE_NOT_FOUND against an artifact that IS present in
124
+ // obj-*/dist/. Assertions that need browser chrome URIs belong in a
125
+ // browser-chrome mochitest (fireforge furnace create --test-style=browser-chrome),
126
+ // not xpcshell.
127
+
106
128
  add_task(async function test_${name.replace(/-/g, '_')}_module_loads() {
107
129
  // Module-load smoke check: resolves the ESM at its registered chrome URI.
108
130
  // Replace or extend with storage-layer assertions as the component grows