@hominis/fireforge 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/src/commands/build.js +9 -1
- package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
- package/dist/src/commands/doctor-furnace-jar.js +47 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/export-flow.js +4 -1
- package/dist/src/commands/export-placement-gate.js +4 -1
- package/dist/src/commands/export.js +61 -4
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
- package/dist/src/commands/furnace/chrome-doc.js +31 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
- package/dist/src/commands/furnace/create-browser-test.js +180 -0
- package/dist/src/commands/furnace/create-mochikit.js +11 -4
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +38 -12
- package/dist/src/commands/furnace/create.js +7 -114
- package/dist/src/commands/furnace/index.js +6 -1
- package/dist/src/commands/furnace/scan.d.ts +1 -0
- package/dist/src/commands/furnace/scan.js +57 -27
- package/dist/src/commands/furnace/validate.js +17 -1
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-options.d.ts +26 -0
- package/dist/src/commands/re-export-options.js +48 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +10 -1
- package/dist/src/commands/test-diagnose.js +12 -0
- package/dist/src/commands/test.js +29 -22
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +26 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-registration.d.ts +24 -0
- package/dist/src/core/furnace-registration.js +56 -3
- package/dist/src/core/furnace-validate-registration.js +18 -0
- package/dist/src/core/mach-resource-shim.d.ts +58 -0
- package/dist/src/core/mach-resource-shim.js +290 -0
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -25
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/manifest-rules.d.ts +13 -3
- package/dist/src/core/manifest-rules.js +44 -17
- package/dist/src/core/patch-export.d.ts +10 -0
- package/dist/src/core/patch-export.js +23 -2
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-module.js +15 -2
- package/dist/src/core/register-result.d.ts +9 -0
- package/dist/src/core/register-scaffold.d.ts +55 -0
- package/dist/src/core/register-scaffold.js +146 -0
- package/dist/src/core/register-test-manifest.js +3 -1
- package/dist/src/core/register-xpcshell-test.d.ts +30 -0
- package/dist/src/core/register-xpcshell-test.js +96 -0
- package/dist/src/core/test-harness-crash.d.ts +32 -4
- package/dist/src/core/test-harness-crash.js +152 -14
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/core/xpcshell-appdir.d.ts +7 -0
- package/dist/src/core/xpcshell-appdir.js +12 -3
- package/dist/src/types/commands/options.d.ts +17 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +4 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Browser-chrome test scaffolding for `furnace create --with-tests`,
|
|
4
|
+
* including the 0.34.0 `--test-dir` redirect and collision safety
|
|
5
|
+
* (existing manifests are appended to; head.js and test implementations
|
|
6
|
+
* are never overwritten). Split out of `create.ts` to keep that command
|
|
7
|
+
* file within the per-file line budget.
|
|
8
|
+
*/
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
11
|
+
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
12
|
+
import { registerTestManifest } from '../../core/manifest-register.js';
|
|
13
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
14
|
+
import { toError } from '../../utils/errors.js';
|
|
15
|
+
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
16
|
+
import { info, success, warn } from '../../utils/logger.js';
|
|
17
|
+
import { stripEnginePrefix } from '../../utils/paths.js';
|
|
18
|
+
/**
|
|
19
|
+
* Validates the `--test-dir` option against the resolved test style before
|
|
20
|
+
* any writes. Mochikit lives in the upstream toolkit/content/tests/widgets
|
|
21
|
+
* tree, so a redirect makes no sense there.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveValidatedTestDir(rawTestDir, testStyle) {
|
|
24
|
+
if (rawTestDir === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (testStyle === 'none') {
|
|
27
|
+
throw new InvalidArgumentError('--test-dir requires --with-tests / --test-style.', 'testDir');
|
|
28
|
+
}
|
|
29
|
+
if (testStyle === 'mochikit') {
|
|
30
|
+
throw new InvalidArgumentError('--test-dir is not supported with --test-style=mochikit (the mochikit harness lives in toolkit/content/tests/widgets).', 'testDir');
|
|
31
|
+
}
|
|
32
|
+
return resolveTestDirOverride(rawTestDir);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Scaffolds browser mochitest files for a newly created custom component.
|
|
36
|
+
* @param componentName - Custom element tag name
|
|
37
|
+
* @param license - Project license used for generated headers
|
|
38
|
+
* @param forgeConfig - Project config fields needed for test naming
|
|
39
|
+
* @param paths - Resolved project paths used to place test files
|
|
40
|
+
* @param journal - Optional rollback journal that snapshots files before writes
|
|
41
|
+
* @returns Relative test filenames created or updated for the component
|
|
42
|
+
*/
|
|
43
|
+
const TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes and validates a `--test-dir` override: engine-relative,
|
|
46
|
+
* under `browser/base/content/test/` (so manifest registration keeps
|
|
47
|
+
* working). Returns the normalized engine-relative directory.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveTestDirOverride(raw) {
|
|
50
|
+
const normalized = stripEnginePrefix(raw).replace(/\/+$/, '');
|
|
51
|
+
if (!normalized.startsWith(TEST_SCAFFOLD_ROOT) ||
|
|
52
|
+
normalized === TEST_SCAFFOLD_ROOT.slice(0, -1)) {
|
|
53
|
+
throw new InvalidArgumentError(`--test-dir must be an engine-relative directory under ${TEST_SCAFFOLD_ROOT} (got "${raw}").`, 'testDir');
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
// Exported for direct unit testing of the --test-dir / collision-safety
|
|
58
|
+
// behaviour (the full create command needs a furnace project fixture).
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
export async function scaffoldTestFiles(componentName, license, forgeConfig, paths, journal, testDirOverride) {
|
|
63
|
+
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
64
|
+
// Avoid double-prefixing: strip binaryName prefix since the default test
|
|
65
|
+
// dir name already uses it
|
|
66
|
+
const binaryName = forgeConfig.binaryName;
|
|
67
|
+
const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
|
|
68
|
+
? strippedName.slice(binaryName.length + 1)
|
|
69
|
+
: strippedName;
|
|
70
|
+
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
71
|
+
const testFileName = `browser_${binaryName}_${underscored}.js`;
|
|
72
|
+
// --test-dir redirects the scaffold (0.34.0 field report: the hardcoded
|
|
73
|
+
// .../test/<binaryName>/ target collided with a test suite owned by a
|
|
74
|
+
// different patch). The manifest-registration name is the path below
|
|
75
|
+
// browser/base/content/test/ (nested manifests are supported).
|
|
76
|
+
const testDirRel = testDirOverride ?? `${TEST_SCAFFOLD_ROOT}${binaryName}`;
|
|
77
|
+
const testDirName = testDirRel.slice(TEST_SCAFFOLD_ROOT.length);
|
|
78
|
+
const testDir = join(paths.engine, testDirRel);
|
|
79
|
+
if (journal && !(await pathExists(testDir))) {
|
|
80
|
+
recordCreatedDir(journal, testDir);
|
|
81
|
+
}
|
|
82
|
+
await ensureDir(testDir);
|
|
83
|
+
const jsHeader = getLicenseHeader(license, 'js');
|
|
84
|
+
const hashHeader = getLicenseHeader(license, 'hash');
|
|
85
|
+
const testFiles = [];
|
|
86
|
+
// browser.toml — create if missing, append entry if existing
|
|
87
|
+
const tomlPath = join(testDir, 'browser.toml');
|
|
88
|
+
if (await pathExists(tomlPath)) {
|
|
89
|
+
// Defensive guard: only append if the entry is not already present.
|
|
90
|
+
// With a fresh journal per create, the same test file name cannot be
|
|
91
|
+
// appended twice in a single run — but retaining the check protects
|
|
92
|
+
// against accidental re-entrance or a future refactor that reuses the
|
|
93
|
+
// helper with a stale test directory.
|
|
94
|
+
const existingToml = await readText(tomlPath);
|
|
95
|
+
if (!existingToml.includes(`["${testFileName}"]`)) {
|
|
96
|
+
if (journal)
|
|
97
|
+
await snapshotFile(journal, tomlPath);
|
|
98
|
+
await writeText(tomlPath, existingToml.trimEnd() + `\n\n["${testFileName}"]\n`);
|
|
99
|
+
info(`Appended ["${testFileName}"] to the existing ${testDirRel}/browser.toml — the manifest ` +
|
|
100
|
+
'is shared; existing entries and support-files were left untouched.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
if (journal)
|
|
105
|
+
await snapshotFile(journal, tomlPath);
|
|
106
|
+
const browserToml = `${hashHeader}
|
|
107
|
+
|
|
108
|
+
[DEFAULT]
|
|
109
|
+
support-files = ["head.js"]
|
|
110
|
+
|
|
111
|
+
["${testFileName}"]
|
|
112
|
+
`;
|
|
113
|
+
await writeText(tomlPath, browserToml);
|
|
114
|
+
}
|
|
115
|
+
testFiles.push('browser.toml');
|
|
116
|
+
// head.js — only create if it doesn't exist (shared across components)
|
|
117
|
+
const headPath = join(testDir, 'head.js');
|
|
118
|
+
if (!(await pathExists(headPath))) {
|
|
119
|
+
if (journal)
|
|
120
|
+
await snapshotFile(journal, headPath);
|
|
121
|
+
const headJs = `${jsHeader}
|
|
122
|
+
|
|
123
|
+
"use strict";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wait for a custom element to be defined.
|
|
127
|
+
* @param {string} tag - Custom element tag name
|
|
128
|
+
* @returns {Promise<CustomElementConstructor>}
|
|
129
|
+
*/
|
|
130
|
+
async function waitForElement(tag) {
|
|
131
|
+
document.createElement(tag);
|
|
132
|
+
return customElements.whenDefined(tag);
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
await writeText(headPath, headJs);
|
|
136
|
+
testFiles.push('head.js');
|
|
137
|
+
}
|
|
138
|
+
// browser_{binaryName}_{stripped}.js
|
|
139
|
+
const testJs = `${jsHeader}
|
|
140
|
+
|
|
141
|
+
"use strict";
|
|
142
|
+
|
|
143
|
+
add_task(async function test_${underscored}_defined() {
|
|
144
|
+
const ctor = await waitForElement("${componentName}");
|
|
145
|
+
Assert.ok(ctor, "${componentName} custom element should be defined");
|
|
146
|
+
Assert.equal(typeof ctor, "function", "Constructor should be a function");
|
|
147
|
+
});
|
|
148
|
+
`;
|
|
149
|
+
const testFilePath = join(testDir, testFileName);
|
|
150
|
+
if (await pathExists(testFilePath)) {
|
|
151
|
+
// Never clobber an existing test implementation (it may be owned by a
|
|
152
|
+
// different patch). The manifest entry above is idempotent, so the
|
|
153
|
+
// existing file simply stays authoritative.
|
|
154
|
+
warn(`${testDirRel}/${testFileName} already exists — keeping the existing file. ` +
|
|
155
|
+
'Pass --test-dir to scaffold into a different directory if you wanted a fresh test.');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (journal)
|
|
159
|
+
await snapshotFile(journal, testFilePath);
|
|
160
|
+
await writeText(testFilePath, testJs);
|
|
161
|
+
testFiles.push(testFileName);
|
|
162
|
+
}
|
|
163
|
+
// Register in moz.build. The registration helper edits browser/base/moz.build,
|
|
164
|
+
// so snapshot it first when a journal is supplied. The existing warn-and-continue
|
|
165
|
+
// contract is preserved so a missing/unparseable moz.build never trips rollback.
|
|
166
|
+
try {
|
|
167
|
+
const mozBuildPath = join(paths.engine, 'browser/base/moz.build');
|
|
168
|
+
if (journal)
|
|
169
|
+
await snapshotFile(journal, mozBuildPath);
|
|
170
|
+
const registerResult = await registerTestManifest(paths.engine, testDirName);
|
|
171
|
+
if (!registerResult.skipped) {
|
|
172
|
+
success(`Registered test manifest in ${registerResult.manifest}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
warn(`Could not register test manifest in moz.build — ${toError(error).message}. Register manually with "fireforge register".`);
|
|
177
|
+
}
|
|
178
|
+
return testFiles;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=create-browser-test.js.map
|
|
@@ -40,10 +40,17 @@ export async function scaffoldMochikitTestFiles(componentName, license, paths, j
|
|
|
40
40
|
const writtenFiles = [];
|
|
41
41
|
const testFileName = mochikitTestFileName(componentName);
|
|
42
42
|
const testFilePath = join(testDir, testFileName);
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (await pathExists(testFilePath)) {
|
|
44
|
+
// Never clobber an existing test implementation (0.34.0 collision
|
|
45
|
+
// safety, matching the browser-chrome and xpcshell scaffolds).
|
|
46
|
+
warn(`toolkit/content/tests/widgets/${testFileName} already exists — keeping the existing file.`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (journal)
|
|
50
|
+
await snapshotFile(journal, testFilePath);
|
|
51
|
+
await writeText(testFilePath, generateMochikitTestContent(componentName));
|
|
52
|
+
writtenFiles.push(testFileName);
|
|
53
|
+
}
|
|
47
54
|
// chrome.toml — append entry if the file already exists, otherwise write
|
|
48
55
|
// a fresh skeleton + entry. Idempotency: if the entry is already present
|
|
49
56
|
// the manifest is left untouched so re-runs don't double-register.
|
|
@@ -24,4 +24,4 @@ export declare function scaffoldXpcshellTestFiles(componentName: string, license
|
|
|
24
24
|
binaryName: string;
|
|
25
25
|
}, paths: {
|
|
26
26
|
engine: string;
|
|
27
|
-
}, journal?: RollbackJournal): Promise<string[]>;
|
|
27
|
+
}, journal?: RollbackJournal, testDirOverride?: string): Promise<string[]>;
|
|
@@ -8,7 +8,7 @@ import { join } from 'node:path';
|
|
|
8
8
|
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
9
9
|
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
10
10
|
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
11
|
-
import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
|
|
11
|
+
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
12
12
|
import { warn } from '../../utils/logger.js';
|
|
13
13
|
import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellTestFileName, } from './create-templates.js';
|
|
14
14
|
/**
|
|
@@ -26,10 +26,13 @@ import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellT
|
|
|
26
26
|
* deliberate choice about which moz.build should own it, and an
|
|
27
27
|
* auto-insertion that guessed wrong would be worse than a note.
|
|
28
28
|
*/
|
|
29
|
-
export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
29
|
+
export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal, testDirOverride) {
|
|
30
30
|
const parentRelDir = xpcshellTestParentDir(forgeConfig.binaryName);
|
|
31
31
|
const parentDirName = parentRelDir.split('/').slice(-1)[0] ?? `${forgeConfig.binaryName}-xpcshell`;
|
|
32
|
-
|
|
32
|
+
// --test-dir names the FINAL directory (no per-component segment is
|
|
33
|
+
// appended) so the operator controls the exact scaffold target.
|
|
34
|
+
const testDirRel = testDirOverride ?? `${parentRelDir}/${componentName}`;
|
|
35
|
+
const testDir = join(paths.engine, testDirRel);
|
|
33
36
|
if (journal && !(await pathExists(testDir))) {
|
|
34
37
|
recordCreatedDir(journal, testDir);
|
|
35
38
|
}
|
|
@@ -39,16 +42,39 @@ export async function scaffoldXpcshellTestFiles(componentName, license, forgeCon
|
|
|
39
42
|
const testFiles = [];
|
|
40
43
|
const testFileName = xpcshellTestFileName(componentName);
|
|
41
44
|
const testFilePath = join(testDir, testFileName);
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (await pathExists(testFilePath)) {
|
|
46
|
+
// Never clobber an existing test implementation (0.34.0 field report:
|
|
47
|
+
// the scaffold overwrote files owned by a different patch).
|
|
48
|
+
warn(`${testDirRel}/${testFileName} already exists — keeping the existing file.`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (journal)
|
|
52
|
+
await snapshotFile(journal, testFilePath);
|
|
53
|
+
await writeText(testFilePath, generateXpcshellTestContent(componentName, jsHeader));
|
|
54
|
+
testFiles.push(testFileName);
|
|
55
|
+
}
|
|
56
|
+
// xpcshell.toml — append the test entry to an existing manifest instead
|
|
57
|
+
// of scaffolding over it; write a fresh one only when absent.
|
|
46
58
|
const manifestPath = join(testDir, 'xpcshell.toml');
|
|
47
|
-
if (
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
if (await pathExists(manifestPath)) {
|
|
60
|
+
const existing = await readText(manifestPath);
|
|
61
|
+
if (!existing.includes(`["${testFileName}"]`)) {
|
|
62
|
+
if (journal)
|
|
63
|
+
await snapshotFile(journal, manifestPath);
|
|
64
|
+
await writeText(manifestPath, existing.trimEnd() + `\n\n["${testFileName}"]\n`);
|
|
65
|
+
warn(`Appended ["${testFileName}"] to the existing ${testDirRel}/xpcshell.toml — the manifest is shared; existing entries were left untouched.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (journal)
|
|
70
|
+
await snapshotFile(journal, manifestPath);
|
|
71
|
+
await writeText(manifestPath, generateXpcshellManifestContent(componentName, hashHeader));
|
|
72
|
+
testFiles.push('xpcshell.toml');
|
|
73
|
+
}
|
|
74
|
+
warn(`xpcshell scaffold written under ${testDirRel}/ ` +
|
|
75
|
+
'(default: browser/base/content/test/' +
|
|
76
|
+
parentDirName +
|
|
77
|
+
'/<component>/). ' +
|
|
52
78
|
'Add the directory to XPCSHELL_TESTS_MANIFESTS in the nearest moz.build to run it via "fireforge test".');
|
|
53
79
|
return testFiles;
|
|
54
80
|
}
|
|
@@ -9,13 +9,13 @@ import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../cor
|
|
|
9
9
|
import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
10
10
|
import { isComponentInEngine, scanWidgetsDirectory } from '../../core/furnace-scanner.js';
|
|
11
11
|
import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
|
|
12
|
-
import { registerTestManifest } from '../../core/manifest-register.js';
|
|
13
12
|
import { validateSharedFtl } from '../../core/shared-ftl.js';
|
|
14
13
|
import { InvalidArgumentError } from '../../errors/base.js';
|
|
15
14
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
16
15
|
import { toError } from '../../utils/errors.js';
|
|
17
|
-
import { ensureDir, pathExists,
|
|
18
|
-
import { cancel, intro, isCancel, note, outro
|
|
16
|
+
import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
|
|
17
|
+
import { cancel, intro, isCancel, note, outro } from '../../utils/logger.js';
|
|
18
|
+
import { resolveValidatedTestDir, scaffoldTestFiles } from './create-browser-test.js';
|
|
19
19
|
import { formatDryRunPlan, formatSuccessNote } from './create-dry-run.js';
|
|
20
20
|
import { resolveCreateFeatures } from './create-features.js';
|
|
21
21
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
@@ -62,115 +62,6 @@ function validateTagName(name) {
|
|
|
62
62
|
return `Name ${CUSTOM_ELEMENT_TAG_RULES}`;
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Scaffolds browser mochitest files for a newly created custom component.
|
|
67
|
-
* @param componentName - Custom element tag name
|
|
68
|
-
* @param license - Project license used for generated headers
|
|
69
|
-
* @param forgeConfig - Project config fields needed for test naming
|
|
70
|
-
* @param paths - Resolved project paths used to place test files
|
|
71
|
-
* @param journal - Optional rollback journal that snapshots files before writes
|
|
72
|
-
* @returns Relative test filenames created or updated for the component
|
|
73
|
-
*/
|
|
74
|
-
async function scaffoldTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
75
|
-
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
76
|
-
// Avoid double-prefixing: strip binaryName prefix since testDirName already uses it
|
|
77
|
-
const testDirName = forgeConfig.binaryName;
|
|
78
|
-
const withoutBinaryPrefix = strippedName.startsWith(testDirName + '-')
|
|
79
|
-
? strippedName.slice(testDirName.length + 1)
|
|
80
|
-
: strippedName;
|
|
81
|
-
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
82
|
-
const testFileName = `browser_${testDirName}_${underscored}.js`;
|
|
83
|
-
const testDir = join(paths.engine, 'browser/base/content/test', testDirName);
|
|
84
|
-
if (journal && !(await pathExists(testDir))) {
|
|
85
|
-
recordCreatedDir(journal, testDir);
|
|
86
|
-
}
|
|
87
|
-
await ensureDir(testDir);
|
|
88
|
-
const jsHeader = getLicenseHeader(license, 'js');
|
|
89
|
-
const hashHeader = getLicenseHeader(license, 'hash');
|
|
90
|
-
const testFiles = [];
|
|
91
|
-
// browser.toml — create if missing, append entry if existing
|
|
92
|
-
const tomlPath = join(testDir, 'browser.toml');
|
|
93
|
-
if (await pathExists(tomlPath)) {
|
|
94
|
-
// Defensive guard: only append if the entry is not already present.
|
|
95
|
-
// With a fresh journal per create, the same test file name cannot be
|
|
96
|
-
// appended twice in a single run — but retaining the check protects
|
|
97
|
-
// against accidental re-entrance or a future refactor that reuses the
|
|
98
|
-
// helper with a stale test directory.
|
|
99
|
-
const existingToml = await readText(tomlPath);
|
|
100
|
-
if (!existingToml.includes(`["${testFileName}"]`)) {
|
|
101
|
-
if (journal)
|
|
102
|
-
await snapshotFile(journal, tomlPath);
|
|
103
|
-
await writeText(tomlPath, existingToml.trimEnd() + `\n\n["${testFileName}"]\n`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
if (journal)
|
|
108
|
-
await snapshotFile(journal, tomlPath);
|
|
109
|
-
const browserToml = `${hashHeader}
|
|
110
|
-
|
|
111
|
-
[DEFAULT]
|
|
112
|
-
support-files = ["head.js"]
|
|
113
|
-
|
|
114
|
-
["${testFileName}"]
|
|
115
|
-
`;
|
|
116
|
-
await writeText(tomlPath, browserToml);
|
|
117
|
-
}
|
|
118
|
-
testFiles.push('browser.toml');
|
|
119
|
-
// head.js — only create if it doesn't exist (shared across components)
|
|
120
|
-
const headPath = join(testDir, 'head.js');
|
|
121
|
-
if (!(await pathExists(headPath))) {
|
|
122
|
-
if (journal)
|
|
123
|
-
await snapshotFile(journal, headPath);
|
|
124
|
-
const headJs = `${jsHeader}
|
|
125
|
-
|
|
126
|
-
"use strict";
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Wait for a custom element to be defined.
|
|
130
|
-
* @param {string} tag - Custom element tag name
|
|
131
|
-
* @returns {Promise<CustomElementConstructor>}
|
|
132
|
-
*/
|
|
133
|
-
async function waitForElement(tag) {
|
|
134
|
-
document.createElement(tag);
|
|
135
|
-
return customElements.whenDefined(tag);
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
await writeText(headPath, headJs);
|
|
139
|
-
testFiles.push('head.js');
|
|
140
|
-
}
|
|
141
|
-
// browser_{binaryName}_{stripped}.js
|
|
142
|
-
const testJs = `${jsHeader}
|
|
143
|
-
|
|
144
|
-
"use strict";
|
|
145
|
-
|
|
146
|
-
add_task(async function test_${underscored}_defined() {
|
|
147
|
-
const ctor = await waitForElement("${componentName}");
|
|
148
|
-
Assert.ok(ctor, "${componentName} custom element should be defined");
|
|
149
|
-
Assert.equal(typeof ctor, "function", "Constructor should be a function");
|
|
150
|
-
});
|
|
151
|
-
`;
|
|
152
|
-
const testFilePath = join(testDir, testFileName);
|
|
153
|
-
if (journal)
|
|
154
|
-
await snapshotFile(journal, testFilePath);
|
|
155
|
-
await writeText(testFilePath, testJs);
|
|
156
|
-
testFiles.push(testFileName);
|
|
157
|
-
// Register in moz.build. The registration helper edits browser/base/moz.build,
|
|
158
|
-
// so snapshot it first when a journal is supplied. The existing warn-and-continue
|
|
159
|
-
// contract is preserved so a missing/unparseable moz.build never trips rollback.
|
|
160
|
-
try {
|
|
161
|
-
const mozBuildPath = join(paths.engine, 'browser/base/moz.build');
|
|
162
|
-
if (journal)
|
|
163
|
-
await snapshotFile(journal, mozBuildPath);
|
|
164
|
-
const registerResult = await registerTestManifest(paths.engine, testDirName);
|
|
165
|
-
if (!registerResult.skipped) {
|
|
166
|
-
success(`Registered test manifest in ${registerResult.manifest}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
warn(`Could not register test manifest in moz.build — ${toError(error).message}. Register manually with "fireforge register".`);
|
|
171
|
-
}
|
|
172
|
-
return testFiles;
|
|
173
|
-
}
|
|
174
65
|
/**
|
|
175
66
|
* Writes the scaffolded component source files to disk.
|
|
176
67
|
* @param componentDir - Destination component directory
|
|
@@ -295,11 +186,11 @@ async function performCreateMutations(args) {
|
|
|
295
186
|
await writeFurnaceConfig(args.projectRoot, freshConfig);
|
|
296
187
|
await assertCustomEntryPersisted(args.projectRoot, args.componentName);
|
|
297
188
|
if (args.testStyle === 'browser-chrome') {
|
|
298
|
-
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
189
|
+
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal, args.testDir);
|
|
299
190
|
testFiles.push(...scafFiles);
|
|
300
191
|
}
|
|
301
192
|
else if (args.testStyle === 'xpcshell') {
|
|
302
|
-
const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
193
|
+
const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal, args.testDir);
|
|
303
194
|
testFiles.push(...xpcshellFiles);
|
|
304
195
|
}
|
|
305
196
|
else if (args.testStyle === 'mochikit') {
|
|
@@ -409,6 +300,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
409
300
|
// incompatible combinations up-front so a bad flag set never strands a
|
|
410
301
|
// partial mutation behind.
|
|
411
302
|
const testStyle = resolveTestStyle(options);
|
|
303
|
+
const testDir = resolveValidatedTestDir(options.testDir, testStyle);
|
|
412
304
|
if (testStyle !== 'none' && !(await pathExists(paths.engine))) {
|
|
413
305
|
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests, --xpcshell, or --test-style.', componentName);
|
|
414
306
|
}
|
|
@@ -477,6 +369,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
477
369
|
paths,
|
|
478
370
|
license,
|
|
479
371
|
testStyle,
|
|
372
|
+
testDir,
|
|
480
373
|
operationContext: ctx,
|
|
481
374
|
}));
|
|
482
375
|
note(formatSuccessNote({
|
|
@@ -52,8 +52,11 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
52
52
|
}));
|
|
53
53
|
furnace
|
|
54
54
|
.command('scan')
|
|
55
|
-
.description('Scan engine for available components'
|
|
55
|
+
.description('Scan engine for available components. Report-only by default: persist the discovered ' +
|
|
56
|
+
'inventory into the stock section of furnace.json with --track (or the interactive ' +
|
|
57
|
+
'prompt); deploy/validate consume the inventory from there.')
|
|
56
58
|
.option('--deep', 'Search additional Firefox directories beyond the default widgets path')
|
|
59
|
+
.option('--track', 'Persist every discovered untracked component into the stock section of furnace.json (non-interactive)')
|
|
57
60
|
.action(withErrorHandling(async (options) => {
|
|
58
61
|
await furnaceScanCommand(getProjectRoot(), pickDefined(options));
|
|
59
62
|
}));
|
|
@@ -80,6 +83,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
80
83
|
}
|
|
81
84
|
return value;
|
|
82
85
|
})
|
|
86
|
+
.option('--test-dir <dir>', 'Redirect the --with-tests scaffold to this engine-relative directory under browser/base/content/test/ (browser-chrome and xpcshell styles) instead of the default <binaryName>-derived directory; existing manifests are appended to, never overwritten')
|
|
83
87
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
84
88
|
.option('--shared-ftl <path>', 'Participate in an existing feature-scoped .ftl at this path (e.g. "browser/mybrowser-dock.ftl"); skips the per-component .ftl scaffold (implies --localized)')
|
|
85
89
|
.option('--dry-run', 'Show the planned file set and furnace.json changes without writing')
|
|
@@ -98,6 +102,7 @@ function registerChromeDocCommands(furnace, context) {
|
|
|
98
102
|
.command('create <name>')
|
|
99
103
|
.description('Scaffold a new top-level chrome document')
|
|
100
104
|
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
105
|
+
.option('--browser-window', 'Emit the browser.xhtml-like main-window skeleton (<html id="main-window"> with windowtype/chromehidden/persist root attributes platform C++ reads before scripts run) for the document configured as the fork\'s main browser window; implies the titlebar markup')
|
|
101
106
|
.option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
|
|
102
107
|
.option('--dry-run', 'Show the chrome-doc scaffold plan without writing')
|
|
103
108
|
.action(withErrorHandling(async (name, options) => {
|
|
@@ -45,12 +45,41 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
45
45
|
outro('Scan complete');
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
await persistStockComponents(projectRoot, selected);
|
|
49
|
+
const addedNames = selected;
|
|
50
|
+
success(`Added ${addedNames.length} component${addedNames.length === 1 ? '' : 's'} to furnace.json`);
|
|
51
|
+
// Offer to immediately override one of the just-added stock components.
|
|
52
|
+
const shouldOverride = await confirm({
|
|
53
|
+
message: 'Override any of the newly added components?',
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(shouldOverride) || !shouldOverride) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const overrideTarget = await select({
|
|
59
|
+
message: 'Select a component to override',
|
|
60
|
+
options: addedNames.map((name) => ({ value: name, label: name })),
|
|
61
|
+
});
|
|
62
|
+
if (isCancel(overrideTarget)) {
|
|
63
|
+
cancel('Cancelled');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await furnaceOverrideCommand(projectRoot, overrideTarget);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Persists discovered component tag names into the `stock` section of
|
|
70
|
+
* furnace.json. Shared by the interactive confirm flow and the
|
|
71
|
+
* non-interactive `--track` flag (0.34.0 field report: scan printed a full
|
|
72
|
+
* inventory but persisted nothing and said nothing about where the
|
|
73
|
+
* inventory goes).
|
|
74
|
+
*
|
|
75
|
+
* Wraps the furnace.json mutation in the standard furnace lifecycle so the
|
|
76
|
+
* write goes through the furnace-wide lock and is visible to the global
|
|
77
|
+
* SIGINT/SIGTERM rollback pathway. The journal snapshots furnace.json
|
|
78
|
+
* *before* `ensureFurnaceConfig` runs, so a failed run after the file is
|
|
79
|
+
* auto-created cleans up after itself instead of leaving an unwanted
|
|
80
|
+
* default config behind.
|
|
81
|
+
*/
|
|
82
|
+
async function persistStockComponents(projectRoot, names) {
|
|
54
83
|
await runFurnaceMutation(projectRoot, 'scan-rollback', async (ctx) => {
|
|
55
84
|
const journal = createRollbackJournal();
|
|
56
85
|
ctx.registerJournal(journal);
|
|
@@ -58,15 +87,14 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
58
87
|
await snapshotFile(journal, furnacePaths.furnaceConfig);
|
|
59
88
|
try {
|
|
60
89
|
const config = await ensureFurnaceConfig(projectRoot);
|
|
61
|
-
// Defensive:
|
|
62
|
-
// currently in config.stock (see untrackedComponents above). This
|
|
90
|
+
// Defensive: callers already filter to untracked components. This
|
|
63
91
|
// re-filter catches the edge case where the config on disk changed
|
|
64
92
|
// between the scan's read and the write (concurrent scan / manual
|
|
65
93
|
// edit). Without it a duplicate scan would introduce duplicate
|
|
66
94
|
// entries into stock; writeFurnaceConfig's validator would then
|
|
67
95
|
// reject the write, but the error would be less actionable than
|
|
68
96
|
// silently de-duplicating here.
|
|
69
|
-
const toAdd =
|
|
97
|
+
const toAdd = names.filter((s) => !config.stock.includes(s));
|
|
70
98
|
config.stock.push(...toAdd);
|
|
71
99
|
await writeFurnaceConfig(projectRoot, config);
|
|
72
100
|
}
|
|
@@ -81,24 +109,6 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
81
109
|
throw error;
|
|
82
110
|
}
|
|
83
111
|
});
|
|
84
|
-
const addedNames = selected;
|
|
85
|
-
success(`Added ${addedNames.length} component${addedNames.length === 1 ? '' : 's'} to furnace.json`);
|
|
86
|
-
// Offer to immediately override one of the just-added stock components.
|
|
87
|
-
const shouldOverride = await confirm({
|
|
88
|
-
message: 'Override any of the newly added components?',
|
|
89
|
-
});
|
|
90
|
-
if (isCancel(shouldOverride) || !shouldOverride) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const overrideTarget = await select({
|
|
94
|
-
message: 'Select a component to override',
|
|
95
|
-
options: addedNames.map((name) => ({ value: name, label: name })),
|
|
96
|
-
});
|
|
97
|
-
if (isCancel(overrideTarget)) {
|
|
98
|
-
cancel('Cancelled');
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
await furnaceOverrideCommand(projectRoot, overrideTarget);
|
|
102
112
|
}
|
|
103
113
|
/**
|
|
104
114
|
* Runs the furnace scan command to discover MozLitElement components.
|
|
@@ -171,11 +181,31 @@ export async function furnaceScanCommand(projectRoot, options = {}) {
|
|
|
171
181
|
}
|
|
172
182
|
const untrackedCount = components.length - trackedCount;
|
|
173
183
|
note(`Total: ${components.length} Tracked: ${trackedCount} Untracked: ${untrackedCount}`, 'Summary');
|
|
184
|
+
// --track: persist the discovered untracked inventory into the `stock`
|
|
185
|
+
// section without prompting (works non-interactively). Without it, scan
|
|
186
|
+
// stays report-only; the interactive confirm flow below is the other
|
|
187
|
+
// persistence path.
|
|
188
|
+
if (options.track) {
|
|
189
|
+
if (untrackedCount === 0) {
|
|
190
|
+
info('Nothing to track: every discovered component is already in furnace.json.');
|
|
191
|
+
outro('Scan complete');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const untrackedNames = components.filter((c) => !tracked.has(c.tagName)).map((c) => c.tagName);
|
|
195
|
+
await persistStockComponents(projectRoot, untrackedNames);
|
|
196
|
+
success(`Tracked ${untrackedNames.length} component${untrackedNames.length === 1 ? '' : 's'} in the stock section of furnace.json`);
|
|
197
|
+
outro('Scan complete');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
174
200
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
175
201
|
if (isInteractive && untrackedCount > 0) {
|
|
176
202
|
await promptAddComponents(components, tracked, projectRoot);
|
|
177
203
|
return;
|
|
178
204
|
}
|
|
205
|
+
if (untrackedCount > 0) {
|
|
206
|
+
info('Scan is report-only: re-run with --track to persist the untracked components into the ' +
|
|
207
|
+
'stock section of furnace.json (deploy/validate consume them from there).');
|
|
208
|
+
}
|
|
179
209
|
outro('Scan complete');
|
|
180
210
|
}
|
|
181
211
|
//# sourceMappingURL=scan.js.map
|
|
@@ -3,7 +3,7 @@ import { readdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getProjectPaths } from '../../core/config.js';
|
|
5
5
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
|
-
import { addCustomElementRegistration, addJarMnEntries } from '../../core/furnace-registration.js';
|
|
6
|
+
import { addCustomElementRegistration, addJarMnEntries, pruneStaleJarMnEntries, } from '../../core/furnace-registration.js';
|
|
7
7
|
import { validateAllComponents, validateComponent } from '../../core/furnace-validate.js';
|
|
8
8
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
9
9
|
import { pathExists } from '../../utils/fs.js';
|
|
@@ -14,6 +14,7 @@ const FIXABLE_CHECKS = new Set([
|
|
|
14
14
|
'missing-jar-mn-mjs',
|
|
15
15
|
'missing-jar-mn-css',
|
|
16
16
|
'wrong-registration-pattern',
|
|
17
|
+
'stale-jar-registration',
|
|
17
18
|
]);
|
|
18
19
|
/**
|
|
19
20
|
* Auto-fixes registration issues that have deterministic solutions.
|
|
@@ -40,6 +41,21 @@ async function autoFixIssues(projectRoot, issues) {
|
|
|
40
41
|
jarMnFixesByComponent.set(issue.component, existing);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
// Prune stale jar.mn registrations (lines pointing at removed/renamed
|
|
45
|
+
// component files — 0.34.0 field report: these broke packaging while
|
|
46
|
+
// validate --fix reported success without touching them).
|
|
47
|
+
if (issues.some((issue) => issue.check === 'stale-jar-registration')) {
|
|
48
|
+
try {
|
|
49
|
+
const pruned = await pruneStaleJarMnEntries(engineDir, furnacePaths.customDir, Object.keys(config.custom));
|
|
50
|
+
fixed += pruned.length;
|
|
51
|
+
for (const entry of pruned) {
|
|
52
|
+
info(`Fixed: pruned stale jar.mn line for ${entry.tagName}/${entry.fileName}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
warn(`Could not prune stale jar.mn lines: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
43
59
|
// Fix jar.mn entries
|
|
44
60
|
for (const [componentName, files] of jarMnFixesByComponent) {
|
|
45
61
|
try {
|