@hominis/fireforge 0.15.5 → 0.15.7
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 +49 -0
- package/README.md +70 -5
- package/dist/src/commands/build.js +60 -3
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +17 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +18 -0
- package/dist/src/commands/furnace/chrome-doc-tests.d.ts +23 -0
- package/dist/src/commands/furnace/chrome-doc-tests.js +120 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +11 -0
- package/dist/src/commands/furnace/chrome-doc.js +37 -4
- package/dist/src/commands/furnace/create-dry-run.d.ts +31 -0
- package/dist/src/commands/furnace/create-dry-run.js +95 -0
- package/dist/src/commands/furnace/create-templates.js +14 -0
- package/dist/src/commands/furnace/create.js +28 -24
- package/dist/src/commands/furnace/index.js +3 -1
- package/dist/src/commands/lint.d.ts +17 -2
- package/dist/src/commands/lint.js +25 -2
- package/dist/src/commands/register.d.ts +1 -1
- package/dist/src/commands/register.js +30 -7
- package/dist/src/commands/test.js +16 -1
- package/dist/src/core/build-audit-platform.d.ts +3 -1
- package/dist/src/core/build-audit-platform.js +87 -20
- package/dist/src/core/build-audit-registration.d.ts +80 -0
- package/dist/src/core/build-audit-registration.js +187 -0
- package/dist/src/core/build-audit-transforms.d.ts +23 -0
- package/dist/src/core/build-audit-transforms.js +94 -0
- package/dist/src/core/build-audit.js +210 -3
- package/dist/src/core/furnace-validate-registration.d.ts +6 -4
- package/dist/src/core/furnace-validate-registration.js +66 -6
- package/dist/src/core/mach-build-artifacts.d.ts +44 -0
- package/dist/src/core/mach-build-artifacts.js +104 -3
- package/dist/src/core/mach.d.ts +1 -1
- package/dist/src/core/mach.js +1 -1
- package/dist/src/core/test-stale-check.d.ts +42 -0
- package/dist/src/core/test-stale-check.js +114 -0
- package/dist/src/types/commands/options.d.ts +16 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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,31 @@
|
|
|
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
|
+
testStyle: ResolvedTestStyle;
|
|
8
|
+
description: string;
|
|
9
|
+
binaryName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds the success-note body printed after `furnace create` has applied
|
|
13
|
+
* its mutations. Lives beside the dry-run formatter so the two renderings
|
|
14
|
+
* stay in lock-step when the scaffolded layout changes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatSuccessNote(args: {
|
|
17
|
+
componentName: string;
|
|
18
|
+
files: string[];
|
|
19
|
+
testFiles: string[];
|
|
20
|
+
testStyle: ResolvedTestStyle;
|
|
21
|
+
binaryName: string;
|
|
22
|
+
}): string;
|
|
23
|
+
/**
|
|
24
|
+
* Builds the planned component + test file list for a dry-run preview.
|
|
25
|
+
*
|
|
26
|
+
* Mirrors the order `writeComponentFiles` and the test-style scaffolders
|
|
27
|
+
* would produce so the dry-run output matches what a real run prints on
|
|
28
|
+
* success. The component directory path is rendered relative to
|
|
29
|
+
* `components/custom/` to match the wording of the real success note.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDryRunPlan(args: DryRunPlanInput): string;
|
|
@@ -0,0 +1,95 @@
|
|
|
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, testStyle, description, binaryName } = args;
|
|
79
|
+
const componentFiles = [`${componentName}.mjs`, `${componentName}.css`];
|
|
80
|
+
if (localized)
|
|
81
|
+
componentFiles.push(`${componentName}.ftl`);
|
|
82
|
+
let plan = `Would create files in components/custom/${componentName}/:\n` +
|
|
83
|
+
componentFiles.map((f) => ` ${f}`).join('\n');
|
|
84
|
+
plan += formatTestSection({ testStyle, componentName, binaryName });
|
|
85
|
+
plan += `\n\nWould add custom entry to furnace.json:`;
|
|
86
|
+
plan += `\n name: ${componentName}`;
|
|
87
|
+
plan += `\n description: ${description || '(empty)'}`;
|
|
88
|
+
plan += `\n register: ${register}`;
|
|
89
|
+
plan += `\n localized: ${localized}`;
|
|
90
|
+
if (composes && composes.length > 0) {
|
|
91
|
+
plan += `\n composes: ${composes.join(', ')}`;
|
|
92
|
+
}
|
|
93
|
+
return plan;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=create-dry-run.js.map
|
|
@@ -103,6 +103,20 @@ export function generateXpcshellTestContent(name, header) {
|
|
|
103
103
|
|
|
104
104
|
"use strict";
|
|
105
105
|
|
|
106
|
+
// Chrome-URI access from xpcshell:
|
|
107
|
+
// Toolkit chrome (chrome://global/*) IS registered and resolvable from
|
|
108
|
+
// this harness — that is what the smoke assertion below uses.
|
|
109
|
+
//
|
|
110
|
+
// Browser chrome (chrome://browser/*) is NOT registered unless the
|
|
111
|
+
// xpcshell.toml sets firefox-appdir = "browser" AND the built app bundle
|
|
112
|
+
// has landed every packaged chrome manifest. Even then, the set of
|
|
113
|
+
// manifests xpcshell loads lags what the real browser loads, so
|
|
114
|
+
// NetUtil.asyncFetch("chrome://browser/content/…") can still fail with
|
|
115
|
+
// NS_ERROR_FILE_NOT_FOUND against an artifact that IS present in
|
|
116
|
+
// obj-*/dist/. Assertions that need browser chrome URIs belong in a
|
|
117
|
+
// browser-chrome mochitest (fireforge furnace create --test-style=browser-chrome),
|
|
118
|
+
// not xpcshell.
|
|
119
|
+
|
|
106
120
|
add_task(async function test_${name.replace(/-/g, '_')}_module_loads() {
|
|
107
121
|
// Module-load smoke check: resolves the ESM at its registered chrome URI.
|
|
108
122
|
// Replace or extend with storage-layer assertions as the component grows
|
|
@@ -15,6 +15,7 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
15
15
|
import { toError } from '../../utils/errors.js';
|
|
16
16
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
17
17
|
import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
|
|
18
|
+
import { formatDryRunPlan, formatSuccessNote } from './create-dry-run.js';
|
|
18
19
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
19
20
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
20
21
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
@@ -374,7 +375,8 @@ function validateComposesTargets(config, componentName, composes) {
|
|
|
374
375
|
*/
|
|
375
376
|
export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
376
377
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
377
|
-
|
|
378
|
+
const isDryRun = options.dryRun ?? false;
|
|
379
|
+
intro(isDryRun ? 'Furnace Create (dry run)' : 'Furnace Create');
|
|
378
380
|
// --- Resolve component name ---
|
|
379
381
|
// Validation runs before we load/create any persisted furnace config so a
|
|
380
382
|
// failed authoring command never auto-creates furnace.json in a fresh
|
|
@@ -453,6 +455,24 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
453
455
|
// does not strand component files behind.
|
|
454
456
|
const composes = options.compose;
|
|
455
457
|
validateComposesTargets(config, componentName, composes);
|
|
458
|
+
// Dry-run exits here — every validation that does not need a write has
|
|
459
|
+
// already run, so the plan we render reflects exactly what the real
|
|
460
|
+
// command would do. The mutation phase and its rollback journal are
|
|
461
|
+
// intentionally skipped so no furnace.json/engine state is touched.
|
|
462
|
+
if (isDryRun) {
|
|
463
|
+
const plan = formatDryRunPlan({
|
|
464
|
+
componentName,
|
|
465
|
+
localized,
|
|
466
|
+
register,
|
|
467
|
+
composes,
|
|
468
|
+
testStyle,
|
|
469
|
+
description,
|
|
470
|
+
binaryName: forgeConfig.binaryName,
|
|
471
|
+
});
|
|
472
|
+
note(plan, componentName);
|
|
473
|
+
outro('Dry run complete (no files modified)');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
456
476
|
// All validation is done. Hand off to the transactional mutation helper
|
|
457
477
|
// so any failure restores the workspace and engine to their pre-command
|
|
458
478
|
// state via the shared rollback journal. The mutation runs under the
|
|
@@ -480,29 +500,13 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
480
500
|
ftlChromeSubPath,
|
|
481
501
|
operationContext: ctx,
|
|
482
502
|
}));
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
files
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
else if (testStyle === 'mochikit') {
|
|
492
|
-
testRoot = 'engine/toolkit/content/tests/widgets/';
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
testRoot = `engine/browser/base/content/test/${forgeConfig.binaryName}/`;
|
|
496
|
-
}
|
|
497
|
-
noteParts += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
|
|
498
|
-
}
|
|
499
|
-
noteParts +=
|
|
500
|
-
'\n\n' +
|
|
501
|
-
'Next steps:\n' +
|
|
502
|
-
` 1. Edit component files in components/custom/${componentName}/\n` +
|
|
503
|
-
' 2. Run "fireforge furnace preview" to see it\n' +
|
|
504
|
-
' 3. Run "fireforge build" to apply and build';
|
|
505
|
-
note(noteParts, componentName);
|
|
503
|
+
note(formatSuccessNote({
|
|
504
|
+
componentName,
|
|
505
|
+
files,
|
|
506
|
+
testFiles,
|
|
507
|
+
testStyle,
|
|
508
|
+
binaryName: forgeConfig.binaryName,
|
|
509
|
+
}), componentName);
|
|
506
510
|
outro('Component created');
|
|
507
511
|
}
|
|
508
512
|
//# sourceMappingURL=create.js.map
|
|
@@ -73,7 +73,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
73
73
|
.option('--localized', 'Include Fluent l10n support')
|
|
74
74
|
.option('--no-register', 'Skip customElements.js registration')
|
|
75
75
|
.option('--with-tests', 'Scaffold a test harness (defaults to MochiKit; see --test-style)')
|
|
76
|
-
.option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser); equivalent to --test-style=xpcshell')
|
|
76
|
+
.option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser); equivalent to --test-style=xpcshell. Note: xpcshell resolves chrome://global/* URIs but not chrome://browser/* — use --test-style=browser-chrome for browser-chrome-dependent tests.')
|
|
77
77
|
.option('--test-style <style>', "Override the harness written by --with-tests: mochikit (default, runs against non-tabbrowser chrome), browser-chrome (today's scaffold, needs tabbrowser), or xpcshell (headless)", (value) => {
|
|
78
78
|
if (value !== 'mochikit' && value !== 'browser-chrome' && value !== 'xpcshell') {
|
|
79
79
|
throw new Error(`--test-style must be one of: mochikit, browser-chrome, xpcshell. Got: "${value}".`);
|
|
@@ -81,6 +81,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
81
81
|
return value;
|
|
82
82
|
})
|
|
83
83
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
84
|
+
.option('--dry-run', 'Show the planned file set and furnace.json changes without writing')
|
|
84
85
|
.action(withErrorHandling(async (name, options) => {
|
|
85
86
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
86
87
|
}));
|
|
@@ -91,6 +92,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
91
92
|
.command('create <name>')
|
|
92
93
|
.description('Scaffold a new top-level chrome document')
|
|
93
94
|
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
95
|
+
.option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
|
|
94
96
|
.action(withErrorHandling(async (name, options) => {
|
|
95
97
|
await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
|
|
96
98
|
}));
|
|
@@ -6,10 +6,25 @@ export interface LintCommandOptions {
|
|
|
6
6
|
* When set, tag each issue as `introduced` or `cumulative` based on
|
|
7
7
|
* whether its file changed since this git revision (e.g. `HEAD`, a
|
|
8
8
|
* branch name, or a SHA). Issues are not filtered — the full set still
|
|
9
|
-
* prints
|
|
10
|
-
*
|
|
9
|
+
* prints — but a diff-scoped summary makes it trivial to see which
|
|
10
|
+
* errors the current task introduced.
|
|
11
11
|
*/
|
|
12
12
|
since?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When set together with {@link since}, scope the exit code to issues
|
|
15
|
+
* tagged `introduced`. Cumulative pre-existing errors still print (so
|
|
16
|
+
* the operator can still see the full queue state) but do not fail
|
|
17
|
+
* lint. Motivating case: a branch whose diff is clean but whose repo
|
|
18
|
+
* already carries unrelated `raw-color` / license-header errors from
|
|
19
|
+
* older patches. Without this flag, CI treats the clean branch as
|
|
20
|
+
* failing; with it, a branch "breaks the build" only when its own diff
|
|
21
|
+
* introduced a new error.
|
|
22
|
+
*
|
|
23
|
+
* Requires {@link since}: without a revision to diff against there is
|
|
24
|
+
* no distinction between introduced and cumulative, so the flag is
|
|
25
|
+
* rejected up-front rather than silently ignored.
|
|
26
|
+
*/
|
|
27
|
+
onlyIntroduced?: boolean;
|
|
13
28
|
}
|
|
14
29
|
/**
|
|
15
30
|
* Runs the lint command to check engine changes against patch quality rules.
|
|
@@ -19,6 +19,14 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export async function lintCommand(projectRoot, files, options = {}) {
|
|
21
21
|
intro('FireForge Lint');
|
|
22
|
+
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
23
|
+
// without a revision to anchor the diff there is no "introduced" subset
|
|
24
|
+
// to scope to — reject the combination up-front so a misconfigured CI
|
|
25
|
+
// invocation fails loud instead of silently treating every error as
|
|
26
|
+
// cumulative and passing.
|
|
27
|
+
if (options.onlyIntroduced && !options.since) {
|
|
28
|
+
throw new GeneralError('--only-introduced requires --since <git-rev> so introduced-vs-cumulative can be distinguished.');
|
|
29
|
+
}
|
|
22
30
|
const paths = getProjectPaths(projectRoot);
|
|
23
31
|
if (!(await pathExists(paths.engine))) {
|
|
24
32
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
@@ -139,9 +147,20 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
139
147
|
else {
|
|
140
148
|
info(`\nLint: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
141
149
|
}
|
|
142
|
-
|
|
150
|
+
// Exit-code scope: `--only-introduced` narrows the failure criterion to
|
|
151
|
+
// issues tagged `introduced`. Cumulative errors still print so the
|
|
152
|
+
// operator sees the full queue state, but do not fail lint — the
|
|
153
|
+
// motivating case is a branch whose own diff is clean but whose repo
|
|
154
|
+
// already carries pre-existing queue errors from older patches.
|
|
155
|
+
const failingErrors = options.onlyIntroduced
|
|
156
|
+
? errors.filter((i) => i.tag === 'introduced')
|
|
157
|
+
: errors;
|
|
158
|
+
if (failingErrors.length > 0) {
|
|
143
159
|
outro('Lint failed');
|
|
144
|
-
|
|
160
|
+
const cumulativeSuppressed = options.onlyIntroduced && errors.length > failingErrors.length
|
|
161
|
+
? ` (${errors.length - failingErrors.length} cumulative error(s) suppressed by --only-introduced)`
|
|
162
|
+
: '';
|
|
163
|
+
throw new GeneralError(`Patch lint found ${failingErrors.length} ${options.onlyIntroduced ? 'introduced ' : ''}error(s). Fix these before exporting.${cumulativeSuppressed}`);
|
|
145
164
|
}
|
|
146
165
|
outro('Lint passed with warnings');
|
|
147
166
|
}
|
|
@@ -151,11 +170,15 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
151
170
|
.command('lint [paths...]')
|
|
152
171
|
.description('Lint engine changes against patch quality rules')
|
|
153
172
|
.option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
|
|
173
|
+
.option('--only-introduced', 'Fail only on issues tagged [introduced] (requires --since). Cumulative errors still print but do not set a non-zero exit.')
|
|
154
174
|
.action(withErrorHandling(async (paths, options) => {
|
|
155
175
|
const lintOptions = {};
|
|
156
176
|
if (options.since !== undefined) {
|
|
157
177
|
lintOptions.since = options.since;
|
|
158
178
|
}
|
|
179
|
+
if (options.onlyIntroduced !== undefined) {
|
|
180
|
+
lintOptions.onlyIntroduced = options.onlyIntroduced;
|
|
181
|
+
}
|
|
159
182
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
160
183
|
}));
|
|
161
184
|
}
|
|
@@ -5,7 +5,7 @@ import type { RegisterOptions } from '../types/commands/index.js';
|
|
|
5
5
|
* Registers a file in the appropriate build manifest.
|
|
6
6
|
*
|
|
7
7
|
* @param projectRoot - Root directory of the project
|
|
8
|
-
* @param filePath - Path relative to engine/
|
|
8
|
+
* @param filePath - Path relative to engine/ (a leading `engine/` segment is stripped)
|
|
9
9
|
* @param options - Command options
|
|
10
10
|
*/
|
|
11
11
|
export declare function registerCommand(projectRoot: string, filePath: string, options?: RegisterOptions): Promise<void>;
|
|
@@ -6,11 +6,28 @@ import { InvalidArgumentError } from '../errors/base.js';
|
|
|
6
6
|
import { pathExists } from '../utils/fs.js';
|
|
7
7
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
8
8
|
import { pickDefined } from '../utils/options.js';
|
|
9
|
+
/**
|
|
10
|
+
* Strips a leading `engine/` segment (either separator flavour) from a
|
|
11
|
+
* user-supplied path so operators can pass either a repo-root-relative
|
|
12
|
+
* path (`engine/browser/base/content/foo.xhtml`) or an engine-relative
|
|
13
|
+
* path (`browser/base/content/foo.xhtml`). The engine-relative form is
|
|
14
|
+
* what the manifest writers expect; without this normalisation, the
|
|
15
|
+
* former failed with a misleading "File not found in engine" pointing
|
|
16
|
+
* at a doubled path like `engine/engine/browser/...` that operators
|
|
17
|
+
* had no way to spot from the error message alone.
|
|
18
|
+
*/
|
|
19
|
+
function normalizeEngineRelativePath(filePath) {
|
|
20
|
+
if (filePath.startsWith('engine/'))
|
|
21
|
+
return filePath.slice('engine/'.length);
|
|
22
|
+
if (filePath.startsWith('engine\\'))
|
|
23
|
+
return filePath.slice('engine\\'.length);
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
9
26
|
/**
|
|
10
27
|
* Registers a file in the appropriate build manifest.
|
|
11
28
|
*
|
|
12
29
|
* @param projectRoot - Root directory of the project
|
|
13
|
-
* @param filePath - Path relative to engine/
|
|
30
|
+
* @param filePath - Path relative to engine/ (a leading `engine/` segment is stripped)
|
|
14
31
|
* @param options - Command options
|
|
15
32
|
*/
|
|
16
33
|
export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
@@ -28,17 +45,23 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
28
45
|
throw new InvalidArgumentError('--after must be a non-empty substring without control characters or line terminators.', 'after');
|
|
29
46
|
}
|
|
30
47
|
}
|
|
48
|
+
// Accept either repo-root-relative (`engine/browser/...`) or
|
|
49
|
+
// engine-relative (`browser/...`) inputs — operators frequently paste
|
|
50
|
+
// the former from the output of tab completion or `git status`, and
|
|
51
|
+
// the mismatch used to produce a "File not found" error that named
|
|
52
|
+
// the original path with no hint that dropping `engine/` would fix it.
|
|
53
|
+
const engineRelativePath = normalizeEngineRelativePath(filePath);
|
|
31
54
|
// Verify the file exists in engine/ (skip for dry-run)
|
|
32
55
|
if (!options.dryRun) {
|
|
33
56
|
const paths = getProjectPaths(projectRoot);
|
|
34
|
-
const fullPath = join(paths.engine,
|
|
57
|
+
const fullPath = join(paths.engine, engineRelativePath);
|
|
35
58
|
if (!(await pathExists(fullPath))) {
|
|
36
|
-
throw new InvalidArgumentError(`File not found in engine: ${
|
|
59
|
+
throw new InvalidArgumentError(`File not found in engine: ${engineRelativePath}`, 'path');
|
|
37
60
|
}
|
|
38
61
|
}
|
|
39
|
-
const result = await registerFile(projectRoot,
|
|
62
|
+
const result = await registerFile(projectRoot, engineRelativePath, options.dryRun, options.after);
|
|
40
63
|
if (options.dryRun) {
|
|
41
|
-
info(`[dry-run] Would register ${
|
|
64
|
+
info(`[dry-run] Would register ${engineRelativePath}`);
|
|
42
65
|
info(` manifest: ${result.manifest}`);
|
|
43
66
|
info(` entry: ${result.entry}`);
|
|
44
67
|
if (result.previousEntry) {
|
|
@@ -54,14 +77,14 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
54
77
|
return;
|
|
55
78
|
}
|
|
56
79
|
if (result.skipped) {
|
|
57
|
-
info(`Already registered: ${
|
|
80
|
+
info(`Already registered: ${engineRelativePath} in ${result.manifest}`);
|
|
58
81
|
}
|
|
59
82
|
else {
|
|
60
83
|
if (result.afterFallback) {
|
|
61
84
|
warn(`--after target "${options.after}" not found, falling back to alphabetical order`);
|
|
62
85
|
}
|
|
63
86
|
const position = result.previousEntry ? ` (after ${result.previousEntry})` : '';
|
|
64
|
-
success(`Registered ${
|
|
87
|
+
success(`Registered ${engineRelativePath} in ${result.manifest}${position}`);
|
|
65
88
|
info("hint: Run 'fireforge build --ui' to make the new module available at runtime");
|
|
66
89
|
}
|
|
67
90
|
outro('Done');
|
|
@@ -4,10 +4,11 @@ import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, testWithOutput, } from '../core/mach.js';
|
|
6
6
|
import { reportMarionettePreflight, runMarionettePreflight } from '../core/marionette-preflight.js';
|
|
7
|
+
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
7
8
|
import { GeneralError } from '../errors/base.js';
|
|
8
9
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
9
10
|
import { pathExists } from '../utils/fs.js';
|
|
10
|
-
import { info, intro, spinner } from '../utils/logger.js';
|
|
11
|
+
import { info, intro, spinner, warn } from '../utils/logger.js';
|
|
11
12
|
import { pickDefined } from '../utils/options.js';
|
|
12
13
|
/**
|
|
13
14
|
* Strips a leading "engine/" or "engine\\" prefix from a path if present.
|
|
@@ -118,6 +119,20 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
118
119
|
s.stop('Build complete');
|
|
119
120
|
info('');
|
|
120
121
|
}
|
|
122
|
+
else {
|
|
123
|
+
// Stale-build preflight — when --build was NOT requested, detect
|
|
124
|
+
// packageable engine edits since the last successful `fireforge build`
|
|
125
|
+
// and warn UP-FRONT. Without this, edits to chrome / packaged resources
|
|
126
|
+
// surface only as a cryptic `NS_ERROR_FILE_NOT_FOUND` inside xpcshell
|
|
127
|
+
// after mach test has already launched (see motivating case in
|
|
128
|
+
// `core/test-stale-check.ts`). The check is warn-only so a fork that
|
|
129
|
+
// rebuilt out-of-band (no FireForge-recorded baseline update) is not
|
|
130
|
+
// blocked from running tests.
|
|
131
|
+
const stale = await checkStaleBuildForTest(projectRoot, paths.engine);
|
|
132
|
+
if (stale.stale) {
|
|
133
|
+
warn(formatStaleBuildWarning(stale));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
121
136
|
// `--doctor` runs a short marionette handshake probe. When test paths are
|
|
122
137
|
// supplied the probe gates the mach test invocation (a FAIL bails out). When
|
|
123
138
|
// no paths are supplied this is the only step — it's the fastest way to tell
|
|
@@ -24,7 +24,9 @@ export interface PlatformGateResult {
|
|
|
24
24
|
export declare function findEnclosingGate(content: string, basename: string): string | undefined;
|
|
25
25
|
/**
|
|
26
26
|
* Determines whether the given source file is gated off on the current
|
|
27
|
-
* host by an enclosing `if CONFIG[...]:` block in its owning moz.build
|
|
27
|
+
* host by an enclosing `if CONFIG[...]:` block in its owning moz.build,
|
|
28
|
+
* OR by a path-convention rule for installer-tree subdirectories that
|
|
29
|
+
* are packaged via Makefile.in recipes the audit does not parse.
|
|
28
30
|
* Returns `gatedOff: false` and no expression when no gate is found —
|
|
29
31
|
* the file is not platform-restricted, so the caller should audit it
|
|
30
32
|
* normally.
|