@hominis/fireforge 0.19.6 → 0.21.0
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 -0
- package/README.md +22 -4
- package/dist/src/commands/build.js +19 -1
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/download.js +188 -185
- package/dist/src/commands/export-flow.js +2 -13
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +13 -0
- package/dist/src/commands/furnace/chrome-doc-remove.js +142 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +32 -0
- package/dist/src/commands/furnace/chrome-doc.js +113 -1
- package/dist/src/commands/furnace/create-validation.d.ts +6 -0
- package/dist/src/commands/furnace/create-validation.js +59 -0
- package/dist/src/commands/furnace/create.js +13 -88
- package/dist/src/commands/furnace/index.js +14 -0
- package/dist/src/commands/furnace/refresh.js +11 -2
- package/dist/src/commands/furnace/remove-state.d.ts +5 -0
- package/dist/src/commands/furnace/remove-state.js +14 -0
- package/dist/src/commands/furnace/remove.js +33 -45
- package/dist/src/commands/furnace/rename-browser-test.d.ts +2 -0
- package/dist/src/commands/furnace/rename-browser-test.js +28 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +13 -0
- package/dist/src/commands/furnace/rename-helpers.js +42 -0
- package/dist/src/commands/furnace/rename.js +29 -48
- package/dist/src/commands/status.js +22 -3
- package/dist/src/commands/test.js +3 -0
- package/dist/src/commands/watch.js +9 -2
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +5 -0
- package/dist/src/core/config.js +11 -7
- package/dist/src/core/file-lock.js +2 -2
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +43 -17
- package/dist/src/core/firefox-download.js +12 -4
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +2 -2
- package/dist/src/core/furnace-config.js +4 -0
- package/dist/src/core/furnace-refresh.js +16 -5
- package/dist/src/core/patch-lint-imports.d.ts +5 -0
- package/dist/src/core/patch-lint-imports.js +68 -0
- package/dist/src/core/patch-lint.js +2 -3
- package/dist/src/types/config.d.ts +2 -0
- package/dist/src/utils/fs.d.ts +5 -0
- package/dist/src/utils/fs.js +54 -1
- package/dist/src/utils/process.js +4 -1
- package/package.json +2 -2
|
@@ -35,7 +35,39 @@ export interface FurnaceChromeDocCreateOptions {
|
|
|
35
35
|
* because the owning moz.build depends on the fork's layout.
|
|
36
36
|
*/
|
|
37
37
|
withTests?: boolean;
|
|
38
|
+
/** Print the scaffold plan without writing files. */
|
|
39
|
+
dryRun?: boolean;
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates a chrome-doc name. Lowercase ASCII, optional hyphens, no
|
|
43
|
+
* leading digit — the name is used verbatim in CSS selectors, jar.mn
|
|
44
|
+
* entries, FTL keys, and file basenames, so anything outside that
|
|
45
|
+
* character set would break at least one downstream consumer.
|
|
46
|
+
* @param name Chrome-doc name (file basename without extension).
|
|
47
|
+
* @throws InvalidArgumentError when the name is unusable.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateChromeDocName(name: string): void;
|
|
50
|
+
export interface ChromeDocPlan {
|
|
51
|
+
files: string[];
|
|
52
|
+
dirs: string[];
|
|
53
|
+
jarEntries: Array<{
|
|
54
|
+
file: string;
|
|
55
|
+
entry: string;
|
|
56
|
+
present: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
localeWildcardCapturesFtl: boolean;
|
|
59
|
+
testDir?: string;
|
|
60
|
+
testFiles: string[];
|
|
61
|
+
}
|
|
62
|
+
/** Builds the shared create/remove plan for a top-level chrome document. */
|
|
63
|
+
export declare function buildChromeDocPlan(args: {
|
|
64
|
+
engineDir: string;
|
|
65
|
+
name: string;
|
|
66
|
+
withTests: boolean;
|
|
67
|
+
binaryName: string;
|
|
68
|
+
validateCreateConflicts?: boolean;
|
|
69
|
+
includeLocaleEntryWhenWildcard?: boolean;
|
|
70
|
+
}): Promise<ChromeDocPlan>;
|
|
39
71
|
/**
|
|
40
72
|
* Runs `furnace chrome-doc create <name>`.
|
|
41
73
|
* @param projectRoot Root directory of the project.
|
|
@@ -37,7 +37,7 @@ const CHROME_DOC_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
37
37
|
* @param name Chrome-doc name (file basename without extension).
|
|
38
38
|
* @throws InvalidArgumentError when the name is unusable.
|
|
39
39
|
*/
|
|
40
|
-
function validateChromeDocName(name) {
|
|
40
|
+
export function validateChromeDocName(name) {
|
|
41
41
|
if (!name.trim()) {
|
|
42
42
|
throw new InvalidArgumentError('Chrome-doc name is required', 'name');
|
|
43
43
|
}
|
|
@@ -45,6 +45,106 @@ function validateChromeDocName(name) {
|
|
|
45
45
|
throw new InvalidArgumentError('Chrome-doc name must be lowercase ASCII, may contain hyphens, and must not start with a digit (e.g. mybrowser, about-onboarding).', 'name');
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/** Builds the shared create/remove plan for a top-level chrome document. */
|
|
49
|
+
export async function buildChromeDocPlan(args) {
|
|
50
|
+
const contentDir = join(args.engineDir, 'browser/base/content');
|
|
51
|
+
const sharedThemeDir = join(args.engineDir, 'browser/themes/shared');
|
|
52
|
+
const localeDir = join(args.engineDir, 'browser/locales/en-US/browser');
|
|
53
|
+
const dirs = [contentDir, sharedThemeDir, localeDir];
|
|
54
|
+
const xhtmlPath = join(contentDir, `${args.name}.xhtml`);
|
|
55
|
+
if (args.validateCreateConflicts && (await pathExists(xhtmlPath))) {
|
|
56
|
+
throw new FurnaceError(`${args.name}.xhtml already exists at ${xhtmlPath}. Remove it or choose a different name.`);
|
|
57
|
+
}
|
|
58
|
+
const jarMnPath = join(args.engineDir, 'browser/base/jar.mn');
|
|
59
|
+
const jarIncMnPath = join(args.engineDir, 'browser/themes/shared/jar.inc.mn');
|
|
60
|
+
const localeJarMnPath = join(args.engineDir, 'browser/locales/jar.mn');
|
|
61
|
+
for (const requiredJarPath of [jarMnPath, jarIncMnPath, localeJarMnPath]) {
|
|
62
|
+
if (!(await pathExists(requiredJarPath))) {
|
|
63
|
+
throw new FurnaceError(`Required jar file ${requiredJarPath} does not exist; cannot register chrome-doc entry. Check that the fork's engine layout matches the expected browser/ and locales/ tree.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const [jarMn, jarIncMn, localeJarMn] = await Promise.all([
|
|
67
|
+
readText(jarMnPath),
|
|
68
|
+
readText(jarIncMnPath),
|
|
69
|
+
readText(localeJarMnPath),
|
|
70
|
+
]);
|
|
71
|
+
const localeWildcardCapturesFtl = localesFtlWildcardCapturesScaffoldedName(localeJarMn);
|
|
72
|
+
const jarEntries = [];
|
|
73
|
+
for (const entry of jarMnEntriesForChromeDoc(args.name)) {
|
|
74
|
+
jarEntries.push({
|
|
75
|
+
file: 'browser/base/jar.mn',
|
|
76
|
+
entry,
|
|
77
|
+
present: jarMn.includes(entry),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const cssEntry = jarIncMnEntryForChromeDoc(args.name);
|
|
81
|
+
jarEntries.push({
|
|
82
|
+
file: 'browser/themes/shared/jar.inc.mn',
|
|
83
|
+
entry: cssEntry,
|
|
84
|
+
present: jarIncMn.includes(cssEntry),
|
|
85
|
+
});
|
|
86
|
+
if (!localeWildcardCapturesFtl || args.includeLocaleEntryWhenWildcard) {
|
|
87
|
+
const ftlEntry = localeJarMnEntryForChromeDoc(args.name);
|
|
88
|
+
jarEntries.push({
|
|
89
|
+
file: 'browser/locales/jar.mn',
|
|
90
|
+
entry: ftlEntry,
|
|
91
|
+
present: localeJarMn.includes(ftlEntry),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const files = [
|
|
95
|
+
`browser/base/content/${args.name}.xhtml`,
|
|
96
|
+
`browser/base/content/${args.name}.js`,
|
|
97
|
+
`browser/themes/shared/${args.name}-chrome.css`,
|
|
98
|
+
`browser/locales/en-US/browser/${args.name}.ftl`,
|
|
99
|
+
];
|
|
100
|
+
const testFiles = [];
|
|
101
|
+
let testDir;
|
|
102
|
+
if (args.withTests) {
|
|
103
|
+
const testParentDir = `${args.binaryName}-xpcshell`;
|
|
104
|
+
testDir = `browser/base/content/test/${testParentDir}/${args.name}`;
|
|
105
|
+
testFiles.push(`${testDir}/${chromeDocPackagingTestFileName(args.name)}`, `${testDir}/xpcshell.toml`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
files,
|
|
109
|
+
dirs,
|
|
110
|
+
jarEntries,
|
|
111
|
+
localeWildcardCapturesFtl,
|
|
112
|
+
...(testDir ? { testDir } : {}),
|
|
113
|
+
testFiles,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function renderChromeDocCreateDryRun(name, plan) {
|
|
117
|
+
const dirLines = plan.dirs.map((dir) => ` ${dir}`);
|
|
118
|
+
const jarLines = plan.jarEntries.map(({ file, entry, present }) => ` engine/${file}: ${present ? 'already present' : 'would add'} ${entry.trim()}`);
|
|
119
|
+
const localeLine = plan.localeWildcardCapturesFtl
|
|
120
|
+
? [
|
|
121
|
+
'',
|
|
122
|
+
'Locale jar.mn already has a [localization] wildcard that captures the FTL;',
|
|
123
|
+
'no per-file locale entry would be added.',
|
|
124
|
+
]
|
|
125
|
+
: [];
|
|
126
|
+
const testLines = plan.testFiles.length > 0
|
|
127
|
+
? [
|
|
128
|
+
'',
|
|
129
|
+
'Would create xpcshell packaging test files:',
|
|
130
|
+
...plan.testFiles.map((f) => ` engine/${f}`),
|
|
131
|
+
]
|
|
132
|
+
: [];
|
|
133
|
+
return [
|
|
134
|
+
`[dry-run] Chrome document "${name}" scaffold plan`,
|
|
135
|
+
'',
|
|
136
|
+
'Directories checked/created as needed:',
|
|
137
|
+
...dirLines,
|
|
138
|
+
'',
|
|
139
|
+
'Would create source files:',
|
|
140
|
+
...plan.files.map((f) => ` engine/${f}`),
|
|
141
|
+
...testLines,
|
|
142
|
+
'',
|
|
143
|
+
'Jar registrations:',
|
|
144
|
+
...jarLines,
|
|
145
|
+
...localeLine,
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
48
148
|
/**
|
|
49
149
|
* Appends a line to a jar.mn-style file when that exact line is not
|
|
50
150
|
* already present. Captures the pre-write contents in the journal so a
|
|
@@ -192,6 +292,18 @@ export async function furnaceChromeDocCreateCommand(projectRoot, name, options =
|
|
|
192
292
|
}
|
|
193
293
|
const withTitlebar = options.titlebar ?? true;
|
|
194
294
|
const withTests = options.withTests ?? false;
|
|
295
|
+
const plan = await buildChromeDocPlan({
|
|
296
|
+
engineDir,
|
|
297
|
+
name,
|
|
298
|
+
withTests,
|
|
299
|
+
binaryName: forgeConfig.binaryName,
|
|
300
|
+
validateCreateConflicts: true,
|
|
301
|
+
});
|
|
302
|
+
if (options.dryRun) {
|
|
303
|
+
note(renderChromeDocCreateDryRun(name, plan), name);
|
|
304
|
+
outro('Dry run complete');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
195
307
|
const written = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocMutations({
|
|
196
308
|
name,
|
|
197
309
|
license,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FurnaceCreateOptions } from '../../types/commands/index.js';
|
|
2
|
+
import type { FurnaceConfig } from '../../types/furnace.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validates a proposed custom component against the current furnace config.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateCreateAgainstConfig(config: FurnaceConfig, componentName: string, allowPrefixMismatch: FurnaceCreateOptions['allowPrefixMismatch'], composes: string[] | undefined): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { detectComposesCycles } from '../../core/furnace-config.js';
|
|
3
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
4
|
+
import { FurnaceError } from '../../errors/furnace.js';
|
|
5
|
+
function checkNameConflict(config, name) {
|
|
6
|
+
if (name in config.custom) {
|
|
7
|
+
return `A custom component named "${name}" already exists in furnace.json`;
|
|
8
|
+
}
|
|
9
|
+
if (name in config.overrides) {
|
|
10
|
+
return `An override component named "${name}" already exists in furnace.json`;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function validateComposesTargets(config, componentName, composes) {
|
|
15
|
+
if (!composes || composes.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const known = new Set([
|
|
18
|
+
...config.stock,
|
|
19
|
+
...Object.keys(config.overrides),
|
|
20
|
+
...Object.keys(config.custom),
|
|
21
|
+
]);
|
|
22
|
+
for (const tag of composes) {
|
|
23
|
+
if (tag === componentName) {
|
|
24
|
+
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
25
|
+
}
|
|
26
|
+
if (!known.has(tag)) {
|
|
27
|
+
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
28
|
+
'The referenced component must be registered as stock, override, or custom.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
detectComposesCycles({
|
|
32
|
+
...config.custom,
|
|
33
|
+
[componentName]: {
|
|
34
|
+
description: '',
|
|
35
|
+
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
36
|
+
register: true,
|
|
37
|
+
localized: false,
|
|
38
|
+
composes,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validates a proposed custom component against the current furnace config.
|
|
44
|
+
*/
|
|
45
|
+
export function validateCreateAgainstConfig(config, componentName, allowPrefixMismatch, composes) {
|
|
46
|
+
const conflict = checkNameConflict(config, componentName);
|
|
47
|
+
if (conflict) {
|
|
48
|
+
throw new FurnaceError(conflict, componentName);
|
|
49
|
+
}
|
|
50
|
+
if (config.componentPrefix &&
|
|
51
|
+
!componentName.startsWith(config.componentPrefix) &&
|
|
52
|
+
!allowPrefixMismatch) {
|
|
53
|
+
throw new InvalidArgumentError(`Name "${componentName}" does not start with the configured prefix "${config.componentPrefix}". ` +
|
|
54
|
+
`Use a prefixed name (e.g. "${config.componentPrefix}${componentName}"), update ` +
|
|
55
|
+
'`componentPrefix` in furnace.json, or pass --allow-prefix-mismatch to create the component anyway.', 'name');
|
|
56
|
+
}
|
|
57
|
+
validateComposesTargets(config, componentName, composes);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=create-validation.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { text } from '@clack/prompts';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
|
-
import { createDefaultFurnaceConfig,
|
|
5
|
+
import { createDefaultFurnaceConfig, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
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';
|
|
@@ -21,6 +21,7 @@ import { resolveCreateFeatures } from './create-features.js';
|
|
|
21
21
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
22
22
|
import { assertCustomEntryPersisted } from './create-readback.js';
|
|
23
23
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
24
|
+
import { validateCreateAgainstConfig } from './create-validation.js';
|
|
24
25
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
25
26
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
26
27
|
if (await furnaceConfigExists(projectRoot)) {
|
|
@@ -41,18 +42,6 @@ function validateTagName(name) {
|
|
|
41
42
|
return `Name ${CUSTOM_ELEMENT_TAG_RULES}`;
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Checks if a component name conflicts with existing entries in furnace.json.
|
|
46
|
-
*/
|
|
47
|
-
function checkNameConflict(config, name) {
|
|
48
|
-
if (name in config.custom) {
|
|
49
|
-
return `A custom component named "${name}" already exists in furnace.json`;
|
|
50
|
-
}
|
|
51
|
-
if (name in config.overrides) {
|
|
52
|
-
return `An override component named "${name}" already exists in furnace.json`;
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
45
|
/**
|
|
57
46
|
* Scaffolds browser mochitest files for a newly created custom component.
|
|
58
47
|
* @param componentName - Custom element tag name
|
|
@@ -247,11 +236,16 @@ async function performCreateMutations(args) {
|
|
|
247
236
|
const testFiles = [];
|
|
248
237
|
let files;
|
|
249
238
|
try {
|
|
239
|
+
const freshConfig = await loadAuthoringFurnaceConfig(args.projectRoot);
|
|
240
|
+
validateCreateAgainstConfig(freshConfig, args.componentName, args.allowPrefixMismatch, args.composes);
|
|
241
|
+
if (await pathExists(args.componentDir)) {
|
|
242
|
+
throw new FurnaceError(`Directory already exists: components/custom/${args.componentName}`, args.componentName);
|
|
243
|
+
}
|
|
250
244
|
// Record the componentDir creation entry immediately after registration
|
|
251
245
|
// so signal-driven rollback can clean it up even if writeComponentFiles
|
|
252
246
|
// is interrupted mid-ensureDir.
|
|
253
247
|
recordCreatedDir(journal, args.componentDir);
|
|
254
|
-
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license,
|
|
248
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, resolveFtlChromeSubPath(freshConfig.ftlBasePath), args.sharedFtl, journal);
|
|
255
249
|
const customEntry = {
|
|
256
250
|
description: args.description,
|
|
257
251
|
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
@@ -264,9 +258,9 @@ async function performCreateMutations(args) {
|
|
|
264
258
|
if (args.sharedFtl) {
|
|
265
259
|
customEntry.sharedFtl = args.sharedFtl;
|
|
266
260
|
}
|
|
267
|
-
|
|
261
|
+
freshConfig.custom[args.componentName] = customEntry;
|
|
268
262
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
269
|
-
await writeFurnaceConfig(args.projectRoot,
|
|
263
|
+
await writeFurnaceConfig(args.projectRoot, freshConfig);
|
|
270
264
|
await assertCustomEntryPersisted(args.projectRoot, args.componentName);
|
|
271
265
|
if (args.testStyle === 'browser-chrome') {
|
|
272
266
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
@@ -310,41 +304,6 @@ async function resolveDescription(isInteractive, options) {
|
|
|
310
304
|
}
|
|
311
305
|
return description;
|
|
312
306
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Validates the `--compose` targets against registered components and runs
|
|
315
|
-
* cycle detection if the new component is introduced into the graph. Throws
|
|
316
|
-
* on any failure; returns when the graph is clean.
|
|
317
|
-
*/
|
|
318
|
-
function validateComposesTargets(config, componentName, composes) {
|
|
319
|
-
if (!composes || composes.length === 0)
|
|
320
|
-
return;
|
|
321
|
-
const known = new Set([
|
|
322
|
-
...config.stock,
|
|
323
|
-
...Object.keys(config.overrides),
|
|
324
|
-
...Object.keys(config.custom),
|
|
325
|
-
]);
|
|
326
|
-
for (const tag of composes) {
|
|
327
|
-
if (tag === componentName) {
|
|
328
|
-
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
329
|
-
}
|
|
330
|
-
if (!known.has(tag)) {
|
|
331
|
-
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
332
|
-
'The referenced component must be registered as stock, override, or custom.');
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Check for cycles that would be introduced by adding this component.
|
|
336
|
-
const tempCustom = {
|
|
337
|
-
...config.custom,
|
|
338
|
-
[componentName]: {
|
|
339
|
-
description: '',
|
|
340
|
-
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
341
|
-
register: true,
|
|
342
|
-
localized: false,
|
|
343
|
-
composes,
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
detectComposesCycles(tempCustom);
|
|
347
|
-
}
|
|
348
307
|
/**
|
|
349
308
|
* Runs the furnace create command to scaffold a new custom component.
|
|
350
309
|
* @param projectRoot - Root directory of the project
|
|
@@ -391,39 +350,14 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
391
350
|
// succeeds so a cancelled create in a fresh project does not strand a new
|
|
392
351
|
// furnace.json behind.
|
|
393
352
|
const config = await loadAuthoringFurnaceConfig(projectRoot);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (conflict) {
|
|
397
|
-
throw new FurnaceError(conflict, componentName);
|
|
398
|
-
}
|
|
353
|
+
const composes = options.compose;
|
|
354
|
+
validateCreateAgainstConfig(config, componentName, options.allowPrefixMismatch, composes);
|
|
399
355
|
// Check if it already exists in the engine source tree
|
|
400
356
|
if (await pathExists(paths.engine)) {
|
|
401
357
|
if (await isComponentInEngine(paths.engine, componentName)) {
|
|
402
358
|
throw new FurnaceError(`"${componentName}" already exists in the engine source tree. Use "fireforge furnace override" instead.`, componentName);
|
|
403
359
|
}
|
|
404
360
|
}
|
|
405
|
-
// Refuse if name doesn't match componentPrefix, unless
|
|
406
|
-
// --allow-prefix-mismatch was explicitly passed.
|
|
407
|
-
//
|
|
408
|
-
// Pre-0.16.0 this was a bare `warn()` and the create flow continued,
|
|
409
|
-
// which produced a class of validation runs where the command reported
|
|
410
|
-
// success, scaffolded files under components/custom/<name>/, and
|
|
411
|
-
// registered tests in browser/base/moz.build, but the component
|
|
412
|
-
// wasn't a good citizen of the fork's convention — subsequent
|
|
413
|
-
// follow-up commands (list, status, rename) behaved inconsistently.
|
|
414
|
-
// Refusing up-front leaves the workspace untouched on a bad name and
|
|
415
|
-
// forces an intentional `--allow-prefix-mismatch` for the rare case
|
|
416
|
-
// where the mismatch is deliberate.
|
|
417
|
-
if (config.componentPrefix &&
|
|
418
|
-
!componentName.startsWith(config.componentPrefix) &&
|
|
419
|
-
!options.allowPrefixMismatch) {
|
|
420
|
-
throw new InvalidArgumentError(`Name "${componentName}" does not start with the configured prefix "${config.componentPrefix}". ` +
|
|
421
|
-
'Use a prefixed name (e.g. "' +
|
|
422
|
-
config.componentPrefix +
|
|
423
|
-
componentName +
|
|
424
|
-
'"), update `componentPrefix` in furnace.json, ' +
|
|
425
|
-
'or pass --allow-prefix-mismatch to create the component anyway.', 'name');
|
|
426
|
-
}
|
|
427
361
|
// --- Resolve description ---
|
|
428
362
|
const description = await resolveDescription(isInteractive, options);
|
|
429
363
|
// --- Resolve features ---
|
|
@@ -447,10 +381,6 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
447
381
|
if (await pathExists(componentDir)) {
|
|
448
382
|
throw new FurnaceError(`Directory already exists: components/custom/${componentName}`, componentName);
|
|
449
383
|
}
|
|
450
|
-
// --- Validate --compose targets BEFORE any writes so a failed validation
|
|
451
|
-
// does not strand component files behind.
|
|
452
|
-
const composes = options.compose;
|
|
453
|
-
validateComposesTargets(config, componentName, composes);
|
|
454
384
|
// --- Normalize and validate --shared-ftl ahead of any writes. Shares the
|
|
455
385
|
// structural rules with furnace-config.ts so the command and the on-disk
|
|
456
386
|
// schema cannot diverge. Pass the resolved `localized` rather than a
|
|
@@ -492,10 +422,6 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
492
422
|
// state via the shared rollback journal. The mutation runs under the
|
|
493
423
|
// furnace-wide lock and is registered with the global SIGINT/SIGTERM
|
|
494
424
|
// rollback pathway.
|
|
495
|
-
// Derive the FTL chrome sub-path from the configured ftlBasePath so the
|
|
496
|
-
// generated `.mjs` calls `insertFTLIfNeeded` at a URI that actually matches
|
|
497
|
-
// the locale jar.mn entry `furnace apply` will write.
|
|
498
|
-
const ftlChromeSubPath = resolveFtlChromeSubPath(config.ftlBasePath);
|
|
499
425
|
const { files, testFiles } = await runFurnaceMutation(projectRoot, 'create-rollback', (ctx) => performCreateMutations({
|
|
500
426
|
projectRoot,
|
|
501
427
|
componentName,
|
|
@@ -507,12 +433,11 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
507
433
|
sharedFtl,
|
|
508
434
|
componentDir,
|
|
509
435
|
furnacePaths,
|
|
510
|
-
|
|
436
|
+
allowPrefixMismatch: options.allowPrefixMismatch,
|
|
511
437
|
forgeConfig,
|
|
512
438
|
paths,
|
|
513
439
|
license,
|
|
514
440
|
testStyle,
|
|
515
|
-
ftlChromeSubPath,
|
|
516
441
|
operationContext: ctx,
|
|
517
442
|
}));
|
|
518
443
|
note(formatSuccessNote({
|
|
@@ -3,6 +3,7 @@ import { Option } from 'commander';
|
|
|
3
3
|
import { pickDefined } from '../../utils/options.js';
|
|
4
4
|
import { furnaceApplyCommand } from './apply.js';
|
|
5
5
|
import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
|
|
6
|
+
import { furnaceChromeDocRemoveCommand } from './chrome-doc-remove.js';
|
|
6
7
|
import { furnaceCreateCommand } from './create.js';
|
|
7
8
|
import { furnaceDeployCommand } from './deploy.js';
|
|
8
9
|
import { furnaceDiffCommand } from './diff.js';
|
|
@@ -86,6 +87,10 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
86
87
|
.action(withErrorHandling(async (name, options) => {
|
|
87
88
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
88
89
|
}));
|
|
90
|
+
registerChromeDocCommands(furnace, context);
|
|
91
|
+
}
|
|
92
|
+
function registerChromeDocCommands(furnace, context) {
|
|
93
|
+
const { getProjectRoot, withErrorHandling } = context;
|
|
89
94
|
const chromeDoc = furnace
|
|
90
95
|
.command('chrome-doc')
|
|
91
96
|
.description('Scaffold top-level chrome documents (xhtml + js + css + ftl + jar.mn)');
|
|
@@ -94,9 +99,18 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
94
99
|
.description('Scaffold a new top-level chrome document')
|
|
95
100
|
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
96
101
|
.option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
|
|
102
|
+
.option('--dry-run', 'Show the chrome-doc scaffold plan without writing')
|
|
97
103
|
.action(withErrorHandling(async (name, options) => {
|
|
98
104
|
await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
|
|
99
105
|
}));
|
|
106
|
+
chromeDoc
|
|
107
|
+
.command('remove <name>')
|
|
108
|
+
.description('Remove a scaffolded top-level chrome document')
|
|
109
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
110
|
+
.option('--dry-run', 'Show the chrome-doc removal plan without writing')
|
|
111
|
+
.action(withErrorHandling(async (name, options) => {
|
|
112
|
+
await furnaceChromeDocRemoveCommand(getProjectRoot(), name, pickDefined(options));
|
|
113
|
+
}));
|
|
100
114
|
}
|
|
101
115
|
/**
|
|
102
116
|
* Registers Furnace commands for authoring, inspection, and maintenance:
|
|
@@ -213,6 +213,7 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
213
213
|
let totalUnchanged = 0;
|
|
214
214
|
let totalSkipped = 0;
|
|
215
215
|
const conflictComponents = [];
|
|
216
|
+
const failedOverrides = [];
|
|
216
217
|
// Snapshot furnace.json before the batch loop so an unexpected failure
|
|
217
218
|
// (process crash, unhandled error) can be recovered from. Per-component
|
|
218
219
|
// errors caught below are expected and do not trigger a restore — only
|
|
@@ -244,7 +245,9 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
catch (error) {
|
|
247
|
-
|
|
248
|
+
const message = toError(error).message;
|
|
249
|
+
warn(`${overrideName}: ${message}`);
|
|
250
|
+
failedOverrides.push({ name: overrideName, message });
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
}
|
|
@@ -257,12 +260,18 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
257
260
|
throw error;
|
|
258
261
|
}
|
|
259
262
|
const summary = `${overrideNames.length} override(s) processed, ${totalSkipped} already up-to-date\n` +
|
|
260
|
-
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s)
|
|
263
|
+
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s), ` +
|
|
264
|
+
`${failedOverrides.length} failed`;
|
|
261
265
|
if (conflictComponents.length > 0) {
|
|
262
266
|
warn(`Conflicts in: ${conflictComponents.join(', ')}. ` +
|
|
263
267
|
'Resolve conflict markers, then re-run refresh for those components to update baseVersion.');
|
|
264
268
|
}
|
|
265
269
|
note(summary, dryRun ? 'Dry Run Summary' : 'Refresh Summary');
|
|
270
|
+
if (failedOverrides.length > 0) {
|
|
271
|
+
outro(dryRun ? 'Dry run completed with failures' : 'Refresh completed with failures');
|
|
272
|
+
throw new FurnaceError(`Failed to refresh ${failedOverrides.length} override(s): ` +
|
|
273
|
+
failedOverrides.map((failure) => `${failure.name}: ${failure.message}`).join('; '));
|
|
274
|
+
}
|
|
266
275
|
outro(dryRun ? 'Dry run complete' : 'Refresh complete');
|
|
267
276
|
}
|
|
268
277
|
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes every checksum entry owned by the removed component.
|
|
3
|
+
*/
|
|
4
|
+
export function dropChecksumsByPrefix(state, prefix) {
|
|
5
|
+
const result = { ...state };
|
|
6
|
+
if (state.appliedChecksums) {
|
|
7
|
+
result.appliedChecksums = Object.fromEntries(Object.entries(state.appliedChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
8
|
+
}
|
|
9
|
+
if (state.engineChecksums) {
|
|
10
|
+
result.engineChecksums = Object.fromEntries(Object.entries(state.engineChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=remove-state.js.map
|