@hominis/fireforge 0.15.1 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -3
- package/README.md +76 -3
- package/dist/src/commands/build.js +41 -3
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
- package/dist/src/commands/furnace/chrome-doc.js +168 -0
- package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
- package/dist/src/commands/furnace/create-mochikit.js +70 -0
- package/dist/src/commands/furnace/create-templates.d.ts +53 -0
- package/dist/src/commands/furnace/create-templates.js +118 -0
- package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
- package/dist/src/commands/furnace/create-xpcshell.js +53 -0
- package/dist/src/commands/furnace/create.d.ts +17 -0
- package/dist/src/commands/furnace/create.js +59 -12
- package/dist/src/commands/furnace/index.d.ts +2 -1
- package/dist/src/commands/furnace/index.js +20 -2
- package/dist/src/commands/lint.d.ts +13 -1
- package/dist/src/commands/lint.js +33 -7
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/core/build-audit.d.ts +46 -0
- package/dist/src/core/build-audit.js +251 -0
- package/dist/src/core/build-baseline.d.ts +59 -0
- package/dist/src/core/build-baseline.js +83 -0
- package/dist/src/core/build-prepare.d.ts +20 -1
- package/dist/src/core/build-prepare.js +94 -4
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-config-tokens.d.ts +6 -0
- package/dist/src/core/furnace-config-tokens.js +15 -0
- package/dist/src/core/furnace-config.js +10 -4
- package/dist/src/core/furnace-operation.d.ts +2 -1
- package/dist/src/core/furnace-operation.js +13 -7
- package/dist/src/core/furnace-registration-ast.d.ts +2 -2
- package/dist/src/core/furnace-registration-ast.js +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +31 -1
- package/dist/src/core/furnace-validate-helpers.js +101 -18
- package/dist/src/core/furnace-validate-registration.d.ts +1 -1
- package/dist/src/core/furnace-validate-registration.js +1 -1
- package/dist/src/core/mach-error-hints.d.ts +29 -0
- package/dist/src/core/mach-error-hints.js +43 -0
- package/dist/src/core/mach.d.ts +5 -2
- package/dist/src/core/mach.js +31 -4
- package/dist/src/core/marionette-preflight.d.ts +14 -7
- package/dist/src/core/marionette-preflight.js +94 -44
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +1 -1
- package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
- package/dist/src/core/patch-lint-diff-tag.js +83 -0
- package/dist/src/core/patch-lint.js +29 -9
- package/dist/src/types/commands/options.d.ts +25 -0
- package/dist/src/types/commands/patches.d.ts +9 -0
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +13 -2
- package/package.json +1 -1
|
@@ -15,7 +15,9 @@ 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 { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
18
19
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
20
|
+
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
19
21
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
20
22
|
if (await furnaceConfigExists(projectRoot)) {
|
|
21
23
|
return loadFurnaceConfig(projectRoot);
|
|
@@ -223,6 +225,35 @@ async function writeComponentFiles(componentDir, componentName, className, descr
|
|
|
223
225
|
}
|
|
224
226
|
return files;
|
|
225
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Collapses `--with-tests`, `--xpcshell`, and `--test-style` into the single
|
|
230
|
+
* scaffold dispatch used inside the mutation phase.
|
|
231
|
+
*
|
|
232
|
+
* Backwards-compat invariants:
|
|
233
|
+
* - `--xpcshell` alone is equivalent to `--test-style=xpcshell`.
|
|
234
|
+
* - `--with-tests` alone (no `--test-style`) now defaults to `mochikit`
|
|
235
|
+
* (previously it defaulted to browser-chrome; the dogfooding pass
|
|
236
|
+
* flagged browser-chrome as unrunnable against non-tabbrowser chrome).
|
|
237
|
+
* Operators who need the old behavior can pass
|
|
238
|
+
* `--with-tests --test-style=browser-chrome`.
|
|
239
|
+
* - `--xpcshell --with-tests` is rejected as ambiguous.
|
|
240
|
+
* @throws InvalidArgumentError when flags conflict.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveTestStyle(options) {
|
|
243
|
+
const xpcshellFlag = options.xpcshell ?? false;
|
|
244
|
+
const withTests = options.withTests ?? false;
|
|
245
|
+
const explicit = options.testStyle;
|
|
246
|
+
if (xpcshellFlag && explicit && explicit !== 'xpcshell') {
|
|
247
|
+
throw new InvalidArgumentError(`--xpcshell cannot be combined with --test-style=${explicit}; choose one.`, 'testStyle');
|
|
248
|
+
}
|
|
249
|
+
if (explicit)
|
|
250
|
+
return explicit;
|
|
251
|
+
if (xpcshellFlag)
|
|
252
|
+
return 'xpcshell';
|
|
253
|
+
if (withTests)
|
|
254
|
+
return 'mochikit';
|
|
255
|
+
return 'none';
|
|
256
|
+
}
|
|
226
257
|
/**
|
|
227
258
|
* Performs the transactional mutation phase of furnace create. All file
|
|
228
259
|
* writes and the config update are recorded in a rollback journal so a
|
|
@@ -258,10 +289,18 @@ async function performCreateMutations(args) {
|
|
|
258
289
|
args.config.custom[args.componentName] = customEntry;
|
|
259
290
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
260
291
|
await writeFurnaceConfig(args.projectRoot, args.config);
|
|
261
|
-
if (args.
|
|
292
|
+
if (args.testStyle === 'browser-chrome') {
|
|
262
293
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
263
294
|
testFiles.push(...scafFiles);
|
|
264
295
|
}
|
|
296
|
+
else if (args.testStyle === 'xpcshell') {
|
|
297
|
+
const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
298
|
+
testFiles.push(...xpcshellFiles);
|
|
299
|
+
}
|
|
300
|
+
else if (args.testStyle === 'mochikit') {
|
|
301
|
+
const mochikitFiles = await scaffoldMochikitTestFiles(args.componentName, args.license, args.paths, journal);
|
|
302
|
+
testFiles.push(...mochikitFiles);
|
|
303
|
+
}
|
|
265
304
|
}
|
|
266
305
|
catch (error) {
|
|
267
306
|
try {
|
|
@@ -395,13 +434,13 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
395
434
|
return;
|
|
396
435
|
}
|
|
397
436
|
const { localized, register } = featureSelection;
|
|
398
|
-
// --with-tests
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
-
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests.', componentName);
|
|
437
|
+
// Collapse --with-tests / --xpcshell / --test-style into the single
|
|
438
|
+
// scaffold selection used by the mutation phase. The resolver validates
|
|
439
|
+
// incompatible combinations up-front so a bad flag set never strands a
|
|
440
|
+
// partial mutation behind.
|
|
441
|
+
const testStyle = resolveTestStyle(options);
|
|
442
|
+
if (testStyle !== 'none' && !(await pathExists(paths.engine))) {
|
|
443
|
+
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests, --xpcshell, or --test-style.', componentName);
|
|
405
444
|
}
|
|
406
445
|
// --- Generate component files ---
|
|
407
446
|
const className = tagNameToClassName(componentName);
|
|
@@ -437,7 +476,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
437
476
|
forgeConfig,
|
|
438
477
|
paths,
|
|
439
478
|
license,
|
|
440
|
-
|
|
479
|
+
testStyle,
|
|
441
480
|
ftlChromeSubPath,
|
|
442
481
|
operationContext: ctx,
|
|
443
482
|
}));
|
|
@@ -445,9 +484,17 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
445
484
|
let noteParts = `Files created in components/custom/${componentName}/:\n` +
|
|
446
485
|
files.map((f) => ` ${f}`).join('\n');
|
|
447
486
|
if (testFiles.length > 0) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
487
|
+
let testRoot;
|
|
488
|
+
if (testStyle === 'xpcshell') {
|
|
489
|
+
testRoot = `engine/browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${componentName}/`;
|
|
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');
|
|
451
498
|
}
|
|
452
499
|
noteParts +=
|
|
453
500
|
'\n\n' +
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../../types/cli.js';
|
|
3
3
|
import { furnaceApplyCommand } from './apply.js';
|
|
4
|
+
import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
|
|
4
5
|
import { furnaceCreateCommand } from './create.js';
|
|
5
6
|
import { furnaceDeployCommand } from './deploy.js';
|
|
6
7
|
import { furnaceDiffCommand } from './diff.js';
|
|
@@ -15,6 +16,6 @@ import { furnaceScanCommand } from './scan.js';
|
|
|
15
16
|
import { furnaceStatusCommand } from './status.js';
|
|
16
17
|
import { furnaceSyncCommand } from './sync.js';
|
|
17
18
|
import { furnaceValidateCommand } from './validate.js';
|
|
18
|
-
export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
|
|
19
|
+
export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceChromeDocCreateCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
|
|
19
20
|
/** Registers the furnace command on the CLI program. */
|
|
20
21
|
export declare function registerFurnace(program: Command, context: CommandContext): void;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Option } from 'commander';
|
|
3
3
|
import { pickDefined } from '../../utils/options.js';
|
|
4
4
|
import { furnaceApplyCommand } from './apply.js';
|
|
5
|
+
import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
|
|
5
6
|
import { furnaceCreateCommand } from './create.js';
|
|
6
7
|
import { furnaceDeployCommand } from './deploy.js';
|
|
7
8
|
import { furnaceDiffCommand } from './diff.js';
|
|
@@ -16,7 +17,7 @@ import { furnaceScanCommand } from './scan.js';
|
|
|
16
17
|
import { furnaceStatusCommand } from './status.js';
|
|
17
18
|
import { furnaceSyncCommand } from './sync.js';
|
|
18
19
|
import { furnaceValidateCommand } from './validate.js';
|
|
19
|
-
export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
|
|
20
|
+
export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceChromeDocCreateCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
|
|
20
21
|
/**
|
|
21
22
|
* Registers Furnace commands for querying component state: status, scan,
|
|
22
23
|
* and action commands like apply, deploy, and create.
|
|
@@ -71,11 +72,28 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
71
72
|
.option('-d, --description <desc>', 'Component description')
|
|
72
73
|
.option('--localized', 'Include Fluent l10n support')
|
|
73
74
|
.option('--no-register', 'Skip customElements.js registration')
|
|
74
|
-
.option('--with-tests', 'Scaffold
|
|
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')
|
|
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
|
+
if (value !== 'mochikit' && value !== 'browser-chrome' && value !== 'xpcshell') {
|
|
79
|
+
throw new Error(`--test-style must be one of: mochikit, browser-chrome, xpcshell. Got: "${value}".`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
})
|
|
75
83
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
76
84
|
.action(withErrorHandling(async (name, options) => {
|
|
77
85
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
78
86
|
}));
|
|
87
|
+
const chromeDoc = furnace
|
|
88
|
+
.command('chrome-doc')
|
|
89
|
+
.description('Scaffold top-level chrome documents (xhtml + js + css + ftl + jar.mn)');
|
|
90
|
+
chromeDoc
|
|
91
|
+
.command('create <name>')
|
|
92
|
+
.description('Scaffold a new top-level chrome document')
|
|
93
|
+
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
94
|
+
.action(withErrorHandling(async (name, options) => {
|
|
95
|
+
await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
|
|
96
|
+
}));
|
|
79
97
|
}
|
|
80
98
|
/**
|
|
81
99
|
* Registers Furnace commands for authoring, inspection, and maintenance:
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
/** Options controlling how the lint command filters and tags its output. */
|
|
4
|
+
export interface LintCommandOptions {
|
|
5
|
+
/**
|
|
6
|
+
* When set, tag each issue as `introduced` or `cumulative` based on
|
|
7
|
+
* whether its file changed since this git revision (e.g. `HEAD`, a
|
|
8
|
+
* branch name, or a SHA). Issues are not filtered — the full set still
|
|
9
|
+
* prints and the exit code is unchanged — but a diff-scoped summary
|
|
10
|
+
* makes it trivial to see which errors the current task introduced.
|
|
11
|
+
*/
|
|
12
|
+
since?: string;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Runs the lint command to check engine changes against patch quality rules.
|
|
5
16
|
* @param projectRoot - Root directory of the project
|
|
6
17
|
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
18
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
7
19
|
*/
|
|
8
|
-
export declare function lintCommand(projectRoot: string, files: string[]): Promise<void>;
|
|
20
|
+
export declare function lintCommand(projectRoot: string, files: string[], options?: LintCommandOptions): Promise<void>;
|
|
9
21
|
/** Registers the lint command on the CLI program. */
|
|
10
22
|
export declare function registerLint(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|
|
@@ -7,6 +7,7 @@ import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
|
7
7
|
import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } from '../core/git-status.js';
|
|
8
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
9
9
|
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
10
|
+
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
10
11
|
import { GeneralError } from '../errors/base.js';
|
|
11
12
|
import { pathExists } from '../utils/fs.js';
|
|
12
13
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
@@ -14,8 +15,9 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
14
15
|
* Runs the lint command to check engine changes against patch quality rules.
|
|
15
16
|
* @param projectRoot - Root directory of the project
|
|
16
17
|
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
18
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
17
19
|
*/
|
|
18
|
-
export async function lintCommand(projectRoot, files) {
|
|
20
|
+
export async function lintCommand(projectRoot, files, options = {}) {
|
|
19
21
|
intro('FireForge Lint');
|
|
20
22
|
const paths = getProjectPaths(projectRoot);
|
|
21
23
|
if (!(await pathExists(paths.engine))) {
|
|
@@ -105,19 +107,38 @@ export async function lintCommand(projectRoot, files) {
|
|
|
105
107
|
outro('Lint passed');
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
110
|
+
// Diff-scoping: tag each issue as introduced-in-current-task vs
|
|
111
|
+
// cumulative-pre-existing-drift. Never filters — full set still prints
|
|
112
|
+
// and exit code semantics are unchanged — but the per-line prefix and
|
|
113
|
+
// summary make triage trivial on a large patch series.
|
|
114
|
+
const sinceActive = Boolean(options.since);
|
|
115
|
+
if (options.since) {
|
|
116
|
+
const diffFiles = await collectDiffFilePaths(paths.engine, options.since);
|
|
117
|
+
tagLintIssues(issues, diffFiles);
|
|
118
|
+
}
|
|
108
119
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
109
120
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
110
121
|
const notices = issues.filter((i) => i.severity === 'notice');
|
|
122
|
+
const tagPrefix = (issue) => sinceActive && issue.tag ? `[${issue.tag}] ` : '';
|
|
111
123
|
for (const issue of notices) {
|
|
112
|
-
info(
|
|
124
|
+
info(`${tagPrefix(issue)}NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
113
125
|
}
|
|
114
126
|
for (const issue of warnings) {
|
|
115
|
-
warn(
|
|
127
|
+
warn(`${tagPrefix(issue)}[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
116
128
|
}
|
|
117
129
|
for (const issue of errors) {
|
|
118
|
-
warn(
|
|
130
|
+
warn(`${tagPrefix(issue)}ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
131
|
+
}
|
|
132
|
+
if (sinceActive) {
|
|
133
|
+
const introducedErrors = errors.filter((i) => i.tag === 'introduced').length;
|
|
134
|
+
const introducedWarnings = warnings.filter((i) => i.tag === 'introduced').length;
|
|
135
|
+
const cumulativeErrors = errors.length - introducedErrors;
|
|
136
|
+
const cumulativeWarnings = warnings.length - introducedWarnings;
|
|
137
|
+
info(`\nLint: ${introducedErrors} introduced error(s), ${introducedWarnings} introduced warning(s); ${cumulativeErrors} cumulative error(s), ${cumulativeWarnings} cumulative warning(s)`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
info(`\nLint: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
119
141
|
}
|
|
120
|
-
info(`\nLint: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
121
142
|
if (errors.length > 0) {
|
|
122
143
|
outro('Lint failed');
|
|
123
144
|
throw new GeneralError(`Patch lint found ${errors.length} error(s). Fix these before exporting.`);
|
|
@@ -129,8 +150,13 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
129
150
|
program
|
|
130
151
|
.command('lint [paths...]')
|
|
131
152
|
.description('Lint engine changes against patch quality rules')
|
|
132
|
-
.
|
|
133
|
-
|
|
153
|
+
.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)')
|
|
154
|
+
.action(withErrorHandling(async (paths, options) => {
|
|
155
|
+
const lintOptions = {};
|
|
156
|
+
if (options.since !== undefined) {
|
|
157
|
+
lintOptions.since = options.since;
|
|
158
|
+
}
|
|
159
|
+
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
134
160
|
}));
|
|
135
161
|
}
|
|
136
162
|
//# sourceMappingURL=lint.js.map
|
|
@@ -8,4 +8,4 @@ import type { SetupOptions } from '../types/commands/index.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function setupCommand(projectRoot: string, options?: SetupOptions): Promise<void>;
|
|
10
10
|
/** Registers the setup command on the CLI program. */
|
|
11
|
-
export declare function registerSetup(program: Command, {
|
|
11
|
+
export declare function registerSetup(program: Command, { withErrorHandling }: CommandContext): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { confirm } from '@clack/prompts';
|
|
3
4
|
import { Option } from 'commander';
|
|
4
5
|
import { configExists } from '../core/config.js';
|
|
@@ -57,7 +58,7 @@ export async function setupCommand(projectRoot, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
/** Registers the setup command on the CLI program. */
|
|
60
|
-
export function registerSetup(program, {
|
|
61
|
+
export function registerSetup(program, { withErrorHandling }) {
|
|
61
62
|
program
|
|
62
63
|
.command('setup')
|
|
63
64
|
.description('Initialize a new FireForge project')
|
|
@@ -88,7 +89,7 @@ export function registerSetup(program, { getProjectRoot, withErrorHandling }) {
|
|
|
88
89
|
setupOptions.license = parsedLicense;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
await setupCommand(
|
|
92
|
+
await setupCommand(resolve(process.cwd()), setupOptions);
|
|
92
93
|
}));
|
|
93
94
|
}
|
|
94
95
|
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BuildBaseline } from './build-baseline.js';
|
|
2
|
+
/** Result of a single artifact lookup. */
|
|
3
|
+
export interface AuditEntry {
|
|
4
|
+
/** Engine-relative source file path (POSIX separators). */
|
|
5
|
+
source: string;
|
|
6
|
+
/**
|
|
7
|
+
* Resolved artifact path inside the dist tree, or undefined when no
|
|
8
|
+
* candidate bundle location was found. An entry with an undefined path
|
|
9
|
+
* and status "missing" means the source was packageable but nothing
|
|
10
|
+
* that looked like its artifact showed up in the bundle.
|
|
11
|
+
*/
|
|
12
|
+
artifact: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* updated: an artifact exists and is at least as new as the source.
|
|
15
|
+
* stale: artifact exists but is older than the source (probable packaging drop).
|
|
16
|
+
* missing: no artifact with a matching basename was found anywhere under dist/.
|
|
17
|
+
* skipped: the file extension / path does not imply packaging; not counted.
|
|
18
|
+
*/
|
|
19
|
+
status: 'updated' | 'stale' | 'missing' | 'skipped';
|
|
20
|
+
}
|
|
21
|
+
/** Summary counts for the "Packaged:" end-of-build line. */
|
|
22
|
+
export interface AuditSummary {
|
|
23
|
+
updated: number;
|
|
24
|
+
stale: number;
|
|
25
|
+
missing: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
entries: AuditEntry[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decides whether a source path should be packaged. Returns true for paths
|
|
31
|
+
* whose extension or directory fragment matches a known-packaged pattern.
|
|
32
|
+
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
33
|
+
* @returns True when the path implies packaging.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isPackageablePath(sourcePath: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
38
|
+
* stale artifacts and a summary info line at the end. Always returns
|
|
39
|
+
* the summary; never throws on audit failure (the audit itself must
|
|
40
|
+
* never fail a successful build).
|
|
41
|
+
* @param projectRoot Root of the project (reserved for future fork-specific rules).
|
|
42
|
+
* @param engineDir Path to the engine directory.
|
|
43
|
+
* @param baseline Optional previous-build baseline marker.
|
|
44
|
+
* @returns Summary of artifact status counts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function auditBuildArtifacts(projectRoot: string, engineDir: string, baseline: BuildBaseline | undefined): Promise<AuditSummary>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* Purpose: catch the class of bug where a file under engine/ was edited
|
|
6
|
+
* but never registered in moz.build, jar.mn, or package-manifest.in, so
|
|
7
|
+
* the mach build reports success but the packaged bundle carries stale
|
|
8
|
+
* or missing content. A fork-specific pref file that was never registered
|
|
9
|
+
* for packaging is the motivating case.
|
|
10
|
+
*
|
|
11
|
+
* The audit is best-effort and warn-only:
|
|
12
|
+
* - It enumerates engine files changed since the previous build baseline
|
|
13
|
+
* (git-tracked diff + workdir modifications).
|
|
14
|
+
* - For each file whose path pattern implies packaging, it resolves
|
|
15
|
+
* the expected dist artifact under obj-star/dist/binary-name-star.
|
|
16
|
+
* - A warning fires when the expected artifact is missing OR when its
|
|
17
|
+
* mtime is older than the engine source (the build was reported
|
|
18
|
+
* successful but that file's path never flowed through packaging).
|
|
19
|
+
* - False positives are acceptable at this stage: fork-specific packaging
|
|
20
|
+
* tricks FireForge doesn't know about will surface as warnings an
|
|
21
|
+
* operator can investigate. The audit never fails the build.
|
|
22
|
+
*/
|
|
23
|
+
import { stat } from 'node:fs/promises';
|
|
24
|
+
import { basename, join } from 'node:path';
|
|
25
|
+
import { toError } from '../utils/errors.js';
|
|
26
|
+
import { pathExists } from '../utils/fs.js';
|
|
27
|
+
import { info, verbose, warn } from '../utils/logger.js';
|
|
28
|
+
import { hasChanges, isMissingHeadError } from './git.js';
|
|
29
|
+
import { git } from './git-base.js';
|
|
30
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
31
|
+
/** Path extensions that are conventionally packaged into the Firefox bundle. */
|
|
32
|
+
const PACKAGEABLE_EXTENSIONS = [
|
|
33
|
+
'.js',
|
|
34
|
+
'.mjs',
|
|
35
|
+
'.jsm',
|
|
36
|
+
'.css',
|
|
37
|
+
'.ftl',
|
|
38
|
+
'.xhtml',
|
|
39
|
+
'.xul',
|
|
40
|
+
'.html',
|
|
41
|
+
'.properties',
|
|
42
|
+
];
|
|
43
|
+
/** Path fragments whose contents are packaged regardless of extension. */
|
|
44
|
+
const PACKAGEABLE_PATH_FRAGMENTS = ['/app/profile/', '/chrome/', '/locales/'];
|
|
45
|
+
/** Directories that are build artifacts, not source — never audited. */
|
|
46
|
+
const IGNORE_PATH_FRAGMENTS = ['obj-', 'node_modules/', '.git/', '.cargo/', '.mozbuild/'];
|
|
47
|
+
/*
|
|
48
|
+
* Finds the first file with the given basename anywhere under the dist
|
|
49
|
+
* bundle. Scans the darwin Contents/Resources layout and the linux/win
|
|
50
|
+
* top-level layout with a depth-limited traversal so deeply-nested
|
|
51
|
+
* node_modules in the dist copy do not dominate the audit wall clock.
|
|
52
|
+
*/
|
|
53
|
+
async function findArtifactByBasename(distRoot, name, maxDepth = 10) {
|
|
54
|
+
const { readdir } = await import('node:fs/promises');
|
|
55
|
+
const stack = [{ dir: distRoot, depth: 0 }];
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const entry = stack.pop();
|
|
58
|
+
if (!entry)
|
|
59
|
+
break;
|
|
60
|
+
if (entry.depth > maxDepth)
|
|
61
|
+
continue;
|
|
62
|
+
let children;
|
|
63
|
+
try {
|
|
64
|
+
children = await readdir(entry.dir, { withFileTypes: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
const fullPath = join(entry.dir, child.name);
|
|
71
|
+
if (child.isDirectory()) {
|
|
72
|
+
// Skip the symlinked mozbuild cache tree which contains full copies
|
|
73
|
+
// and would dominate the scan on macOS.
|
|
74
|
+
if (child.name.startsWith('.'))
|
|
75
|
+
continue;
|
|
76
|
+
stack.push({ dir: fullPath, depth: entry.depth + 1 });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (child.name === name) {
|
|
80
|
+
return fullPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decides whether a source path should be packaged. Returns true for paths
|
|
88
|
+
* whose extension or directory fragment matches a known-packaged pattern.
|
|
89
|
+
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
90
|
+
* @returns True when the path implies packaging.
|
|
91
|
+
*/
|
|
92
|
+
export function isPackageablePath(sourcePath) {
|
|
93
|
+
for (const fragment of IGNORE_PATH_FRAGMENTS) {
|
|
94
|
+
if (sourcePath.includes(fragment))
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
for (const ext of PACKAGEABLE_EXTENSIONS) {
|
|
98
|
+
if (sourcePath.endsWith(ext))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
for (const fragment of PACKAGEABLE_PATH_FRAGMENTS) {
|
|
102
|
+
if (sourcePath.includes(fragment))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Collects engine-relative paths changed since the baseline's HEAD SHA.
|
|
109
|
+
* Always includes modified + untracked workdir paths. When the baseline is
|
|
110
|
+
* missing or the engine has no HEAD yet, falls back to workdir-only diffs.
|
|
111
|
+
*/
|
|
112
|
+
async function collectChangedFiles(engineDir, baseline) {
|
|
113
|
+
const collected = new Set();
|
|
114
|
+
if (baseline?.engineHeadSha) {
|
|
115
|
+
try {
|
|
116
|
+
const output = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
|
|
117
|
+
for (const line of output.split('\n')) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (trimmed)
|
|
120
|
+
collected.add(trimmed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isMissingHeadError(error)) {
|
|
125
|
+
verbose(`Audit: could not diff against baseline SHA — ${toError(error).message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
if (await hasChanges(engineDir)) {
|
|
131
|
+
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
132
|
+
for (const line of worktreeDiff.split('\n')) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed)
|
|
135
|
+
collected.add(trimmed);
|
|
136
|
+
}
|
|
137
|
+
const untracked = await getUntrackedFiles(engineDir);
|
|
138
|
+
for (const file of untracked) {
|
|
139
|
+
collected.add(file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
verbose(`Audit: could not enumerate workdir changes — ${toError(error).message}`);
|
|
145
|
+
}
|
|
146
|
+
return [...collected].sort();
|
|
147
|
+
}
|
|
148
|
+
/*
|
|
149
|
+
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
150
|
+
* when zero or multiple match. The ambiguous case is already rejected
|
|
151
|
+
* by pre-flight in build.ts, so the auditor only has to handle
|
|
152
|
+
* one-or-none.
|
|
153
|
+
*/
|
|
154
|
+
async function resolveDistRoot(engineDir) {
|
|
155
|
+
const { readdir } = await import('node:fs/promises');
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = await readdir(engineDir);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const objDirs = entries.filter((e) => e.startsWith('obj-'));
|
|
164
|
+
for (const objDir of objDirs) {
|
|
165
|
+
const distPath = join(engineDir, objDir, 'dist');
|
|
166
|
+
if (await pathExists(distPath)) {
|
|
167
|
+
return distPath;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
174
|
+
* stale artifacts and a summary info line at the end. Always returns
|
|
175
|
+
* the summary; never throws on audit failure (the audit itself must
|
|
176
|
+
* never fail a successful build).
|
|
177
|
+
* @param projectRoot Root of the project (reserved for future fork-specific rules).
|
|
178
|
+
* @param engineDir Path to the engine directory.
|
|
179
|
+
* @param baseline Optional previous-build baseline marker.
|
|
180
|
+
* @returns Summary of artifact status counts.
|
|
181
|
+
*/
|
|
182
|
+
export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
|
|
183
|
+
void projectRoot;
|
|
184
|
+
const summary = {
|
|
185
|
+
updated: 0,
|
|
186
|
+
stale: 0,
|
|
187
|
+
missing: 0,
|
|
188
|
+
skipped: 0,
|
|
189
|
+
entries: [],
|
|
190
|
+
};
|
|
191
|
+
const distRoot = await resolveDistRoot(engineDir);
|
|
192
|
+
if (!distRoot) {
|
|
193
|
+
verbose('Audit skipped: no dist tree found under obj-*/dist/.');
|
|
194
|
+
return summary;
|
|
195
|
+
}
|
|
196
|
+
const changed = await collectChangedFiles(engineDir, baseline);
|
|
197
|
+
if (changed.length === 0) {
|
|
198
|
+
return summary;
|
|
199
|
+
}
|
|
200
|
+
for (const source of changed) {
|
|
201
|
+
if (!isPackageablePath(source)) {
|
|
202
|
+
summary.skipped += 1;
|
|
203
|
+
summary.entries.push({ source, artifact: undefined, status: 'skipped' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const sourcePath = join(engineDir, source);
|
|
207
|
+
let sourceMtime;
|
|
208
|
+
try {
|
|
209
|
+
const sourceStat = await stat(sourcePath);
|
|
210
|
+
sourceMtime = sourceStat.mtimeMs;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// File was deleted since the diff was computed. Skip — a deletion
|
|
214
|
+
// that didn't propagate to the dist tree is a distinct class of bug
|
|
215
|
+
// we don't audit yet.
|
|
216
|
+
summary.skipped += 1;
|
|
217
|
+
summary.entries.push({ source, artifact: undefined, status: 'skipped' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const artifact = await findArtifactByBasename(distRoot, basename(source));
|
|
221
|
+
if (!artifact) {
|
|
222
|
+
summary.missing += 1;
|
|
223
|
+
summary.entries.push({ source, artifact: undefined, status: 'missing' });
|
|
224
|
+
warn(`Audit: engine/${source} was touched but no packaged artifact with basename "${basename(source)}" was found under ${distRoot}. Missing moz.build / jar.mn / package-manifest.in registration?`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
let artifactMtime;
|
|
228
|
+
try {
|
|
229
|
+
const artifactStat = await stat(artifact);
|
|
230
|
+
artifactMtime = artifactStat.mtimeMs;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Disappeared after the directory scan; treat as missing.
|
|
234
|
+
summary.missing += 1;
|
|
235
|
+
summary.entries.push({ source, artifact, status: 'missing' });
|
|
236
|
+
warn(`Audit: engine/${source} has no readable packaged artifact at ${artifact} (disappeared during audit).`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (artifactMtime + 1 < sourceMtime) {
|
|
240
|
+
summary.stale += 1;
|
|
241
|
+
summary.entries.push({ source, artifact, status: 'stale' });
|
|
242
|
+
warn(`Audit: engine/${source} is newer than its packaged artifact ${artifact}. Build reported success but the file's path may not flow through packaging — check moz.build / jar.mn entries.`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
summary.updated += 1;
|
|
246
|
+
summary.entries.push({ source, artifact, status: 'updated' });
|
|
247
|
+
}
|
|
248
|
+
info(`Packaged: ${summary.updated} updated, ${summary.stale} stale, ${summary.missing} missing, ${summary.skipped} skipped`);
|
|
249
|
+
return summary;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=build-audit.js.map
|