@hominis/fireforge 0.16.2 → 0.16.5
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 +93 -1
- package/README.md +15 -2
- package/dist/bin/fireforge.js +11 -2
- package/dist/src/commands/doctor-furnace.js +83 -1
- package/dist/src/commands/doctor.js +18 -0
- package/dist/src/commands/download.js +58 -12
- package/dist/src/commands/export-all.js +19 -2
- package/dist/src/commands/export-shared.d.ts +36 -0
- package/dist/src/commands/export-shared.js +76 -0
- package/dist/src/commands/export.js +23 -2
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +21 -3
- package/dist/src/commands/furnace/chrome-doc-templates.js +23 -5
- package/dist/src/commands/furnace/chrome-doc-tests.js +42 -17
- package/dist/src/commands/furnace/create-readback.d.ts +23 -0
- package/dist/src/commands/furnace/create-readback.js +34 -0
- package/dist/src/commands/furnace/create-templates.d.ts +17 -7
- package/dist/src/commands/furnace/create-templates.js +85 -31
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +1 -1
- package/dist/src/commands/furnace/create.js +2 -0
- package/dist/src/commands/furnace/preview.d.ts +12 -0
- package/dist/src/commands/furnace/preview.js +34 -2
- package/dist/src/commands/furnace/status.js +1 -1
- package/dist/src/commands/import.js +63 -11
- package/dist/src/commands/patch/delete.js +10 -1
- package/dist/src/commands/patch/index.js +10 -1
- package/dist/src/commands/re-export.js +79 -6
- package/dist/src/commands/resolve.js +15 -1
- package/dist/src/commands/run.js +27 -5
- package/dist/src/commands/setup-support.js +60 -7
- package/dist/src/commands/status.js +28 -1
- package/dist/src/commands/test.js +28 -5
- package/dist/src/commands/token-coverage.js +55 -1
- package/dist/src/commands/token.js +19 -2
- package/dist/src/commands/wire.js +22 -2
- package/dist/src/core/branding.d.ts +10 -0
- package/dist/src/core/branding.js +7 -9
- package/dist/src/core/build-prepare.js +8 -1
- package/dist/src/core/file-lock.js +49 -15
- package/dist/src/core/furnace-operation.d.ts +17 -0
- package/dist/src/core/furnace-operation.js +30 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +33 -1
- package/dist/src/core/furnace-validate-helpers.js +53 -2
- package/dist/src/core/git.js +39 -10
- package/dist/src/core/mach-error-hints.js +16 -0
- package/dist/src/core/mach.js +15 -6
- package/dist/src/core/manifest-rules.js +16 -0
- package/dist/src/core/marionette-preflight.js +43 -12
- package/dist/src/core/patch-files.d.ts +12 -1
- package/dist/src/core/patch-files.js +14 -11
- package/dist/src/core/patch-lint.js +62 -11
- package/dist/src/core/wire-destroy.js +18 -5
- package/dist/src/core/wire-init.js +20 -5
- package/dist/src/core/wire-utils.d.ts +15 -0
- package/dist/src/core/wire-utils.js +17 -0
- package/dist/src/types/commands/options.d.ts +7 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { confirm, select, text } from '@clack/prompts';
|
|
|
4
4
|
import { addLicenseHeaderToFile, getLicenseHeader } from '../core/license-headers.js';
|
|
5
5
|
import { findAllPatchesForFiles } from '../core/patch-export.js';
|
|
6
6
|
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, } from '../core/patch-lint.js';
|
|
7
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
7
8
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
8
9
|
import { pathExists, readText } from '../utils/fs.js';
|
|
9
10
|
import { cancel, info, isCancel, warn } from '../utils/logger.js';
|
|
@@ -222,4 +223,79 @@ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isIn
|
|
|
222
223
|
}
|
|
223
224
|
return true;
|
|
224
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Maps every file in `filesAffected` to the existing patches that already
|
|
228
|
+
* claim ownership of it, excluding the caller's own patch (when `newFilename`
|
|
229
|
+
* is provided) and any patches that the caller intends to fully supersede.
|
|
230
|
+
*
|
|
231
|
+
* Returns an empty map when no overlap exists. Used by the overlap gate in
|
|
232
|
+
* `export` and `export-all` to refuse a default-mode export that would
|
|
233
|
+
* silently create cross-patch ownership conflicts — the same class of
|
|
234
|
+
* conflict `verify` immediately fails with.
|
|
235
|
+
*/
|
|
236
|
+
export function findPartialOwnershipOverlap(manifest, filesAffected, excludeFilenames) {
|
|
237
|
+
const overlap = new Map();
|
|
238
|
+
const targetSet = new Set(filesAffected);
|
|
239
|
+
for (const patch of manifest.patches) {
|
|
240
|
+
if (excludeFilenames.has(patch.filename))
|
|
241
|
+
continue;
|
|
242
|
+
for (const file of patch.filesAffected) {
|
|
243
|
+
if (!targetSet.has(file))
|
|
244
|
+
continue;
|
|
245
|
+
const owners = overlap.get(file) ?? [];
|
|
246
|
+
owners.push(patch.filename);
|
|
247
|
+
overlap.set(file, owners);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return overlap;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Gate that refuses the default export path when the new patch would
|
|
254
|
+
* silently claim files that are already tracked by other non-superseded
|
|
255
|
+
* patches. `findAllPatchesForFiles` already catches the full-coverage
|
|
256
|
+
* supersede case — this helper fills the gap for partial overlap, which
|
|
257
|
+
* was the eval finding #12 scenario (two patches both claiming
|
|
258
|
+
* `browser/themes/shared/jar.inc.mn` after a second export with
|
|
259
|
+
* `--before`).
|
|
260
|
+
*
|
|
261
|
+
* Proceeds silently when there is no overlap, or when the caller passed
|
|
262
|
+
* `--allow-overlap`. In interactive mode the caller is prompted to
|
|
263
|
+
* acknowledge the overlap (the proper fix path is `re-export --files` to
|
|
264
|
+
* repartition ownership, so the prompt surfaces that pointer). In
|
|
265
|
+
* non-interactive mode the function throws — better to fail fast than
|
|
266
|
+
* let the queue fall out of sync with verify.
|
|
267
|
+
*/
|
|
268
|
+
export async function guardOwnershipOverlap(args) {
|
|
269
|
+
const { patchesDir, filesAffected, supersedingFilenames, allowOverlap, isInteractive, s } = args;
|
|
270
|
+
if (allowOverlap)
|
|
271
|
+
return true;
|
|
272
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
273
|
+
if (!manifest)
|
|
274
|
+
return true;
|
|
275
|
+
const overlap = findPartialOwnershipOverlap(manifest, filesAffected, supersedingFilenames);
|
|
276
|
+
if (overlap.size === 0)
|
|
277
|
+
return true;
|
|
278
|
+
s.stop();
|
|
279
|
+
const entries = [...overlap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
280
|
+
warn(`This export would create cross-patch ownership overlap on ${String(entries.length)} file${entries.length === 1 ? '' : 's'}:`);
|
|
281
|
+
for (const [file, owners] of entries) {
|
|
282
|
+
warn(` - ${file} already claimed by: ${owners.join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
warn('The queue would fail `fireforge verify` immediately after this export. ' +
|
|
285
|
+
'To repartition ownership safely, run `fireforge re-export --files <paths> <existing-patch>` ' +
|
|
286
|
+
'on the overlapping patches first, then re-run the export.');
|
|
287
|
+
if (!isInteractive) {
|
|
288
|
+
throw new GeneralError('Refusing to export a queue with cross-patch ownership overlap in non-interactive mode. ' +
|
|
289
|
+
'Pass --allow-overlap to acknowledge the conflict, or repartition ownership via `fireforge re-export --files`.');
|
|
290
|
+
}
|
|
291
|
+
const confirmed = await confirm({
|
|
292
|
+
message: 'Proceed with overlapping ownership? This will leave the queue in a verify-failing state.',
|
|
293
|
+
initialValue: false,
|
|
294
|
+
});
|
|
295
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
296
|
+
cancel('Export cancelled');
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
225
301
|
//# sourceMappingURL=export-shared.js.map
|
|
@@ -10,7 +10,7 @@ import { generateBinaryFilePatch, generateFullFilePatch } from '../core/git-diff
|
|
|
10
10
|
import { isBinaryFile } from '../core/git-file-ops.js';
|
|
11
11
|
import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } from '../core/git-status.js';
|
|
12
12
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
13
|
-
import { commitExportedPatch } from '../core/patch-export.js';
|
|
13
|
+
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
14
14
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
15
15
|
import { toError } from '../utils/errors.js';
|
|
16
16
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
@@ -19,7 +19,7 @@ import { pickDefined } from '../utils/options.js';
|
|
|
19
19
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
20
20
|
import { parsePositiveIntegerFlag, PATCH_CATEGORIES } from '../utils/validation.js';
|
|
21
21
|
import { commitPlacementExport, placementSummary, projectPlacementForLint, renderDryRunPreview, resolvePlacementPlan, } from './export-flow.js';
|
|
22
|
-
import { autoFixLicenseHeaders, confirmSupersedePatches, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
|
|
22
|
+
import { autoFixLicenseHeaders, confirmSupersedePatches, guardOwnershipOverlap, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
|
|
23
23
|
async function collectExportFiles(paths, files) {
|
|
24
24
|
const collectedFiles = new Set();
|
|
25
25
|
let fileStatuses;
|
|
@@ -286,6 +286,26 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
286
286
|
const shouldProceed = await confirmSupersedePatches(paths.patches, filesAffected, options.supersede, isInteractive, s);
|
|
287
287
|
if (!shouldProceed)
|
|
288
288
|
return;
|
|
289
|
+
// Overlap gate: pre-0.16.0 `export` only caught FULL-coverage
|
|
290
|
+
// supersedes, so a second export targeting a shared file like
|
|
291
|
+
// `browser/themes/shared/jar.inc.mn` happily created a queue where
|
|
292
|
+
// two patches both listed the same file in `filesAffected`. `verify`
|
|
293
|
+
// then failed immediately on "cross-patch filesAffected conflicts".
|
|
294
|
+
// `confirmSupersedePatches` might already have confirmed full
|
|
295
|
+
// supersedes above; pass their filenames through so we do not flag
|
|
296
|
+
// a file claimed by a patch that is about to be removed.
|
|
297
|
+
const willSupersede = await findAllPatchesForFiles(paths.patches, filesAffected);
|
|
298
|
+
const supersedingFilenames = new Set(willSupersede.map((p) => p.filename));
|
|
299
|
+
const shouldProceedPastOverlap = await guardOwnershipOverlap({
|
|
300
|
+
patchesDir: paths.patches,
|
|
301
|
+
filesAffected,
|
|
302
|
+
supersedingFilenames,
|
|
303
|
+
allowOverlap: options.allowOverlap === true,
|
|
304
|
+
isInteractive,
|
|
305
|
+
s,
|
|
306
|
+
});
|
|
307
|
+
if (!shouldProceedPastOverlap)
|
|
308
|
+
return;
|
|
289
309
|
const { patchFilename, superseded } = await commitExportedPatch({
|
|
290
310
|
patchesDir: paths.patches,
|
|
291
311
|
category: selectedCategory,
|
|
@@ -327,6 +347,7 @@ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
|
|
|
327
347
|
.option('-y, --yes', 'Skip confirmation for placement renumbers (required for non-TTY)')
|
|
328
348
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal on projected placement')
|
|
329
349
|
.option('--exclude-furnace', 'Exclude furnace-managed file paths from the export')
|
|
350
|
+
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap (default mode only; the resulting queue fails verify)')
|
|
330
351
|
.action(withErrorHandling(async (paths, options) => {
|
|
331
352
|
const { category, ...rest } = options;
|
|
332
353
|
await exportCommand(getProjectRoot(), paths, {
|
|
@@ -56,11 +56,29 @@ export declare function generateChromeDocCss(name: string, withTitlebar: boolean
|
|
|
56
56
|
export declare function generateChromeDocFtl(name: string, licenseHeader: string): string;
|
|
57
57
|
/**
|
|
58
58
|
* Single-line jar.mn entry that registers an xhtml + js pair under
|
|
59
|
-
* `content/browser/`.
|
|
60
|
-
*
|
|
59
|
+
* `content/browser/`.
|
|
60
|
+
*
|
|
61
|
+
* Neither emitted line carries the `*` preprocessor flag. The scaffolded
|
|
62
|
+
* XHTML and JS contain no `#filter` / `#expand` / `#include` directives,
|
|
63
|
+
* and mach's `process_install_manifest.py` fails the whole package step
|
|
64
|
+
* with "no preprocessor directives found" when a preprocessed entry has
|
|
65
|
+
* nothing for the preprocessor to do. A fork that later needs brand
|
|
66
|
+
* substitution can re-introduce `*` and add a top-of-file
|
|
67
|
+
* `#filter substitution` directive itself.
|
|
61
68
|
*/
|
|
62
69
|
export declare function jarMnEntriesForChromeDoc(name: string): string[];
|
|
63
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* jar.inc.mn entry that registers the scoped CSS under `content/browser/`.
|
|
72
|
+
*
|
|
73
|
+
* The source path is `../shared/<name>-chrome.css` because `jar.inc.mn`
|
|
74
|
+
* is included from each theme-specific manifest (`browser/themes/osx/jar.mn`,
|
|
75
|
+
* `browser/themes/linux/jar.mn`, `browser/themes/windows/jar.mn`), and every
|
|
76
|
+
* existing entry in those manifests resolves paths relative to the including
|
|
77
|
+
* manifest's directory. A bare `(shared/…)` path produced
|
|
78
|
+
* `obj-.../browser/themes/osx/shared/<name>-chrome.css` which does not exist;
|
|
79
|
+
* `(../shared/…)` matches the upstream pattern and resolves under
|
|
80
|
+
* `browser/themes/shared/`.
|
|
81
|
+
*/
|
|
64
82
|
export declare function jarIncMnEntryForChromeDoc(name: string): string;
|
|
65
83
|
/**
|
|
66
84
|
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
@@ -149,18 +149,36 @@ ${name}-window-title = ${name}
|
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
151
|
* Single-line jar.mn entry that registers an xhtml + js pair under
|
|
152
|
-
* `content/browser/`.
|
|
153
|
-
*
|
|
152
|
+
* `content/browser/`.
|
|
153
|
+
*
|
|
154
|
+
* Neither emitted line carries the `*` preprocessor flag. The scaffolded
|
|
155
|
+
* XHTML and JS contain no `#filter` / `#expand` / `#include` directives,
|
|
156
|
+
* and mach's `process_install_manifest.py` fails the whole package step
|
|
157
|
+
* with "no preprocessor directives found" when a preprocessed entry has
|
|
158
|
+
* nothing for the preprocessor to do. A fork that later needs brand
|
|
159
|
+
* substitution can re-introduce `*` and add a top-of-file
|
|
160
|
+
* `#filter substitution` directive itself.
|
|
154
161
|
*/
|
|
155
162
|
export function jarMnEntriesForChromeDoc(name) {
|
|
156
163
|
return [
|
|
157
|
-
|
|
164
|
+
` content/browser/${name}.xhtml (content/${name}.xhtml)`,
|
|
158
165
|
` content/browser/${name}.js (content/${name}.js)`,
|
|
159
166
|
];
|
|
160
167
|
}
|
|
161
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* jar.inc.mn entry that registers the scoped CSS under `content/browser/`.
|
|
170
|
+
*
|
|
171
|
+
* The source path is `../shared/<name>-chrome.css` because `jar.inc.mn`
|
|
172
|
+
* is included from each theme-specific manifest (`browser/themes/osx/jar.mn`,
|
|
173
|
+
* `browser/themes/linux/jar.mn`, `browser/themes/windows/jar.mn`), and every
|
|
174
|
+
* existing entry in those manifests resolves paths relative to the including
|
|
175
|
+
* manifest's directory. A bare `(shared/…)` path produced
|
|
176
|
+
* `obj-.../browser/themes/osx/shared/<name>-chrome.css` which does not exist;
|
|
177
|
+
* `(../shared/…)` matches the upstream pattern and resolves under
|
|
178
|
+
* `browser/themes/shared/`.
|
|
179
|
+
*/
|
|
162
180
|
export function jarIncMnEntryForChromeDoc(name) {
|
|
163
|
-
return ` content/browser/${name}-chrome.css (shared/${name}-chrome.css)`;
|
|
181
|
+
return ` content/browser/${name}-chrome.css (../shared/${name}-chrome.css)`;
|
|
164
182
|
}
|
|
165
183
|
/**
|
|
166
184
|
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
@@ -69,32 +69,57 @@ export function generateChromeDocPackagingTest(name, header) {
|
|
|
69
69
|
add_task(async function test_${taskSuffix}_files_packaged() {
|
|
70
70
|
const appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// Probes a pair of candidate layouts for the same packaged file:
|
|
73
|
+
// 1) \`<AppDir>/chrome/browser/…\` — the unpacked layout when
|
|
74
|
+
// XCurProcD honours \`firefox-appdir = "browser"\` and resolves
|
|
75
|
+
// into \`dist/bin/browser/\`.
|
|
76
|
+
// 2) \`<AppDir>/browser/chrome/browser/…\` — the macOS .app bundle
|
|
77
|
+
// layout and some ESR configurations, where XCurProcD sits one
|
|
78
|
+
// level above \`browser/\` even when the appdir directive is set.
|
|
79
|
+
// If either path exists the file is packaged; the assertion only fails
|
|
80
|
+
// when BOTH layouts miss, which is the actual stale-build / missing
|
|
81
|
+
// jar.mn entry case. Before this dual probe, the eval on macOS
|
|
82
|
+
// consistently failed against layout (2) even though the file was
|
|
83
|
+
// packaged correctly.
|
|
84
|
+
function probeEither(primary, fallback, description) {
|
|
85
|
+
const primaryFile = appDir.clone();
|
|
86
|
+
for (const segment of primary) {
|
|
87
|
+
primaryFile.append(segment);
|
|
76
88
|
}
|
|
89
|
+
const fallbackFile = appDir.clone();
|
|
90
|
+
for (const segment of fallback) {
|
|
91
|
+
fallbackFile.append(segment);
|
|
92
|
+
}
|
|
93
|
+
const found = primaryFile.exists() ? primaryFile : fallbackFile.exists() ? fallbackFile : null;
|
|
77
94
|
Assert.ok(
|
|
78
|
-
|
|
79
|
-
description +
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
file.fileSize,
|
|
86
|
-
0,
|
|
87
|
-
description + " is zero-length at " + file.path +
|
|
88
|
-
" — packaging copied an empty file, check the source template.",
|
|
95
|
+
found !== null,
|
|
96
|
+
description +
|
|
97
|
+
" missing at both " +
|
|
98
|
+
primaryFile.path +
|
|
99
|
+
" and " +
|
|
100
|
+
fallbackFile.path +
|
|
101
|
+
' — run "fireforge build --ui" and retry. If one of those paths IS populated, the xpcshell harness is probing a stale build tree; the post-build audit should flag the same miss.',
|
|
89
102
|
);
|
|
103
|
+
if (found !== null) {
|
|
104
|
+
Assert.greater(
|
|
105
|
+
found.fileSize,
|
|
106
|
+
0,
|
|
107
|
+
description +
|
|
108
|
+
" is zero-length at " +
|
|
109
|
+
found.path +
|
|
110
|
+
" — packaging copied an empty file, check the source template.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
probeEither(
|
|
93
116
|
["chrome", "browser", "content", "browser", "${name}.xhtml"],
|
|
117
|
+
["browser", "chrome", "browser", "content", "browser", "${name}.xhtml"],
|
|
94
118
|
"${name}.xhtml",
|
|
95
119
|
);
|
|
96
|
-
|
|
120
|
+
probeEither(
|
|
97
121
|
["chrome", "browser", "skin", "classic", "browser", "${name}-chrome.css"],
|
|
122
|
+
["browser", "chrome", "browser", "skin", "classic", "browser", "${name}-chrome.css"],
|
|
98
123
|
"${name}-chrome.css",
|
|
99
124
|
);
|
|
100
125
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive read-back helper for `furnace create`. Extracted from
|
|
3
|
+
* `create.ts` so the authoring command stays under the per-file LOC
|
|
4
|
+
* budget.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Asserts that the just-written furnace.json contains the expected
|
|
8
|
+
* custom component entry. The eval run's finding #9 observed a
|
|
9
|
+
* scenario where `furnace create --allow-prefix-mismatch` reported
|
|
10
|
+
* success and wrote the component files, but the subsequent
|
|
11
|
+
* `furnace status` found `custom: {}` in furnace.json — an invariant
|
|
12
|
+
* violation with no clear smoking gun in the code path. Local repros
|
|
13
|
+
* do not trigger it, so the defensive readback is the safest recovery
|
|
14
|
+
* contract we can offer: if the new entry is not visible on the next
|
|
15
|
+
* load, throw a `FurnaceError` so the rollback journal restores the
|
|
16
|
+
* pre-command state and the operator sees the failure instead of a
|
|
17
|
+
* phantom success.
|
|
18
|
+
*
|
|
19
|
+
* @param projectRoot - Root of the FireForge project
|
|
20
|
+
* @param componentName - Custom-element tag name that must be present
|
|
21
|
+
* in `config.custom` after the write. Throws when absent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertCustomEntryPersisted(projectRoot: string, componentName: string): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Defensive read-back helper for `furnace create`. Extracted from
|
|
4
|
+
* `create.ts` so the authoring command stays under the per-file LOC
|
|
5
|
+
* budget.
|
|
6
|
+
*/
|
|
7
|
+
import { loadFurnaceConfig } from '../../core/furnace-config.js';
|
|
8
|
+
import { FurnaceError } from '../../errors/furnace.js';
|
|
9
|
+
/**
|
|
10
|
+
* Asserts that the just-written furnace.json contains the expected
|
|
11
|
+
* custom component entry. The eval run's finding #9 observed a
|
|
12
|
+
* scenario where `furnace create --allow-prefix-mismatch` reported
|
|
13
|
+
* success and wrote the component files, but the subsequent
|
|
14
|
+
* `furnace status` found `custom: {}` in furnace.json — an invariant
|
|
15
|
+
* violation with no clear smoking gun in the code path. Local repros
|
|
16
|
+
* do not trigger it, so the defensive readback is the safest recovery
|
|
17
|
+
* contract we can offer: if the new entry is not visible on the next
|
|
18
|
+
* load, throw a `FurnaceError` so the rollback journal restores the
|
|
19
|
+
* pre-command state and the operator sees the failure instead of a
|
|
20
|
+
* phantom success.
|
|
21
|
+
*
|
|
22
|
+
* @param projectRoot - Root of the FireForge project
|
|
23
|
+
* @param componentName - Custom-element tag name that must be present
|
|
24
|
+
* in `config.custom` after the write. Throws when absent.
|
|
25
|
+
*/
|
|
26
|
+
export async function assertCustomEntryPersisted(projectRoot, componentName) {
|
|
27
|
+
const persisted = await loadFurnaceConfig(projectRoot);
|
|
28
|
+
if (!(componentName in persisted.custom)) {
|
|
29
|
+
throw new FurnaceError(`Wrote furnace.json but "${componentName}" is missing from config.custom on read-back. ` +
|
|
30
|
+
'This should not happen — please report the issue. As a workaround, ' +
|
|
31
|
+
're-run the command, or add the entry to furnace.json by hand.', componentName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=create-readback.js.map
|
|
@@ -33,13 +33,23 @@ export declare function xpcshellTestFileName(name: string): string;
|
|
|
33
33
|
/**
|
|
34
34
|
* Generates an xpcshell test file for a custom component.
|
|
35
35
|
*
|
|
36
|
-
* xpcshell
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
36
|
+
* xpcshell cannot execute a component module that imports
|
|
37
|
+
* `chrome://global/content/vendor/lit.all.mjs` — the Lit bundle touches
|
|
38
|
+
* `window` at module-load time and the xpcshell harness has no `window`
|
|
39
|
+
* global. Before 0.16.0 the scaffold called `ChromeUtils.importESModule`
|
|
40
|
+
* on the component's MJS, which reliably failed with
|
|
41
|
+
* `ReferenceError: window is not defined` for every Lit-based fork
|
|
42
|
+
* component. FireForge's diagnostics then misrouted the failure to the
|
|
43
|
+
* "stale build artifacts" branch, sending operators on a rebuild loop
|
|
44
|
+
* that couldn't fix a runtime-environment incompatibility.
|
|
45
|
+
*
|
|
46
|
+
* The rewrite here mirrors the chrome-doc packaging test: XCurProcD is
|
|
47
|
+
* probed at a pair of candidate layouts (dist/bin/browser and the macOS
|
|
48
|
+
* .app-bundle / ESR layout) to confirm the `.mjs` and `.css` files
|
|
49
|
+
* landed where jar.mn promised. That's the assertion xpcshell CAN make.
|
|
50
|
+
* Functional tests that need DOM/shadow-root/keyboard behaviour belong
|
|
51
|
+
* in a browser-chrome mochitest — scaffolded via
|
|
52
|
+
* `fireforge furnace create --test-style browser-chrome`.
|
|
43
53
|
*/
|
|
44
54
|
export declare function generateXpcshellTestContent(name: string, header: string): string;
|
|
45
55
|
/**
|
|
@@ -93,48 +93,102 @@ export function generateFtlContent(name, header) {
|
|
|
93
93
|
}
|
|
94
94
|
/** Returns the canonical xpcshell test file basename for a component. */
|
|
95
95
|
export function xpcshellTestFileName(name) {
|
|
96
|
-
return `test_${name.replace(/-/g, '_')}
|
|
96
|
+
return `test_${name.replace(/-/g, '_')}_packaged.js`;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* Generates an xpcshell test file for a custom component.
|
|
100
100
|
*
|
|
101
|
-
* xpcshell
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
101
|
+
* xpcshell cannot execute a component module that imports
|
|
102
|
+
* `chrome://global/content/vendor/lit.all.mjs` — the Lit bundle touches
|
|
103
|
+
* `window` at module-load time and the xpcshell harness has no `window`
|
|
104
|
+
* global. Before 0.16.0 the scaffold called `ChromeUtils.importESModule`
|
|
105
|
+
* on the component's MJS, which reliably failed with
|
|
106
|
+
* `ReferenceError: window is not defined` for every Lit-based fork
|
|
107
|
+
* component. FireForge's diagnostics then misrouted the failure to the
|
|
108
|
+
* "stale build artifacts" branch, sending operators on a rebuild loop
|
|
109
|
+
* that couldn't fix a runtime-environment incompatibility.
|
|
110
|
+
*
|
|
111
|
+
* The rewrite here mirrors the chrome-doc packaging test: XCurProcD is
|
|
112
|
+
* probed at a pair of candidate layouts (dist/bin/browser and the macOS
|
|
113
|
+
* .app-bundle / ESR layout) to confirm the `.mjs` and `.css` files
|
|
114
|
+
* landed where jar.mn promised. That's the assertion xpcshell CAN make.
|
|
115
|
+
* Functional tests that need DOM/shadow-root/keyboard behaviour belong
|
|
116
|
+
* in a browser-chrome mochitest — scaffolded via
|
|
117
|
+
* `fireforge furnace create --test-style browser-chrome`.
|
|
108
118
|
*/
|
|
109
119
|
export function generateXpcshellTestContent(name, header) {
|
|
120
|
+
const taskSuffix = name.replace(/-/g, '_');
|
|
110
121
|
return `${header}
|
|
111
122
|
|
|
112
123
|
"use strict";
|
|
113
124
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
125
|
+
// Packaging verification for the "${name}" custom component.
|
|
126
|
+
//
|
|
127
|
+
// Why this is not a module-load test:
|
|
128
|
+
// ChromeUtils.importESModule("chrome://global/content/elements/${name}.mjs")
|
|
129
|
+
// pulls in \`chrome://global/content/vendor/lit.all.mjs\`, which
|
|
130
|
+
// references \`window\` during its module body — there is no \`window\`
|
|
131
|
+
// global in xpcshell, so every attempt throws
|
|
132
|
+
// \`ReferenceError: window is not defined\`. For Lit-based components,
|
|
133
|
+
// xpcshell can only verify that the files reached the packaged tree;
|
|
134
|
+
// functional UI assertions belong in a browser-chrome mochitest
|
|
135
|
+
// (see \`fireforge furnace create --test-style browser-chrome\`).
|
|
117
136
|
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
// Out of scope: builds that pack omni.ja (MOZ_CHROME_MULTILOCALE, some
|
|
138
|
+
// release configs). The probe assumes an unpacked tree, which is what
|
|
139
|
+
// \`mach build\` produces by default. A packed build would need to unzip
|
|
140
|
+
// omni.ja to verify the same files.
|
|
141
|
+
|
|
142
|
+
add_task(async function test_${taskSuffix}_files_packaged() {
|
|
143
|
+
const appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
|
144
|
+
|
|
145
|
+
// Two candidate layouts are probed per asset:
|
|
146
|
+
// 1) \`<AppDir>/chrome/global/elements/…\` — unpacked layout when
|
|
147
|
+
// XCurProcD honours \`firefox-appdir = "browser"\` and resolves
|
|
148
|
+
// into \`dist/bin/browser/\`.
|
|
149
|
+
// 2) \`<AppDir>/browser/chrome/global/elements/…\` — macOS .app
|
|
150
|
+
// bundle and some ESR layouts where XCurProcD sits one level
|
|
151
|
+
// above \`browser/\`.
|
|
152
|
+
function probeEither(primary, fallback, description) {
|
|
153
|
+
const primaryFile = appDir.clone();
|
|
154
|
+
for (const segment of primary) {
|
|
155
|
+
primaryFile.append(segment);
|
|
156
|
+
}
|
|
157
|
+
const fallbackFile = appDir.clone();
|
|
158
|
+
for (const segment of fallback) {
|
|
159
|
+
fallbackFile.append(segment);
|
|
160
|
+
}
|
|
161
|
+
const found = primaryFile.exists() ? primaryFile : fallbackFile.exists() ? fallbackFile : null;
|
|
162
|
+
Assert.ok(
|
|
163
|
+
found !== null,
|
|
164
|
+
description +
|
|
165
|
+
" missing at both " +
|
|
166
|
+
primaryFile.path +
|
|
167
|
+
" and " +
|
|
168
|
+
fallbackFile.path +
|
|
169
|
+
' — run "fireforge build --ui" and retry. If the file IS present at one of those paths, xpcshell is probing a stale build tree.',
|
|
170
|
+
);
|
|
171
|
+
if (found !== null) {
|
|
172
|
+
Assert.greater(
|
|
173
|
+
found.fileSize,
|
|
174
|
+
0,
|
|
175
|
+
description +
|
|
176
|
+
" is zero-length at " +
|
|
177
|
+
found.path +
|
|
178
|
+
" — packaging copied an empty file, check the source template.",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
probeEither(
|
|
184
|
+
["chrome", "global", "elements", "${name}.mjs"],
|
|
185
|
+
["browser", "chrome", "global", "elements", "${name}.mjs"],
|
|
186
|
+
"${name}.mjs",
|
|
187
|
+
);
|
|
188
|
+
probeEither(
|
|
189
|
+
["chrome", "global", "elements", "${name}.css"],
|
|
190
|
+
["browser", "chrome", "global", "elements", "${name}.css"],
|
|
191
|
+
"${name}.css",
|
|
138
192
|
);
|
|
139
193
|
});
|
|
140
194
|
`;
|
|
@@ -13,7 +13,7 @@ import type { ProjectLicense } from '../../types/config.js';
|
|
|
13
13
|
* chrome mochitests require tabbrowser; xpcshell does not, so storage,
|
|
14
14
|
* observers, and ESM-loading logic can be covered headless.
|
|
15
15
|
*
|
|
16
|
-
* Writes `test_<name>
|
|
16
|
+
* Writes `test_<name>_packaged.js` and an `xpcshell.toml` manifest
|
|
17
17
|
* into `engine/browser/base/content/test/<binary-name>-xpcshell/
|
|
18
18
|
* <component-name>/`. moz.build registration is intentionally left to the
|
|
19
19
|
* operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
|
|
@@ -18,7 +18,7 @@ import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellT
|
|
|
18
18
|
* chrome mochitests require tabbrowser; xpcshell does not, so storage,
|
|
19
19
|
* observers, and ESM-loading logic can be covered headless.
|
|
20
20
|
*
|
|
21
|
-
* Writes `test_<name>
|
|
21
|
+
* Writes `test_<name>_packaged.js` and an `xpcshell.toml` manifest
|
|
22
22
|
* into `engine/browser/base/content/test/<binary-name>-xpcshell/
|
|
23
23
|
* <component-name>/`. moz.build registration is intentionally left to the
|
|
24
24
|
* operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
|
|
@@ -19,6 +19,7 @@ import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils
|
|
|
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';
|
|
22
|
+
import { assertCustomEntryPersisted } from './create-readback.js';
|
|
22
23
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
23
24
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
24
25
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
@@ -266,6 +267,7 @@ async function performCreateMutations(args) {
|
|
|
266
267
|
args.config.custom[args.componentName] = customEntry;
|
|
267
268
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
268
269
|
await writeFurnaceConfig(args.projectRoot, args.config);
|
|
270
|
+
await assertCustomEntryPersisted(args.projectRoot, args.componentName);
|
|
269
271
|
if (args.testStyle === 'browser-chrome') {
|
|
270
272
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
271
273
|
testFiles.push(...scafFiles);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { FurnacePreviewOptions } from '../../types/commands/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a targeted Storybook failure message from captured mach output.
|
|
4
|
+
*
|
|
5
|
+
* Exported for the test suite: the heuristic has three branches (backend
|
|
6
|
+
* artifact missing, Storybook dep missing, generic) and regression
|
|
7
|
+
* testing each is easier when the classifier is addressable directly.
|
|
8
|
+
*
|
|
9
|
+
* @param output - Combined stdout and stderr from the Storybook command
|
|
10
|
+
* @param installRequested - Whether the caller requested a dependency reinstall first
|
|
11
|
+
* @returns User-facing guidance for the specific failure mode
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildStorybookFailureMessage(output: string, installRequested: boolean): string;
|
|
2
14
|
/**
|
|
3
15
|
* Runs the furnace preview command to start Storybook for component preview.
|
|
4
16
|
* @param projectRoot - Root directory of the project
|
|
@@ -72,17 +72,49 @@ function reportPreviewStagingFailures(stageResult) {
|
|
|
72
72
|
const totalFailures = stageResult.errors.length + appliedWithStepErrorsCount;
|
|
73
73
|
throw new FurnaceError(`${totalFailures} component${totalFailures === 1 ? '' : 's'} failed to stage for preview`);
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Filenames emitted by the Firefox build backend (not by Storybook's npm
|
|
77
|
+
* package set) — their absence means `mach build` has not produced its
|
|
78
|
+
* post-configure artifacts, which is a different failure mode from a
|
|
79
|
+
* missing Storybook workspace dependency tree. The eval log for finding
|
|
80
|
+
* #11 reported `FileNotFoundError: [...] chrome-map.json` *after* a
|
|
81
|
+
* successful Storybook `npm install`, and the pre-0.16 heuristic
|
|
82
|
+
* misdiagnosed it as a dep failure and sent the operator back to
|
|
83
|
+
* `--install`. Pattern list is narrow on purpose so we only surface the
|
|
84
|
+
* backend-rebuild hint when we are confident.
|
|
85
|
+
*/
|
|
86
|
+
const BACKEND_ARTIFACT_PATTERNS = [
|
|
87
|
+
/chrome-map\.json/i,
|
|
88
|
+
/config\.status/i,
|
|
89
|
+
/obj-[^\s/]+\/dist\/bin\/\.lldbinit/i,
|
|
90
|
+
];
|
|
75
91
|
/**
|
|
76
92
|
* Builds a targeted Storybook failure message from captured mach output.
|
|
93
|
+
*
|
|
94
|
+
* Exported for the test suite: the heuristic has three branches (backend
|
|
95
|
+
* artifact missing, Storybook dep missing, generic) and regression
|
|
96
|
+
* testing each is easier when the classifier is addressable directly.
|
|
97
|
+
*
|
|
77
98
|
* @param output - Combined stdout and stderr from the Storybook command
|
|
78
99
|
* @param installRequested - Whether the caller requested a dependency reinstall first
|
|
79
100
|
* @returns User-facing guidance for the specific failure mode
|
|
80
101
|
*/
|
|
81
|
-
function buildStorybookFailureMessage(output, installRequested) {
|
|
102
|
+
export function buildStorybookFailureMessage(output, installRequested) {
|
|
82
103
|
const installHint = installRequested
|
|
83
104
|
? 'Try running "python3 ./mach storybook upgrade" manually in the engine directory.'
|
|
84
105
|
: 'Run "fireforge furnace preview --install" to bootstrap Storybook dependencies, or run "python3 ./mach storybook upgrade" manually in engine/.';
|
|
85
|
-
|
|
106
|
+
const hasFileNotFoundSignal = /(ENOENT|No such file or directory|FileNotFoundError)/i.test(output);
|
|
107
|
+
// Check backend-artifact signal first — a missing chrome-map.json looks
|
|
108
|
+
// like any other "No such file" error to a naïve regex, but the fix is
|
|
109
|
+
// to rerun `fireforge build`, not to reinstall Storybook dependencies.
|
|
110
|
+
if (hasFileNotFoundSignal && BACKEND_ARTIFACT_PATTERNS.some((p) => p.test(output))) {
|
|
111
|
+
return ('Storybook failed because the Firefox build backend artifacts are missing or stale ' +
|
|
112
|
+
'(chrome-map.json / config.status / obj-*/dist/bin/.lldbinit). ' +
|
|
113
|
+
'This is a Firefox-build completeness issue, not a Storybook dependency issue.\n\n' +
|
|
114
|
+
'Rerun "fireforge build" and let it finish, then retry "fireforge furnace preview". ' +
|
|
115
|
+
'A full rebuild regenerates the backend artifacts Storybook reads.');
|
|
116
|
+
}
|
|
117
|
+
if (hasFileNotFoundSignal && /storybook|backend/i.test(output)) {
|
|
86
118
|
return ('Storybook failed because the Firefox checkout appears to be missing Storybook workspace files or backend dependencies.\n\n' +
|
|
87
119
|
installHint);
|
|
88
120
|
}
|