@hominis/fireforge 0.14.0 → 0.15.2
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 +44 -1
- package/README.md +41 -3
- package/dist/src/commands/build.js +12 -1
- package/dist/src/commands/furnace/create-templates.d.ts +47 -0
- package/dist/src/commands/furnace/create-templates.js +135 -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.js +81 -109
- package/dist/src/commands/furnace/deploy.js +3 -3
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/commands/test.js +20 -0
- package/dist/src/core/build-prepare.js +6 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +32 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +33 -0
- package/dist/src/core/furnace-apply-ftl.js +102 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +10 -1
- package/dist/src/core/furnace-apply-helpers.js +16 -12
- package/dist/src/core/furnace-apply.js +7 -4
- package/dist/src/core/furnace-config-tokens.d.ts +17 -0
- package/dist/src/core/furnace-config-tokens.js +43 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +16 -3
- package/dist/src/core/furnace-constants.d.ts +20 -0
- package/dist/src/core/furnace-constants.js +32 -0
- package/dist/src/core/furnace-registration-ast.d.ts +13 -1
- package/dist/src/core/furnace-registration-ast.js +58 -25
- package/dist/src/core/furnace-registration.d.ts +27 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +39 -1
- package/dist/src/core/furnace-validate-helpers.js +182 -18
- package/dist/src/core/furnace-validate-registration.d.ts +8 -2
- package/dist/src/core/furnace-validate-registration.js +34 -9
- package/dist/src/core/furnace-validate.js +2 -2
- package/dist/src/core/marionette-preflight.d.ts +46 -0
- package/dist/src/core/marionette-preflight.js +260 -0
- 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.js +29 -9
- package/dist/src/types/commands/options.d.ts +16 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +19 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { multiselect, 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
|
-
import { tagNameToClassName } from '../../core/furnace-constants.js';
|
|
6
|
+
import { resolveFtlChromeSubPath, tagNameToClassName } from '../../core/furnace-constants.js';
|
|
7
7
|
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
8
8
|
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
9
9
|
import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
@@ -15,6 +15,8 @@ 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 { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
19
|
+
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
18
20
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
19
21
|
if (await furnaceConfigExists(projectRoot)) {
|
|
20
22
|
return loadFurnaceConfig(projectRoot);
|
|
@@ -46,65 +48,6 @@ function checkNameConflict(config, name) {
|
|
|
46
48
|
}
|
|
47
49
|
return undefined;
|
|
48
50
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Generates the .mjs file content for a custom component.
|
|
51
|
-
*/
|
|
52
|
-
function generateMjsContent(name, className, description, localized, header) {
|
|
53
|
-
const connectedCallback = localized
|
|
54
|
-
? `
|
|
55
|
-
connectedCallback() {
|
|
56
|
-
super.connectedCallback();
|
|
57
|
-
this.insertFTLIfNeeded("${name}.ftl");
|
|
58
|
-
}
|
|
59
|
-
`
|
|
60
|
-
: '';
|
|
61
|
-
return `${header}
|
|
62
|
-
|
|
63
|
-
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
|
64
|
-
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* ${description || name}
|
|
68
|
-
*
|
|
69
|
-
* @tagname ${name}
|
|
70
|
-
*/
|
|
71
|
-
class ${className} extends MozLitElement {
|
|
72
|
-
static properties = {};
|
|
73
|
-
|
|
74
|
-
constructor() {
|
|
75
|
-
super();
|
|
76
|
-
}
|
|
77
|
-
${connectedCallback}
|
|
78
|
-
render() {
|
|
79
|
-
return html\`
|
|
80
|
-
<link rel="stylesheet" href="chrome://global/content/elements/${name}.css" />
|
|
81
|
-
<slot></slot>
|
|
82
|
-
\`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
customElements.define("${name}", ${className});
|
|
86
|
-
`;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Generates the .css file content for a custom component.
|
|
90
|
-
*/
|
|
91
|
-
function generateCssContent(header) {
|
|
92
|
-
return `${header}
|
|
93
|
-
|
|
94
|
-
:host {
|
|
95
|
-
display: block;
|
|
96
|
-
}
|
|
97
|
-
`;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Generates the .ftl file content for a custom component.
|
|
101
|
-
*/
|
|
102
|
-
function generateFtlContent(name, header) {
|
|
103
|
-
return `${header}
|
|
104
|
-
|
|
105
|
-
## Strings for the ${name} component
|
|
106
|
-
`;
|
|
107
|
-
}
|
|
108
51
|
/**
|
|
109
52
|
* Scaffolds browser mochitest files for a newly created custom component.
|
|
110
53
|
* @param componentName - Custom element tag name
|
|
@@ -258,13 +201,13 @@ async function resolveCreateFeatures(isInteractive, options) {
|
|
|
258
201
|
* @param journal - Optional rollback journal that snapshots files before writes
|
|
259
202
|
* @returns Relative filenames written for the component
|
|
260
203
|
*/
|
|
261
|
-
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, journal) {
|
|
204
|
+
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, ftlChromeSubPath, journal) {
|
|
262
205
|
await ensureDir(componentDir);
|
|
263
206
|
const files = [`${componentName}.mjs`, `${componentName}.css`];
|
|
264
207
|
const mjsPath = join(componentDir, `${componentName}.mjs`);
|
|
265
208
|
if (journal)
|
|
266
209
|
await snapshotFile(journal, mjsPath);
|
|
267
|
-
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'));
|
|
210
|
+
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'), ftlChromeSubPath);
|
|
268
211
|
await writeText(mjsPath, mjsContent);
|
|
269
212
|
const cssPath = join(componentDir, `${componentName}.css`);
|
|
270
213
|
if (journal)
|
|
@@ -303,7 +246,7 @@ async function performCreateMutations(args) {
|
|
|
303
246
|
// so signal-driven rollback can clean it up even if writeComponentFiles
|
|
304
247
|
// is interrupted mid-ensureDir.
|
|
305
248
|
recordCreatedDir(journal, args.componentDir);
|
|
306
|
-
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, journal);
|
|
249
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, args.ftlChromeSubPath, journal);
|
|
307
250
|
const customEntry = {
|
|
308
251
|
description: args.description,
|
|
309
252
|
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
@@ -320,6 +263,10 @@ async function performCreateMutations(args) {
|
|
|
320
263
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
321
264
|
testFiles.push(...scafFiles);
|
|
322
265
|
}
|
|
266
|
+
if (args.xpcshellTests) {
|
|
267
|
+
const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
268
|
+
testFiles.push(...xpcshellFiles);
|
|
269
|
+
}
|
|
323
270
|
}
|
|
324
271
|
catch (error) {
|
|
325
272
|
try {
|
|
@@ -333,6 +280,58 @@ async function performCreateMutations(args) {
|
|
|
333
280
|
}
|
|
334
281
|
return { files, testFiles };
|
|
335
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Prompts the operator for a description when the command is interactive and
|
|
285
|
+
* the operator did not pass `-d`. Returns the resolved description string.
|
|
286
|
+
*/
|
|
287
|
+
async function resolveDescription(isInteractive, options) {
|
|
288
|
+
let description = options.description ?? '';
|
|
289
|
+
if (!description && isInteractive) {
|
|
290
|
+
const descResult = await text({
|
|
291
|
+
message: 'Description (optional):',
|
|
292
|
+
placeholder: 'A brief description of the component',
|
|
293
|
+
});
|
|
294
|
+
if (!isCancel(descResult)) {
|
|
295
|
+
description = String(descResult);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return description;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Validates the `--compose` targets against registered components and runs
|
|
302
|
+
* cycle detection if the new component is introduced into the graph. Throws
|
|
303
|
+
* on any failure; returns when the graph is clean.
|
|
304
|
+
*/
|
|
305
|
+
function validateComposesTargets(config, componentName, composes) {
|
|
306
|
+
if (!composes || composes.length === 0)
|
|
307
|
+
return;
|
|
308
|
+
const known = new Set([
|
|
309
|
+
...config.stock,
|
|
310
|
+
...Object.keys(config.overrides),
|
|
311
|
+
...Object.keys(config.custom),
|
|
312
|
+
]);
|
|
313
|
+
for (const tag of composes) {
|
|
314
|
+
if (tag === componentName) {
|
|
315
|
+
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
316
|
+
}
|
|
317
|
+
if (!known.has(tag)) {
|
|
318
|
+
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
319
|
+
'The referenced component must be registered as stock, override, or custom.');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Check for cycles that would be introduced by adding this component.
|
|
323
|
+
const tempCustom = {
|
|
324
|
+
...config.custom,
|
|
325
|
+
[componentName]: {
|
|
326
|
+
description: '',
|
|
327
|
+
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
328
|
+
register: true,
|
|
329
|
+
localized: false,
|
|
330
|
+
composes,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
detectComposesCycles(tempCustom);
|
|
334
|
+
}
|
|
336
335
|
/**
|
|
337
336
|
* Runs the furnace create command to scaffold a new custom component.
|
|
338
337
|
* @param projectRoot - Root directory of the project
|
|
@@ -394,16 +393,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
394
393
|
warn(`Name "${componentName}" does not start with the configured prefix "${config.componentPrefix}".`);
|
|
395
394
|
}
|
|
396
395
|
// --- Resolve description ---
|
|
397
|
-
|
|
398
|
-
if (!description && isInteractive) {
|
|
399
|
-
const descResult = await text({
|
|
400
|
-
message: 'Description (optional):',
|
|
401
|
-
placeholder: 'A brief description of the component',
|
|
402
|
-
});
|
|
403
|
-
if (!isCancel(descResult)) {
|
|
404
|
-
description = String(descResult);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
396
|
+
const description = await resolveDescription(isInteractive, options);
|
|
407
397
|
// --- Resolve features ---
|
|
408
398
|
const featureSelection = await resolveCreateFeatures(isInteractive, options);
|
|
409
399
|
if (!featureSelection) {
|
|
@@ -411,12 +401,14 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
411
401
|
}
|
|
412
402
|
const { localized, register } = featureSelection;
|
|
413
403
|
// --with-tests writes files under engine/browser/base/content/test/ and
|
|
414
|
-
// registers them in moz.build.
|
|
415
|
-
//
|
|
416
|
-
//
|
|
404
|
+
// registers them in moz.build. --xpcshell is the equivalent for forks
|
|
405
|
+
// without a tabbrowser (storage-layer code). Guard against a missing
|
|
406
|
+
// engine now rather than letting the scaffolders fabricate a partial
|
|
407
|
+
// engine tree with ensureDir.
|
|
417
408
|
const withTests = options.withTests ?? false;
|
|
418
|
-
|
|
419
|
-
|
|
409
|
+
const xpcshellTests = options.xpcshell ?? false;
|
|
410
|
+
if ((withTests || xpcshellTests) && !(await pathExists(paths.engine))) {
|
|
411
|
+
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests or --xpcshell.', componentName);
|
|
420
412
|
}
|
|
421
413
|
// --- Generate component files ---
|
|
422
414
|
const className = tagNameToClassName(componentName);
|
|
@@ -428,39 +420,16 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
428
420
|
// --- Validate --compose targets BEFORE any writes so a failed validation
|
|
429
421
|
// does not strand component files behind.
|
|
430
422
|
const composes = options.compose;
|
|
431
|
-
|
|
432
|
-
const known = new Set([
|
|
433
|
-
...config.stock,
|
|
434
|
-
...Object.keys(config.overrides),
|
|
435
|
-
...Object.keys(config.custom),
|
|
436
|
-
]);
|
|
437
|
-
for (const tag of composes) {
|
|
438
|
-
if (tag === componentName) {
|
|
439
|
-
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
440
|
-
}
|
|
441
|
-
if (!known.has(tag)) {
|
|
442
|
-
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
443
|
-
'The referenced component must be registered as stock, override, or custom.');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
// Check for cycles that would be introduced by adding this component.
|
|
447
|
-
const tempCustom = {
|
|
448
|
-
...config.custom,
|
|
449
|
-
[componentName]: {
|
|
450
|
-
description: '',
|
|
451
|
-
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
452
|
-
register: true,
|
|
453
|
-
localized: false,
|
|
454
|
-
composes,
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
detectComposesCycles(tempCustom);
|
|
458
|
-
}
|
|
423
|
+
validateComposesTargets(config, componentName, composes);
|
|
459
424
|
// All validation is done. Hand off to the transactional mutation helper
|
|
460
425
|
// so any failure restores the workspace and engine to their pre-command
|
|
461
426
|
// state via the shared rollback journal. The mutation runs under the
|
|
462
427
|
// furnace-wide lock and is registered with the global SIGINT/SIGTERM
|
|
463
428
|
// rollback pathway.
|
|
429
|
+
// Derive the FTL chrome sub-path from the configured ftlBasePath so the
|
|
430
|
+
// generated `.mjs` calls `insertFTLIfNeeded` at a URI that actually matches
|
|
431
|
+
// the locale jar.mn entry `furnace apply` will write.
|
|
432
|
+
const ftlChromeSubPath = resolveFtlChromeSubPath(config.ftlBasePath);
|
|
464
433
|
const { files, testFiles } = await runFurnaceMutation(projectRoot, 'create-rollback', (ctx) => performCreateMutations({
|
|
465
434
|
projectRoot,
|
|
466
435
|
componentName,
|
|
@@ -476,15 +445,18 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
476
445
|
paths,
|
|
477
446
|
license,
|
|
478
447
|
withTests,
|
|
448
|
+
xpcshellTests,
|
|
449
|
+
ftlChromeSubPath,
|
|
479
450
|
operationContext: ctx,
|
|
480
451
|
}));
|
|
481
452
|
// --- Success ---
|
|
482
453
|
let noteParts = `Files created in components/custom/${componentName}/:\n` +
|
|
483
454
|
files.map((f) => ` ${f}`).join('\n');
|
|
484
455
|
if (testFiles.length > 0) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
456
|
+
const testRoot = xpcshellTests
|
|
457
|
+
? `engine/browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${componentName}/`
|
|
458
|
+
: `engine/browser/base/content/test/${forgeConfig.binaryName}/`;
|
|
459
|
+
noteParts += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
|
|
488
460
|
}
|
|
489
461
|
noteParts +=
|
|
490
462
|
'\n\n' +
|
|
@@ -162,7 +162,7 @@ async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
|
|
|
162
162
|
* @param isDryRun - Whether file writes should be skipped
|
|
163
163
|
* @returns Apply result for the named component, or `stock` for stock-only entries
|
|
164
164
|
*/
|
|
165
|
-
async function applyNamedComponent(name, engineDir, furnacePaths, config, ftlDir, isDryRun, operationContext, projectRoot) {
|
|
165
|
+
async function applyNamedComponent(name, engineDir, furnacePaths, config, ftlDir, isDryRun, operationContext, projectRoot, markerComment) {
|
|
166
166
|
const rollbackJournal = isDryRun ? undefined : createRollbackJournal();
|
|
167
167
|
if (rollbackJournal && operationContext) {
|
|
168
168
|
operationContext.registerJournal(rollbackJournal);
|
|
@@ -201,7 +201,7 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, ftlDir
|
|
|
201
201
|
throw new FurnaceError(`Component directory not found: components/custom/${name}`, name);
|
|
202
202
|
}
|
|
203
203
|
try {
|
|
204
|
-
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, isDryRun, rollbackJournal);
|
|
204
|
+
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, isDryRun, rollbackJournal, markerComment !== undefined ? { markerComment } : {});
|
|
205
205
|
if (isDryRun && actions) {
|
|
206
206
|
result.actions = actions;
|
|
207
207
|
}
|
|
@@ -317,7 +317,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
317
317
|
// `furnace deploy` runs only contend on the actual mutation.
|
|
318
318
|
const applyOutcome = await runFurnaceMutation(projectRoot, 'deploy-rollback', async (ctx) => {
|
|
319
319
|
if (name) {
|
|
320
|
-
const namedApplyResult = await applyNamedComponent(name, paths.engine, furnacePaths, config, ftlDir, isDryRun, ctx, projectRoot);
|
|
320
|
+
const namedApplyResult = await applyNamedComponent(name, paths.engine, furnacePaths, config, ftlDir, isDryRun, ctx, projectRoot, forgeConfig.markerComment);
|
|
321
321
|
if (namedApplyResult === 'stock') {
|
|
322
322
|
return { kind: 'stock' };
|
|
323
323
|
}
|
|
@@ -72,6 +72,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
72
72
|
.option('--localized', 'Include Fluent l10n support')
|
|
73
73
|
.option('--no-register', 'Skip customElements.js registration')
|
|
74
74
|
.option('--with-tests', 'Scaffold Mochitest directory and register in moz.build')
|
|
75
|
+
.option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser)')
|
|
75
76
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
76
77
|
.action(withErrorHandling(async (name, options) => {
|
|
77
78
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
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
|
+
import { reportMarionettePreflight, runMarionettePreflight } from '../core/marionette-preflight.js';
|
|
6
7
|
import { GeneralError } from '../errors/base.js';
|
|
7
8
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
8
9
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -117,6 +118,24 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
117
118
|
s.stop('Build complete');
|
|
118
119
|
info('');
|
|
119
120
|
}
|
|
121
|
+
// `--doctor` runs a short marionette handshake probe. When test paths are
|
|
122
|
+
// supplied the probe gates the mach test invocation (a FAIL bails out). When
|
|
123
|
+
// no paths are supplied this is the only step — it's the fastest way to tell
|
|
124
|
+
// marionette-wedged apart from test-discovery-failure.
|
|
125
|
+
if (options.doctor) {
|
|
126
|
+
info('Running marionette preflight...');
|
|
127
|
+
const preflight = await runMarionettePreflight(paths.engine);
|
|
128
|
+
reportMarionettePreflight(preflight);
|
|
129
|
+
if (testPaths.length === 0) {
|
|
130
|
+
if (!preflight.ok) {
|
|
131
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!preflight.ok) {
|
|
136
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
120
139
|
// Normalize test paths (strip engine/ prefix if present)
|
|
121
140
|
const normalizedPaths = testPaths.map(normalizeTestPath);
|
|
122
141
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
@@ -149,6 +168,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
149
168
|
.description('Run tests via mach test')
|
|
150
169
|
.option('--headless', 'Run tests in headless mode')
|
|
151
170
|
.option('--build', 'Run incremental UI build before testing')
|
|
171
|
+
.option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
|
|
152
172
|
.action(withErrorHandling(async (paths, options) => {
|
|
153
173
|
await testCommand(getProjectRoot(), paths, pickDefined(options));
|
|
154
174
|
}));
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { FurnaceError } from '../errors/furnace.js';
|
|
7
7
|
import { pathExists } from '../utils/fs.js';
|
|
8
|
-
import { spinner, warn } from '../utils/logger.js';
|
|
8
|
+
import { info, spinner, warn } from '../utils/logger.js';
|
|
9
9
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
10
10
|
import { applyAllComponents } from './furnace-apply.js';
|
|
11
11
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
@@ -95,7 +95,12 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
|
|
|
95
95
|
throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
96
96
|
}
|
|
97
97
|
if (furnaceApplied > 0) {
|
|
98
|
+
const appliedNames = result.applied.map((entry) => entry.name).join(', ');
|
|
98
99
|
furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
|
|
100
|
+
// Loud banner: the build operator needs to see that engine/ was
|
|
101
|
+
// updated before this build, otherwise a silent re-apply is
|
|
102
|
+
// indistinguishable from a build that shipped stale components.
|
|
103
|
+
info(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
|
|
99
104
|
}
|
|
100
105
|
else {
|
|
101
106
|
furnaceSpinner.stop('Components up to date');
|
|
@@ -17,9 +17,9 @@ export declare const CONFIGS_DIR = "configs";
|
|
|
17
17
|
/** Name of the source directory */
|
|
18
18
|
export declare const SRC_DIR = "src";
|
|
19
19
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
20
|
-
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint"];
|
|
20
|
+
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "markerComment"];
|
|
21
21
|
/** Supported config paths that can be read or set without --force. */
|
|
22
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist"];
|
|
22
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist", "markerComment"];
|
|
23
23
|
/**
|
|
24
24
|
* Gets all project paths based on a root directory.
|
|
25
25
|
* @param root - Root directory of the project
|
|
@@ -28,6 +28,7 @@ export const SUPPORTED_CONFIG_ROOT_KEYS = [
|
|
|
28
28
|
'license',
|
|
29
29
|
'wire',
|
|
30
30
|
'patchLint',
|
|
31
|
+
'markerComment',
|
|
31
32
|
];
|
|
32
33
|
/** Supported config paths that can be read or set without --force. */
|
|
33
34
|
export const SUPPORTED_CONFIG_PATHS = [
|
|
@@ -46,6 +47,7 @@ export const SUPPORTED_CONFIG_PATHS = [
|
|
|
46
47
|
'patchLint',
|
|
47
48
|
'patchLint.checkJs',
|
|
48
49
|
'patchLint.rawColorAllowlist',
|
|
50
|
+
'markerComment',
|
|
49
51
|
];
|
|
50
52
|
/**
|
|
51
53
|
* Gets all project paths based on a root directory.
|
|
@@ -121,6 +121,11 @@ export function validateConfig(data) {
|
|
|
121
121
|
}
|
|
122
122
|
config.license = licenseRaw;
|
|
123
123
|
}
|
|
124
|
+
// Marker comment — appended to lines FireForge writes into upstream files.
|
|
125
|
+
const markerComment = parseMarkerComment(rec.raw('markerComment'));
|
|
126
|
+
if (markerComment !== undefined) {
|
|
127
|
+
config.markerComment = markerComment;
|
|
128
|
+
}
|
|
124
129
|
// PatchLint
|
|
125
130
|
const patchLintRec = optionalConfigObject(rec, 'patchLint');
|
|
126
131
|
if (patchLintRec) {
|
|
@@ -167,6 +172,33 @@ function optionalConfigString(rec, key, label) {
|
|
|
167
172
|
}
|
|
168
173
|
return value;
|
|
169
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Validates a raw `markerComment` value. Rejected values: non-strings, empty
|
|
177
|
+
* strings, surrounding whitespace (ambiguous format), newlines (would break
|
|
178
|
+
* source formatting), and `*/` (would terminate an enclosing block comment
|
|
179
|
+
* downstream). Control characters are rejected for the same reason.
|
|
180
|
+
*/
|
|
181
|
+
function parseMarkerComment(raw) {
|
|
182
|
+
if (raw === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
if (typeof raw !== 'string') {
|
|
185
|
+
throw new ConfigError('Config field "markerComment" must be a string');
|
|
186
|
+
}
|
|
187
|
+
if (raw.trim() === '') {
|
|
188
|
+
throw new ConfigError('Config field "markerComment" must not be empty');
|
|
189
|
+
}
|
|
190
|
+
if (raw !== raw.trim()) {
|
|
191
|
+
throw new ConfigError('Config field "markerComment" must not have leading or trailing whitespace');
|
|
192
|
+
}
|
|
193
|
+
if (/[\n\r]/.test(raw) || raw.includes('*/')) {
|
|
194
|
+
throw new ConfigError('Config field "markerComment" must not contain newlines or "*/"');
|
|
195
|
+
}
|
|
196
|
+
// eslint-disable-next-line no-control-regex -- intentionally rejecting control chars
|
|
197
|
+
if (/[\x00-\x1f]/.test(raw)) {
|
|
198
|
+
throw new ConfigError('Config field "markerComment" must not contain control characters');
|
|
199
|
+
}
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
170
202
|
function optionalConfigObject(rec, key) {
|
|
171
203
|
const value = rec.raw(key);
|
|
172
204
|
if (value === undefined)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.ftl` apply/undeploy helpers for custom components. Extracted from
|
|
3
|
+
* `furnace-apply-helpers.ts` so the main helper module stays under the
|
|
4
|
+
* per-file LOC budget.
|
|
5
|
+
*
|
|
6
|
+
* Every helper here degrades gracefully: if the locale jar.mn is missing or
|
|
7
|
+
* the FTL tree is non-standard, apply logs a `stepError` rather than
|
|
8
|
+
* aborting the whole command. Missing jar.mn on a fork without a locale
|
|
9
|
+
* package should not block a working `.mjs`/`.css` from shipping.
|
|
10
|
+
*/
|
|
11
|
+
import type { DryRunAction, StepError } from '../types/furnace.js';
|
|
12
|
+
import { type RollbackJournal } from './furnace-rollback.js';
|
|
13
|
+
/**
|
|
14
|
+
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
15
|
+
* in the locale jar.mn.
|
|
16
|
+
*
|
|
17
|
+
* Failure modes (missing jar.mn, regex write error) are captured as
|
|
18
|
+
* stepErrors rather than thrown — a well-formed `.mjs`/`.css` must never be
|
|
19
|
+
* blocked by a broken locale path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyCustomFtlFile(engineDir: string, name: string, componentDir: string, ftlDir: string, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns a dry-run action for registering a locale jar.mn entry for the
|
|
24
|
+
* `.ftl` that `applyCustomFtlFile` would write. `undefined` when the FTL
|
|
25
|
+
* tree does not expose a locale jar.mn we can confidently name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function describeLocaleFtlJarMnRegistration(name: string, ftlDir: string, ftlFile: string): DryRunAction | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Drops the locale jar.mn entry for `fileName` when it's a `.ftl` whose
|
|
30
|
+
* source workspace file has been deleted. Idempotent — absent entries are a
|
|
31
|
+
* no-op.
|
|
32
|
+
*/
|
|
33
|
+
export declare function removeCustomFtlJarMnEntry(engineDir: string, fileName: string, ftlDir: string, rollbackJournal?: RollbackJournal): Promise<void>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `.ftl` apply/undeploy helpers for custom components. Extracted from
|
|
4
|
+
* `furnace-apply-helpers.ts` so the main helper module stays under the
|
|
5
|
+
* per-file LOC budget.
|
|
6
|
+
*
|
|
7
|
+
* Every helper here degrades gracefully: if the locale jar.mn is missing or
|
|
8
|
+
* the FTL tree is non-standard, apply logs a `stepError` rather than
|
|
9
|
+
* aborting the whole command. Missing jar.mn on a fork without a locale
|
|
10
|
+
* package should not block a working `.mjs`/`.css` from shipping.
|
|
11
|
+
*/
|
|
12
|
+
import { join, relative } from 'node:path';
|
|
13
|
+
import { toError } from '../utils/errors.js';
|
|
14
|
+
import { copyFile, pathExists } from '../utils/fs.js';
|
|
15
|
+
import { resolveFtlChromeSubPath, resolveFtlLocaleJarMnPath } from './furnace-constants.js';
|
|
16
|
+
import { addLocaleFtlJarMnEntry, removeLocaleFtlJarMnEntry } from './furnace-registration.js';
|
|
17
|
+
import { snapshotFile } from './furnace-rollback.js';
|
|
18
|
+
/**
|
|
19
|
+
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
20
|
+
* in the locale jar.mn.
|
|
21
|
+
*
|
|
22
|
+
* Failure modes (missing jar.mn, regex write error) are captured as
|
|
23
|
+
* stepErrors rather than thrown — a well-formed `.mjs`/`.css` must never be
|
|
24
|
+
* blocked by a broken locale path.
|
|
25
|
+
*/
|
|
26
|
+
export async function applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal) {
|
|
27
|
+
const ftlFile = `${name}.ftl`;
|
|
28
|
+
const ftlSrc = join(componentDir, ftlFile);
|
|
29
|
+
if (!(await pathExists(ftlSrc)))
|
|
30
|
+
return;
|
|
31
|
+
const ftlDest = join(engineDir, ftlDir, ftlFile);
|
|
32
|
+
if (rollbackJournal) {
|
|
33
|
+
await snapshotFile(rollbackJournal, ftlDest);
|
|
34
|
+
}
|
|
35
|
+
await copyFile(ftlSrc, ftlDest);
|
|
36
|
+
affectedPaths.push(relative(engineDir, ftlDest));
|
|
37
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
38
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
39
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
40
|
+
return;
|
|
41
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
42
|
+
if (!(await pathExists(localeJarAbs))) {
|
|
43
|
+
stepErrors.push({
|
|
44
|
+
step: 'locale jar.mn registration',
|
|
45
|
+
error: `Locale jar.mn not found at ${localeJarRel}; component "${name}" ships without a chrome URI for ${ftlFile}. Add the file manually or set furnace.json "ftlBasePath" to a tree that owns a jar.mn.`,
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (rollbackJournal) {
|
|
51
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
52
|
+
}
|
|
53
|
+
const inserted = await addLocaleFtlJarMnEntry(engineDir, localeJarRel, name, chromeSubPath);
|
|
54
|
+
if (inserted > 0) {
|
|
55
|
+
affectedPaths.push(localeJarRel);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
stepErrors.push({
|
|
60
|
+
step: 'locale jar.mn registration',
|
|
61
|
+
error: toError(error).message,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns a dry-run action for registering a locale jar.mn entry for the
|
|
67
|
+
* `.ftl` that `applyCustomFtlFile` would write. `undefined` when the FTL
|
|
68
|
+
* tree does not expose a locale jar.mn we can confidently name.
|
|
69
|
+
*/
|
|
70
|
+
export function describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile) {
|
|
71
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
72
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
73
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
return {
|
|
76
|
+
component: name,
|
|
77
|
+
action: 'register-jar',
|
|
78
|
+
description: `Register ${chromeSubPath}/${ftlFile} in ${localeJarRel}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Drops the locale jar.mn entry for `fileName` when it's a `.ftl` whose
|
|
83
|
+
* source workspace file has been deleted. Idempotent — absent entries are a
|
|
84
|
+
* no-op.
|
|
85
|
+
*/
|
|
86
|
+
export async function removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, rollbackJournal) {
|
|
87
|
+
if (!fileName.endsWith('.ftl'))
|
|
88
|
+
return;
|
|
89
|
+
const tagName = fileName.slice(0, -'.ftl'.length);
|
|
90
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
91
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
92
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
93
|
+
return;
|
|
94
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
95
|
+
if (!(await pathExists(localeJarAbs)))
|
|
96
|
+
return;
|
|
97
|
+
if (rollbackJournal) {
|
|
98
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
99
|
+
}
|
|
100
|
+
await removeLocaleFtlJarMnEntry(engineDir, localeJarRel, tagName, chromeSubPath);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=furnace-apply-ftl.js.map
|
|
@@ -90,8 +90,17 @@ export declare function hasOverrideEngineDrift(engineDir: string, componentDir:
|
|
|
90
90
|
* the validate command.
|
|
91
91
|
*/
|
|
92
92
|
export declare function hasCustomEngineDrift(root: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string): Promise<boolean>;
|
|
93
|
+
/** Extra knobs threaded into `applyCustomComponent` from the project config. */
|
|
94
|
+
export interface CustomApplyOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Trailing project marker appended to inserted `customElements.js` entries
|
|
97
|
+
* (e.g. `"MYBROWSER"` emits ` // MYBROWSER:` on each line). Mirrors the
|
|
98
|
+
* `markerComment` field in fireforge.json.
|
|
99
|
+
*/
|
|
100
|
+
markerComment?: string;
|
|
101
|
+
}
|
|
93
102
|
/** Applies a custom component into the engine tree and captures registration step errors. */
|
|
94
|
-
export declare function applyCustomComponent(engineDir: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string, dryRun?: boolean, rollbackJournal?: RollbackJournal): Promise<{
|
|
103
|
+
export declare function applyCustomComponent(engineDir: string, name: string, componentDir: string, config: CustomComponentConfig, ftlDir: string, dryRun?: boolean, rollbackJournal?: RollbackJournal, applyOptions?: CustomApplyOptions): Promise<{
|
|
95
104
|
affectedPaths: string[];
|
|
96
105
|
stepErrors: StepError[];
|
|
97
106
|
actions?: DryRunAction[];
|