@hominis/fireforge 0.10.1 → 0.11.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 +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { multiselect, text } from '@clack/prompts';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createDefaultFurnaceConfig, detectComposesCycles, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
|
+
import { tagNameToClassName } from '../../core/furnace-constants.js';
|
|
7
|
+
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
8
|
+
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
9
|
+
import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
6
10
|
import { isComponentInEngine } from '../../core/furnace-scanner.js';
|
|
7
11
|
import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
|
|
8
12
|
import { registerTestManifest } from '../../core/manifest-register.js';
|
|
@@ -11,15 +15,11 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
11
15
|
import { toError } from '../../utils/errors.js';
|
|
12
16
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
13
17
|
import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return tagName
|
|
20
|
-
.split('-')
|
|
21
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
22
|
-
.join('');
|
|
18
|
+
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
19
|
+
if (await furnaceConfigExists(projectRoot)) {
|
|
20
|
+
return loadFurnaceConfig(projectRoot);
|
|
21
|
+
}
|
|
22
|
+
return createDefaultFurnaceConfig();
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Validates a custom element tag name.
|
|
@@ -30,8 +30,8 @@ function validateTagName(name) {
|
|
|
30
30
|
return 'Name is required';
|
|
31
31
|
if (!name.includes('-'))
|
|
32
32
|
return 'Custom element names must contain a hyphen (e.g., "my-widget")';
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
33
|
+
if (!CUSTOM_ELEMENT_TAG_PATTERN.test(name))
|
|
34
|
+
return `Name ${CUSTOM_ELEMENT_TAG_RULES}`;
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
@@ -111,9 +111,10 @@ function generateFtlContent(name, header) {
|
|
|
111
111
|
* @param license - Project license used for generated headers
|
|
112
112
|
* @param forgeConfig - Project config fields needed for test naming
|
|
113
113
|
* @param paths - Resolved project paths used to place test files
|
|
114
|
+
* @param journal - Optional rollback journal that snapshots files before writes
|
|
114
115
|
* @returns Relative test filenames created or updated for the component
|
|
115
116
|
*/
|
|
116
|
-
async function scaffoldTestFiles(componentName, license, forgeConfig, paths) {
|
|
117
|
+
async function scaffoldTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
117
118
|
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
118
119
|
// Avoid double-prefixing: strip binaryName prefix since testDirName already uses it
|
|
119
120
|
const testDirName = forgeConfig.binaryName;
|
|
@@ -123,6 +124,9 @@ async function scaffoldTestFiles(componentName, license, forgeConfig, paths) {
|
|
|
123
124
|
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
124
125
|
const testFileName = `browser_${testDirName}_${underscored}.js`;
|
|
125
126
|
const testDir = join(paths.engine, 'browser/base/content/test', testDirName);
|
|
127
|
+
if (journal && !(await pathExists(testDir))) {
|
|
128
|
+
recordCreatedDir(journal, testDir);
|
|
129
|
+
}
|
|
126
130
|
await ensureDir(testDir);
|
|
127
131
|
const jsHeader = getLicenseHeader(license, 'js');
|
|
128
132
|
const hashHeader = getLicenseHeader(license, 'hash');
|
|
@@ -133,10 +137,14 @@ async function scaffoldTestFiles(componentName, license, forgeConfig, paths) {
|
|
|
133
137
|
// Append the new test entry if not already present
|
|
134
138
|
const existingToml = await readText(tomlPath);
|
|
135
139
|
if (!existingToml.includes(`["${testFileName}"]`)) {
|
|
140
|
+
if (journal)
|
|
141
|
+
await snapshotFile(journal, tomlPath);
|
|
136
142
|
await writeText(tomlPath, existingToml.trimEnd() + `\n\n["${testFileName}"]\n`);
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
145
|
else {
|
|
146
|
+
if (journal)
|
|
147
|
+
await snapshotFile(journal, tomlPath);
|
|
140
148
|
const browserToml = `${hashHeader}
|
|
141
149
|
|
|
142
150
|
[DEFAULT]
|
|
@@ -150,6 +158,8 @@ support-files = ["head.js"]
|
|
|
150
158
|
// head.js — only create if it doesn't exist (shared across components)
|
|
151
159
|
const headPath = join(testDir, 'head.js');
|
|
152
160
|
if (!(await pathExists(headPath))) {
|
|
161
|
+
if (journal)
|
|
162
|
+
await snapshotFile(journal, headPath);
|
|
153
163
|
const headJs = `${jsHeader}
|
|
154
164
|
|
|
155
165
|
"use strict";
|
|
@@ -177,10 +187,18 @@ add_task(async function test_${underscored}_defined() {
|
|
|
177
187
|
Assert.equal(typeof ctor, "function", "Constructor should be a function");
|
|
178
188
|
});
|
|
179
189
|
`;
|
|
180
|
-
|
|
190
|
+
const testFilePath = join(testDir, testFileName);
|
|
191
|
+
if (journal)
|
|
192
|
+
await snapshotFile(journal, testFilePath);
|
|
193
|
+
await writeText(testFilePath, testJs);
|
|
181
194
|
testFiles.push(testFileName);
|
|
182
|
-
// Register in moz.build
|
|
195
|
+
// Register in moz.build. The registration helper edits browser/base/moz.build,
|
|
196
|
+
// so snapshot it first when a journal is supplied. The existing warn-and-continue
|
|
197
|
+
// contract is preserved so a missing/unparseable moz.build never trips rollback.
|
|
183
198
|
try {
|
|
199
|
+
const mozBuildPath = join(paths.engine, 'browser/base/moz.build');
|
|
200
|
+
if (journal)
|
|
201
|
+
await snapshotFile(journal, mozBuildPath);
|
|
184
202
|
const registerResult = await registerTestManifest(paths.engine, testDirName);
|
|
185
203
|
if (!registerResult.skipped) {
|
|
186
204
|
success(`Registered test manifest in ${registerResult.manifest}`);
|
|
@@ -233,22 +251,77 @@ async function resolveCreateFeatures(isInteractive, options) {
|
|
|
233
251
|
* @param description - Human-readable component description
|
|
234
252
|
* @param localized - Whether to include a Fluent file
|
|
235
253
|
* @param license - Project license used for generated headers
|
|
254
|
+
* @param journal - Optional rollback journal that snapshots files before writes
|
|
236
255
|
* @returns Relative filenames written for the component
|
|
237
256
|
*/
|
|
238
|
-
async function writeComponentFiles(componentDir, componentName, className, description, localized, license) {
|
|
257
|
+
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, journal) {
|
|
239
258
|
await ensureDir(componentDir);
|
|
240
259
|
const files = [`${componentName}.mjs`, `${componentName}.css`];
|
|
260
|
+
const mjsPath = join(componentDir, `${componentName}.mjs`);
|
|
261
|
+
if (journal)
|
|
262
|
+
await snapshotFile(journal, mjsPath);
|
|
241
263
|
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'));
|
|
242
|
-
await writeText(
|
|
264
|
+
await writeText(mjsPath, mjsContent);
|
|
265
|
+
const cssPath = join(componentDir, `${componentName}.css`);
|
|
266
|
+
if (journal)
|
|
267
|
+
await snapshotFile(journal, cssPath);
|
|
243
268
|
const cssContent = generateCssContent(getLicenseHeader(license, 'css'));
|
|
244
|
-
await writeText(
|
|
269
|
+
await writeText(cssPath, cssContent);
|
|
245
270
|
if (localized) {
|
|
271
|
+
const ftlPath = join(componentDir, `${componentName}.ftl`);
|
|
272
|
+
if (journal)
|
|
273
|
+
await snapshotFile(journal, ftlPath);
|
|
246
274
|
const ftlContent = generateFtlContent(componentName, getLicenseHeader(license, 'hash'));
|
|
247
|
-
await writeText(
|
|
275
|
+
await writeText(ftlPath, ftlContent);
|
|
248
276
|
files.push(`${componentName}.ftl`);
|
|
249
277
|
}
|
|
250
278
|
return files;
|
|
251
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Performs the transactional mutation phase of furnace create. All file
|
|
282
|
+
* writes and the config update are recorded in a rollback journal so a
|
|
283
|
+
* failure mid-phase restores the workspace and engine to their pre-command
|
|
284
|
+
* state.
|
|
285
|
+
*/
|
|
286
|
+
async function performCreateMutations(args) {
|
|
287
|
+
const journal = createRollbackJournal();
|
|
288
|
+
if (args.operationContext) {
|
|
289
|
+
args.operationContext.registerJournal(journal);
|
|
290
|
+
}
|
|
291
|
+
recordCreatedDir(journal, args.componentDir);
|
|
292
|
+
const testFiles = [];
|
|
293
|
+
let files;
|
|
294
|
+
try {
|
|
295
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, journal);
|
|
296
|
+
const customEntry = {
|
|
297
|
+
description: args.description,
|
|
298
|
+
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
299
|
+
register: args.register,
|
|
300
|
+
localized: args.localized,
|
|
301
|
+
};
|
|
302
|
+
if (args.composes && args.composes.length > 0) {
|
|
303
|
+
customEntry.composes = args.composes;
|
|
304
|
+
}
|
|
305
|
+
args.config.custom[args.componentName] = customEntry;
|
|
306
|
+
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
307
|
+
await writeFurnaceConfig(args.projectRoot, args.config);
|
|
308
|
+
if (args.withTests) {
|
|
309
|
+
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
310
|
+
testFiles.push(...scafFiles);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
try {
|
|
315
|
+
await restoreRollbackJournalOrThrow(journal, `Failed to create custom component "${args.componentName}"`);
|
|
316
|
+
}
|
|
317
|
+
catch (rollbackError) {
|
|
318
|
+
await recordFurnaceRollbackFailure(args.projectRoot, 'create-rollback', toError(rollbackError).message);
|
|
319
|
+
throw rollbackError;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
return { files, testFiles };
|
|
324
|
+
}
|
|
252
325
|
/**
|
|
253
326
|
* Runs the furnace create command to scaffold a new custom component.
|
|
254
327
|
* @param projectRoot - Root directory of the project
|
|
@@ -258,22 +331,27 @@ async function writeComponentFiles(componentDir, componentName, className, descr
|
|
|
258
331
|
export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
259
332
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
260
333
|
intro('Furnace Create');
|
|
261
|
-
// Load or create furnace.json
|
|
262
|
-
const config = await ensureFurnaceConfig(projectRoot);
|
|
263
|
-
const paths = getProjectPaths(projectRoot);
|
|
264
|
-
const forgeConfig = await loadConfig(projectRoot);
|
|
265
|
-
const license = forgeConfig.license ?? DEFAULT_LICENSE;
|
|
266
|
-
const furnacePaths = getFurnacePaths(projectRoot);
|
|
267
334
|
// --- Resolve component name ---
|
|
335
|
+
// Validation runs before we load/create any persisted furnace config so a
|
|
336
|
+
// failed authoring command never auto-creates furnace.json in a fresh
|
|
337
|
+
// directory.
|
|
268
338
|
let componentName = name;
|
|
269
339
|
if (componentName) {
|
|
270
|
-
// Validate CLI-provided name
|
|
271
340
|
const validationError = validateTagName(componentName);
|
|
272
341
|
if (validationError) {
|
|
273
342
|
throw new InvalidArgumentError(validationError, 'name');
|
|
274
343
|
}
|
|
275
344
|
}
|
|
276
|
-
else if (isInteractive) {
|
|
345
|
+
else if (!isInteractive) {
|
|
346
|
+
throw new InvalidArgumentError('Component name is required in non-interactive mode.\n' +
|
|
347
|
+
'Usage: fireforge furnace create <name> -d "description"', 'name');
|
|
348
|
+
}
|
|
349
|
+
const paths = getProjectPaths(projectRoot);
|
|
350
|
+
const forgeConfig = await loadConfig(projectRoot);
|
|
351
|
+
const license = forgeConfig.license ?? DEFAULT_LICENSE;
|
|
352
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
353
|
+
if (!componentName) {
|
|
354
|
+
// Interactive prompt path; non-interactive missing-name was rejected above.
|
|
277
355
|
const nameResult = await text({
|
|
278
356
|
message: 'Component tag name:',
|
|
279
357
|
placeholder: 'moz-my-widget',
|
|
@@ -285,10 +363,10 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
285
363
|
}
|
|
286
364
|
componentName = String(nameResult);
|
|
287
365
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
366
|
+
// Load the current furnace config only after the interactive name prompt
|
|
367
|
+
// succeeds so a cancelled create in a fresh project does not strand a new
|
|
368
|
+
// furnace.json behind.
|
|
369
|
+
const config = await loadAuthoringFurnaceConfig(projectRoot);
|
|
292
370
|
// Check for conflicts
|
|
293
371
|
const conflict = checkNameConflict(config, componentName);
|
|
294
372
|
if (conflict) {
|
|
@@ -321,6 +399,14 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
321
399
|
return;
|
|
322
400
|
}
|
|
323
401
|
const { localized, register } = featureSelection;
|
|
402
|
+
// --with-tests writes files under engine/browser/base/content/test/ and
|
|
403
|
+
// registers them in moz.build. Guard against a missing engine now rather
|
|
404
|
+
// than letting scaffoldTestFiles fabricate a partial engine tree with
|
|
405
|
+
// ensureDir.
|
|
406
|
+
const withTests = options.withTests ?? false;
|
|
407
|
+
if (withTests && !(await pathExists(paths.engine))) {
|
|
408
|
+
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests.', componentName);
|
|
409
|
+
}
|
|
324
410
|
// --- Generate component files ---
|
|
325
411
|
const className = tagNameToClassName(componentName);
|
|
326
412
|
const componentDir = join(furnacePaths.customDir, componentName);
|
|
@@ -328,35 +414,59 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
328
414
|
if (await pathExists(componentDir)) {
|
|
329
415
|
throw new FurnaceError(`Directory already exists: components/custom/${componentName}`, componentName);
|
|
330
416
|
}
|
|
331
|
-
|
|
332
|
-
//
|
|
417
|
+
// --- Validate --compose targets BEFORE any writes so a failed validation
|
|
418
|
+
// does not strand component files behind.
|
|
333
419
|
const composes = options.compose;
|
|
334
420
|
if (composes && composes.length > 0) {
|
|
421
|
+
const known = new Set([
|
|
422
|
+
...config.stock,
|
|
423
|
+
...Object.keys(config.overrides),
|
|
424
|
+
...Object.keys(config.custom),
|
|
425
|
+
]);
|
|
335
426
|
for (const tag of composes) {
|
|
336
|
-
if (
|
|
337
|
-
|
|
427
|
+
if (tag === componentName) {
|
|
428
|
+
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
429
|
+
}
|
|
430
|
+
if (!known.has(tag)) {
|
|
431
|
+
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
432
|
+
'The referenced component must be registered as stock, override, or custom.');
|
|
338
433
|
}
|
|
339
434
|
}
|
|
435
|
+
// Check for cycles that would be introduced by adding this component.
|
|
436
|
+
const tempCustom = {
|
|
437
|
+
...config.custom,
|
|
438
|
+
[componentName]: {
|
|
439
|
+
description: '',
|
|
440
|
+
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
441
|
+
register: true,
|
|
442
|
+
localized: false,
|
|
443
|
+
composes,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
detectComposesCycles(tempCustom);
|
|
340
447
|
}
|
|
341
|
-
//
|
|
342
|
-
|
|
448
|
+
// All validation is done. Hand off to the transactional mutation helper
|
|
449
|
+
// so any failure restores the workspace and engine to their pre-command
|
|
450
|
+
// state via the shared rollback journal. The mutation runs under the
|
|
451
|
+
// furnace-wide lock and is registered with the global SIGINT/SIGTERM
|
|
452
|
+
// rollback pathway.
|
|
453
|
+
const { files, testFiles } = await runFurnaceMutation(projectRoot, 'create-rollback', (ctx) => performCreateMutations({
|
|
454
|
+
projectRoot,
|
|
455
|
+
componentName,
|
|
456
|
+
className,
|
|
343
457
|
description,
|
|
344
|
-
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
345
|
-
register,
|
|
346
458
|
localized,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
testFiles.push(...scafFiles);
|
|
359
|
-
}
|
|
459
|
+
register,
|
|
460
|
+
composes,
|
|
461
|
+
componentDir,
|
|
462
|
+
furnacePaths,
|
|
463
|
+
config,
|
|
464
|
+
forgeConfig,
|
|
465
|
+
paths,
|
|
466
|
+
license,
|
|
467
|
+
withTests,
|
|
468
|
+
operationContext: ctx,
|
|
469
|
+
}));
|
|
360
470
|
// --- Success ---
|
|
361
471
|
let noteParts = `Files created in components/custom/${componentName}/:\n` +
|
|
362
472
|
files.map((f) => ` ${f}`).join('\n');
|