@hominis/fireforge 0.15.6 → 0.15.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +158 -15
- 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 +38 -0
- package/dist/src/commands/furnace/create-dry-run.js +100 -0
- package/dist/src/commands/furnace/create-features.d.ts +24 -0
- package/dist/src/commands/furnace/create-features.js +56 -0
- package/dist/src/commands/furnace/create-templates.d.ts +9 -5
- package/dist/src/commands/furnace/create-templates.js +28 -6
- package/dist/src/commands/furnace/create.js +62 -63
- package/dist/src/commands/furnace/index.js +4 -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/run.d.ts +15 -1
- package/dist/src/commands/run.js +202 -7
- package/dist/src/commands/test.js +113 -3
- 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 +107 -7
- package/dist/src/core/furnace-apply-ftl.d.ts +5 -3
- package/dist/src/core/furnace-apply-ftl.js +6 -2
- package/dist/src/core/furnace-apply-helpers.js +14 -4
- package/dist/src/core/furnace-config-custom.d.ts +14 -0
- package/dist/src/core/furnace-config-custom.js +64 -0
- package/dist/src/core/furnace-config.js +2 -39
- package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
- package/dist/src/core/furnace-validate-accessibility.js +17 -3
- package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
- package/dist/src/core/furnace-validate-helpers.js +19 -0
- 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/furnace-validate-structure.js +6 -2
- package/dist/src/core/furnace-validate.js +6 -3
- 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 +27 -1
- package/dist/src/core/mach.js +26 -2
- package/dist/src/core/shared-ftl.d.ts +28 -0
- package/dist/src/core/shared-ftl.js +42 -0
- package/dist/src/core/smoke-patterns.d.ts +45 -0
- package/dist/src/core/smoke-patterns.js +100 -0
- package/dist/src/core/test-stale-check.d.ts +42 -0
- package/dist/src/core/test-stale-check.js +114 -0
- package/dist/src/core/xpcshell-appdir.d.ts +143 -0
- package/dist/src/core/xpcshell-appdir.js +273 -0
- package/dist/src/errors/codes.d.ts +13 -0
- package/dist/src/errors/codes.js +13 -0
- package/dist/src/errors/run.d.ts +16 -0
- package/dist/src/errors/run.js +22 -0
- package/dist/src/types/commands/options.d.ts +64 -0
- package/dist/src/types/furnace.d.ts +39 -0
- package/dist/src/utils/process.d.ts +63 -0
- package/dist/src/utils/process.js +122 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { text } from '@clack/prompts';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
5
|
import { createDefaultFurnaceConfig, detectComposesCycles, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
6
|
import { resolveFtlChromeSubPath, tagNameToClassName } from '../../core/furnace-constants.js';
|
|
@@ -10,11 +10,14 @@ import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow,
|
|
|
10
10
|
import { isComponentInEngine } from '../../core/furnace-scanner.js';
|
|
11
11
|
import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
|
|
12
12
|
import { registerTestManifest } from '../../core/manifest-register.js';
|
|
13
|
+
import { validateSharedFtl } from '../../core/shared-ftl.js';
|
|
13
14
|
import { InvalidArgumentError } from '../../errors/base.js';
|
|
14
15
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
15
16
|
import { toError } from '../../utils/errors.js';
|
|
16
17
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
17
18
|
import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
|
|
19
|
+
import { formatDryRunPlan, formatSuccessNote } from './create-dry-run.js';
|
|
20
|
+
import { resolveCreateFeatures } from './create-features.js';
|
|
18
21
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
19
22
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
20
23
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
@@ -157,40 +160,6 @@ add_task(async function test_${underscored}_defined() {
|
|
|
157
160
|
}
|
|
158
161
|
return testFiles;
|
|
159
162
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Resolves the localized and registration feature flags for a new component.
|
|
162
|
-
* @param isInteractive - Whether interactive prompts are available
|
|
163
|
-
* @param options - CLI-provided feature flags
|
|
164
|
-
* @returns Final feature selections, or null when creation is cancelled
|
|
165
|
-
*/
|
|
166
|
-
async function resolveCreateFeatures(isInteractive, options) {
|
|
167
|
-
let localized = options.localized ?? false;
|
|
168
|
-
let register = options.register ?? true;
|
|
169
|
-
if (isInteractive && options.localized === undefined && options.register === undefined) {
|
|
170
|
-
const features = await multiselect({
|
|
171
|
-
message: 'Component features:',
|
|
172
|
-
options: [
|
|
173
|
-
{
|
|
174
|
-
value: 'localized',
|
|
175
|
-
label: 'Fluent localization (data-l10n-id)',
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
value: 'register',
|
|
179
|
-
label: 'Register in customElements.js',
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
initialValues: ['register'],
|
|
183
|
-
});
|
|
184
|
-
if (isCancel(features)) {
|
|
185
|
-
cancel('Create cancelled');
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
const selected = features;
|
|
189
|
-
localized = selected.includes('localized');
|
|
190
|
-
register = selected.includes('register');
|
|
191
|
-
}
|
|
192
|
-
return { localized, register };
|
|
193
|
-
}
|
|
194
163
|
/**
|
|
195
164
|
* Writes the scaffolded component source files to disk.
|
|
196
165
|
* @param componentDir - Destination component directory
|
|
@@ -202,20 +171,25 @@ async function resolveCreateFeatures(isInteractive, options) {
|
|
|
202
171
|
* @param journal - Optional rollback journal that snapshots files before writes
|
|
203
172
|
* @returns Relative filenames written for the component
|
|
204
173
|
*/
|
|
205
|
-
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, ftlChromeSubPath, journal) {
|
|
174
|
+
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, ftlChromeSubPath, sharedFtl, journal) {
|
|
206
175
|
await ensureDir(componentDir);
|
|
207
176
|
const files = [`${componentName}.mjs`, `${componentName}.css`];
|
|
208
177
|
const mjsPath = join(componentDir, `${componentName}.mjs`);
|
|
209
178
|
if (journal)
|
|
210
179
|
await snapshotFile(journal, mjsPath);
|
|
211
|
-
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'), ftlChromeSubPath);
|
|
180
|
+
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'), ftlChromeSubPath, sharedFtl);
|
|
212
181
|
await writeText(mjsPath, mjsContent);
|
|
213
182
|
const cssPath = join(componentDir, `${componentName}.css`);
|
|
214
183
|
if (journal)
|
|
215
184
|
await snapshotFile(journal, cssPath);
|
|
216
185
|
const cssContent = generateCssContent(getLicenseHeader(license, 'css'));
|
|
217
186
|
await writeText(cssPath, cssContent);
|
|
218
|
-
|
|
187
|
+
// Skip the per-component .ftl stub when the component participates in a
|
|
188
|
+
// pre-existing feature-scoped bundle. The shared bundle is owned
|
|
189
|
+
// elsewhere; dropping a stub here would clutter the workspace with
|
|
190
|
+
// empty files that never get packaged (furnace apply also skips copying
|
|
191
|
+
// them in this mode).
|
|
192
|
+
if (localized && !sharedFtl) {
|
|
219
193
|
const ftlPath = join(componentDir, `${componentName}.ftl`);
|
|
220
194
|
if (journal)
|
|
221
195
|
await snapshotFile(journal, ftlPath);
|
|
@@ -276,7 +250,7 @@ async function performCreateMutations(args) {
|
|
|
276
250
|
// so signal-driven rollback can clean it up even if writeComponentFiles
|
|
277
251
|
// is interrupted mid-ensureDir.
|
|
278
252
|
recordCreatedDir(journal, args.componentDir);
|
|
279
|
-
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, args.ftlChromeSubPath, journal);
|
|
253
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, args.ftlChromeSubPath, args.sharedFtl, journal);
|
|
280
254
|
const customEntry = {
|
|
281
255
|
description: args.description,
|
|
282
256
|
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
@@ -286,6 +260,9 @@ async function performCreateMutations(args) {
|
|
|
286
260
|
if (args.composes && args.composes.length > 0) {
|
|
287
261
|
customEntry.composes = args.composes;
|
|
288
262
|
}
|
|
263
|
+
if (args.sharedFtl) {
|
|
264
|
+
customEntry.sharedFtl = args.sharedFtl;
|
|
265
|
+
}
|
|
289
266
|
args.config.custom[args.componentName] = customEntry;
|
|
290
267
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
291
268
|
await writeFurnaceConfig(args.projectRoot, args.config);
|
|
@@ -374,7 +351,8 @@ function validateComposesTargets(config, componentName, composes) {
|
|
|
374
351
|
*/
|
|
375
352
|
export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
376
353
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
377
|
-
|
|
354
|
+
const isDryRun = options.dryRun ?? false;
|
|
355
|
+
intro(isDryRun ? 'Furnace Create (dry run)' : 'Furnace Create');
|
|
378
356
|
// --- Resolve component name ---
|
|
379
357
|
// Validation runs before we load/create any persisted furnace config so a
|
|
380
358
|
// failed authoring command never auto-creates furnace.json in a fresh
|
|
@@ -453,6 +431,42 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
453
431
|
// does not strand component files behind.
|
|
454
432
|
const composes = options.compose;
|
|
455
433
|
validateComposesTargets(config, componentName, composes);
|
|
434
|
+
// --- Normalize and validate --shared-ftl ahead of any writes. Shares the
|
|
435
|
+
// structural rules with furnace-config.ts so the command and the on-disk
|
|
436
|
+
// schema cannot diverge. Pass the resolved `localized` rather than a
|
|
437
|
+
// `true` literal so the validator's cross-field check stays anchored to
|
|
438
|
+
// the real feature selection — `resolveCreateFeatures` promotes localized
|
|
439
|
+
// upstream, but hard-coding `true` here would hide a regression if that
|
|
440
|
+
// promotion ever moved or dropped.
|
|
441
|
+
let sharedFtl;
|
|
442
|
+
if (options.sharedFtl !== undefined) {
|
|
443
|
+
const result = validateSharedFtl(options.sharedFtl, { localized });
|
|
444
|
+
if (!result.ok) {
|
|
445
|
+
throw new InvalidArgumentError(`--shared-ftl ${result.reason}.`, 'sharedFtl');
|
|
446
|
+
}
|
|
447
|
+
sharedFtl = result.value;
|
|
448
|
+
}
|
|
449
|
+
// Dry-run exits here — every validation that does not need a write has
|
|
450
|
+
// already run, so the plan we render reflects exactly what the real
|
|
451
|
+
// command would do. The mutation phase and its rollback journal are
|
|
452
|
+
// intentionally skipped so no furnace.json/engine state is touched.
|
|
453
|
+
if (isDryRun) {
|
|
454
|
+
const plan = formatDryRunPlan({
|
|
455
|
+
componentName,
|
|
456
|
+
localized,
|
|
457
|
+
register,
|
|
458
|
+
composes,
|
|
459
|
+
// Spread rather than assign so the key is absent when sharedFtl is
|
|
460
|
+
// undefined — the DryRunPlanInput type uses strict-optional shape.
|
|
461
|
+
...(sharedFtl !== undefined ? { sharedFtl } : {}),
|
|
462
|
+
testStyle,
|
|
463
|
+
description,
|
|
464
|
+
binaryName: forgeConfig.binaryName,
|
|
465
|
+
});
|
|
466
|
+
note(plan, componentName);
|
|
467
|
+
outro('Dry run complete (no files modified)');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
456
470
|
// All validation is done. Hand off to the transactional mutation helper
|
|
457
471
|
// so any failure restores the workspace and engine to their pre-command
|
|
458
472
|
// state via the shared rollback journal. The mutation runs under the
|
|
@@ -470,6 +484,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
470
484
|
localized,
|
|
471
485
|
register,
|
|
472
486
|
composes,
|
|
487
|
+
sharedFtl,
|
|
473
488
|
componentDir,
|
|
474
489
|
furnacePaths,
|
|
475
490
|
config,
|
|
@@ -480,29 +495,13 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
480
495
|
ftlChromeSubPath,
|
|
481
496
|
operationContext: ctx,
|
|
482
497
|
}));
|
|
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);
|
|
498
|
+
note(formatSuccessNote({
|
|
499
|
+
componentName,
|
|
500
|
+
files,
|
|
501
|
+
testFiles,
|
|
502
|
+
testStyle,
|
|
503
|
+
binaryName: forgeConfig.binaryName,
|
|
504
|
+
}), componentName);
|
|
506
505
|
outro('Component created');
|
|
507
506
|
}
|
|
508
507
|
//# 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,8 @@ 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('--shared-ftl <path>', 'Participate in an existing feature-scoped .ftl at this path (e.g. "browser/hominis-dock.ftl"); skips the per-component .ftl scaffold (implies --localized)')
|
|
85
|
+
.option('--dry-run', 'Show the planned file set and furnace.json changes without writing')
|
|
84
86
|
.action(withErrorHandling(async (name, options) => {
|
|
85
87
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
86
88
|
}));
|
|
@@ -91,6 +93,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
91
93
|
.command('create <name>')
|
|
92
94
|
.description('Scaffold a new top-level chrome document')
|
|
93
95
|
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
96
|
+
.option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
|
|
94
97
|
.action(withErrorHandling(async (name, options) => {
|
|
95
98
|
await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
|
|
96
99
|
}));
|
|
@@ -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');
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
import type { RunOptions } from '../types/commands/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exit code returned by smoke-run mode when the captured console stream
|
|
6
|
+
* produced one or more error lines that did NOT match the operator's
|
|
7
|
+
* allowlist.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SMOKE_EXIT_FAILURE: 12;
|
|
10
|
+
/**
|
|
11
|
+
* Exit code returned by smoke-run mode when the browser itself exited
|
|
12
|
+
* with a non-clean status before the smoke window elapsed — i.e. a
|
|
13
|
+
* launch-side failure we could NOT observe as a console error line
|
|
14
|
+
* (crash before console wiring, missing profile, etc.).
|
|
15
|
+
*/
|
|
16
|
+
export declare const SMOKE_LAUNCH_FAILURE: 13;
|
|
3
17
|
/**
|
|
4
18
|
* Runs the run command to launch the built browser.
|
|
5
19
|
* @param projectRoot - Root directory of the project
|
|
6
20
|
*/
|
|
7
|
-
export declare function runCommand(projectRoot: string): Promise<void>;
|
|
21
|
+
export declare function runCommand(projectRoot: string, options?: RunOptions): Promise<void>;
|
|
8
22
|
/** Registers the run command on the CLI program. */
|
|
9
23
|
export declare function registerRun(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|