@hominis/fireforge 0.16.2 → 0.16.5
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 +15 -2
- package/dist/bin/fireforge.js +11 -2
- package/dist/src/commands/doctor-furnace.js +83 -1
- package/dist/src/commands/doctor.js +18 -0
- package/dist/src/commands/download.js +58 -12
- package/dist/src/commands/export-all.js +19 -2
- package/dist/src/commands/export-shared.d.ts +36 -0
- package/dist/src/commands/export-shared.js +76 -0
- package/dist/src/commands/export.js +23 -2
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +21 -3
- package/dist/src/commands/furnace/chrome-doc-templates.js +23 -5
- package/dist/src/commands/furnace/chrome-doc-tests.js +42 -17
- package/dist/src/commands/furnace/create-readback.d.ts +23 -0
- package/dist/src/commands/furnace/create-readback.js +34 -0
- package/dist/src/commands/furnace/create-templates.d.ts +17 -7
- package/dist/src/commands/furnace/create-templates.js +85 -31
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +1 -1
- package/dist/src/commands/furnace/create.js +2 -0
- package/dist/src/commands/furnace/preview.d.ts +12 -0
- package/dist/src/commands/furnace/preview.js +34 -2
- package/dist/src/commands/furnace/status.js +1 -1
- package/dist/src/commands/import.js +63 -11
- package/dist/src/commands/patch/delete.js +10 -1
- package/dist/src/commands/patch/index.js +10 -1
- package/dist/src/commands/re-export.js +79 -6
- package/dist/src/commands/resolve.js +15 -1
- package/dist/src/commands/run.js +27 -5
- package/dist/src/commands/setup-support.js +60 -7
- package/dist/src/commands/status.js +28 -1
- package/dist/src/commands/test.js +28 -5
- package/dist/src/commands/token-coverage.js +55 -1
- package/dist/src/commands/token.js +19 -2
- package/dist/src/commands/wire.js +22 -2
- package/dist/src/core/branding.d.ts +10 -0
- package/dist/src/core/branding.js +7 -9
- package/dist/src/core/build-prepare.js +8 -1
- package/dist/src/core/file-lock.js +49 -15
- package/dist/src/core/furnace-operation.d.ts +17 -0
- package/dist/src/core/furnace-operation.js +30 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +33 -1
- package/dist/src/core/furnace-validate-helpers.js +53 -2
- package/dist/src/core/git.js +39 -10
- package/dist/src/core/mach-error-hints.js +16 -0
- package/dist/src/core/mach.js +15 -6
- package/dist/src/core/manifest-rules.js +16 -0
- package/dist/src/core/marionette-preflight.js +43 -12
- package/dist/src/core/patch-files.d.ts +12 -1
- package/dist/src/core/patch-files.js +14 -11
- package/dist/src/core/patch-lint.js +62 -11
- package/dist/src/core/wire-destroy.js +18 -5
- package/dist/src/core/wire-init.js +20 -5
- package/dist/src/core/wire-utils.d.ts +15 -0
- package/dist/src/core/wire-utils.js +17 -0
- package/dist/src/types/commands/options.d.ts +7 -0
- package/package.json +1 -1
|
@@ -106,7 +106,18 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
106
106
|
}
|
|
107
107
|
/** Registers token management commands on the CLI program. */
|
|
108
108
|
export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
109
|
-
const token = program
|
|
109
|
+
const token = program
|
|
110
|
+
.command('token')
|
|
111
|
+
.description('Design token management')
|
|
112
|
+
// Match `fireforge furnace`'s no-args contract: print the group's help and
|
|
113
|
+
// exit 0. Without this default action, commander routes `fireforge token`
|
|
114
|
+
// (no subcommand) through its own help-then-exit-1 path, so scripts that
|
|
115
|
+
// probe the CLI surface see a misleading non-zero exit for a purely
|
|
116
|
+
// informational invocation. The action prints the exact same help commander
|
|
117
|
+
// would otherwise print, but returns successfully.
|
|
118
|
+
.action(() => {
|
|
119
|
+
token.outputHelp();
|
|
120
|
+
});
|
|
110
121
|
token
|
|
111
122
|
.command('add <token-name> <value>')
|
|
112
123
|
.description('Add a design token to CSS and documentation')
|
|
@@ -117,7 +128,13 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
117
128
|
// valid choices up-front. The runtime check in tokenAddCommand remains
|
|
118
129
|
// as a defence-in-depth guard for programmatic callers that bypass
|
|
119
130
|
// Commander's argument parsing.
|
|
120
|
-
|
|
131
|
+
// Description ends with `(required)` because Commander's
|
|
132
|
+
// `makeOptionMandatory` does not render a required marker in `--help`
|
|
133
|
+
// output — only `.requiredOption` does that, and switching to
|
|
134
|
+
// `.requiredOption` would lose the `.choices()` enforcement. The
|
|
135
|
+
// explicit suffix keeps the runtime validation AND surfaces required
|
|
136
|
+
// status in help alongside the other options that use `.requiredOption`.
|
|
137
|
+
new Option('--mode <mode>', 'Dark mode behavior (required)')
|
|
121
138
|
.choices(['auto', 'static', 'override'])
|
|
122
139
|
.makeOptionMandatory(true))
|
|
123
140
|
.option('--description <desc>', 'Comment description for the CSS file')
|
|
@@ -5,6 +5,7 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
5
5
|
import { furnaceConfigExists as checkFurnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
6
6
|
import { consumeParserFallbackEvents } from '../core/parser-fallback.js';
|
|
7
7
|
import { DEFAULT_DOM_TARGET } from '../core/wire-dom-fragment.js';
|
|
8
|
+
import { coerceToCall, validateWireName as validateWireExpression } from '../core/wire-utils.js';
|
|
8
9
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
9
10
|
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -17,10 +18,14 @@ function printWireDryRun(engineDir, name, subscriptDir, domFilePath, domTargetPa
|
|
|
17
18
|
info(` source: ${subscriptDir}/${name}.js`);
|
|
18
19
|
info(` browser-main.js: loadSubScript("chrome://browser/content/${name}.js")`);
|
|
19
20
|
if (options.init) {
|
|
20
|
-
|
|
21
|
+
// Show the coerced form so the preview matches the emitted block.
|
|
22
|
+
// Before 0.16.0 the preview echoed the raw input ("EvalStartup.init"),
|
|
23
|
+
// which did not reflect that the real run writes `EvalStartup.init();`
|
|
24
|
+
// to browser-init.js.
|
|
25
|
+
info(` browser-init.js: ${coerceToCall(options.init)}`);
|
|
21
26
|
}
|
|
22
27
|
if (options.destroy) {
|
|
23
|
-
info(` browser-init.js onUnload(): ${options.destroy}`);
|
|
28
|
+
info(` browser-init.js onUnload(): ${coerceToCall(options.destroy)}`);
|
|
24
29
|
}
|
|
25
30
|
if (domFilePath) {
|
|
26
31
|
const includePath = relative(join(engineDir, subscriptDir), join(engineDir, domFilePath)).replace(/\\/g, '/');
|
|
@@ -95,6 +100,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
95
100
|
// --after and have it forwarded unchanged to the lookup layer.
|
|
96
101
|
validateWireName(options.after);
|
|
97
102
|
}
|
|
103
|
+
// Validate init/destroy expressions BEFORE the dry-run/real fork so
|
|
104
|
+
// both paths enforce the same contract. Pre-0.16.0, validation only
|
|
105
|
+
// ran inside `addInitToBrowserInit`/`addDestroyToBrowserInit` (the
|
|
106
|
+
// real-execution path), so `--dry-run --init 'void 0'` succeeded and
|
|
107
|
+
// rendered a plausible-looking preview even though the real run would
|
|
108
|
+
// reject the same arguments. Dropping `void 0` into the template
|
|
109
|
+
// silently (or breaking out of the string literal) was already
|
|
110
|
+
// prevented downstream — this hoist just makes the failure surface
|
|
111
|
+
// identical in preview mode.
|
|
112
|
+
if (options.init !== undefined) {
|
|
113
|
+
validateWireExpression(options.init, 'init expression');
|
|
114
|
+
}
|
|
115
|
+
if (options.destroy !== undefined) {
|
|
116
|
+
validateWireExpression(options.destroy, 'destroy expression');
|
|
117
|
+
}
|
|
98
118
|
consumeParserFallbackEvents();
|
|
99
119
|
// Resolve subscript directory: CLI flag > fireforge.json > default
|
|
100
120
|
let subscriptDir = DEFAULT_BROWSER_SUBSCRIPT_DIR;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FireForgeError } from '../errors/base.js';
|
|
2
|
+
import type { ProjectLicense } from '../types/config.js';
|
|
2
3
|
/**
|
|
3
4
|
* Error thrown when branding operations fail.
|
|
4
5
|
*/
|
|
@@ -41,6 +42,15 @@ export interface BrandingConfig {
|
|
|
41
42
|
appId: string;
|
|
42
43
|
/** Binary/branding directory name (e.g., "mybrowser") */
|
|
43
44
|
binaryName: string;
|
|
45
|
+
/**
|
|
46
|
+
* Project license (from fireforge.json). Used to stamp the generated
|
|
47
|
+
* `configure.sh`, `brand.properties`, and `brand.ftl` files with the
|
|
48
|
+
* matching header so `patch-lint` does not flag them for
|
|
49
|
+
* `missing-license-header` when the project is not MPL-2.0. Optional for
|
|
50
|
+
* backwards compatibility with pre-0.16 callers that did not thread the
|
|
51
|
+
* license through — falls back to {@link DEFAULT_LICENSE}.
|
|
52
|
+
*/
|
|
53
|
+
license?: ProjectLicense;
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
46
56
|
* Sets up the custom branding directory for the browser.
|
|
@@ -4,6 +4,7 @@ import { FireForgeError } from '../errors/base.js';
|
|
|
4
4
|
import { ExitCode } from '../errors/codes.js';
|
|
5
5
|
import { copyDir, pathExists, readText, writeTextIfChanged } from '../utils/fs.js';
|
|
6
6
|
import { warn } from '../utils/logger.js';
|
|
7
|
+
import { DEFAULT_LICENSE, getLicenseHeader } from './license-headers.js';
|
|
7
8
|
/**
|
|
8
9
|
* Error thrown when branding operations fail.
|
|
9
10
|
*/
|
|
@@ -91,9 +92,8 @@ async function createConfigureScript(brandingDir, config) {
|
|
|
91
92
|
await writeTextIfChanged(configureShPath, buildConfigureScriptContent(config));
|
|
92
93
|
}
|
|
93
94
|
function buildConfigureScriptContent(config) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
95
|
+
const header = getLicenseHeader(config.license ?? DEFAULT_LICENSE, 'hash');
|
|
96
|
+
return `${header}
|
|
97
97
|
|
|
98
98
|
MOZ_APP_DISPLAYNAME="${escapeShellValue(config.name)}"
|
|
99
99
|
MOZ_MACBUNDLE_ID="${escapeShellValue(config.appId)}"
|
|
@@ -111,9 +111,8 @@ async function updateBrandProperties(brandingDir, config) {
|
|
|
111
111
|
await writeTextIfChanged(propsPath, buildBrandPropertiesContent(config));
|
|
112
112
|
}
|
|
113
113
|
function buildBrandPropertiesContent(config) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
114
|
+
const header = getLicenseHeader(config.license ?? DEFAULT_LICENSE, 'hash');
|
|
115
|
+
return `${header}
|
|
117
116
|
|
|
118
117
|
brandShorterName=${escapePropertiesValue(config.name)}
|
|
119
118
|
brandShortName=${escapePropertiesValue(config.name)}
|
|
@@ -132,9 +131,8 @@ async function updateBrandFtl(brandingDir, config) {
|
|
|
132
131
|
await writeTextIfChanged(ftlPath, buildBrandFtlContent(config));
|
|
133
132
|
}
|
|
134
133
|
function buildBrandFtlContent(config) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
134
|
+
const header = getLicenseHeader(config.license ?? DEFAULT_LICENSE, 'hash');
|
|
135
|
+
return `${header}
|
|
138
136
|
|
|
139
137
|
## Brand names
|
|
140
138
|
##
|
|
@@ -123,12 +123,19 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
123
123
|
}
|
|
124
124
|
// Clean stories before build to ensure they don't leak into production binary
|
|
125
125
|
await cleanStories(paths.engine);
|
|
126
|
-
// Set up custom branding directory and patch moz.configure
|
|
126
|
+
// Set up custom branding directory and patch moz.configure. Thread the
|
|
127
|
+
// project license through so `buildConfigureScriptContent` /
|
|
128
|
+
// `buildBrandPropertiesContent` / `buildBrandFtlContent` stamp the
|
|
129
|
+
// generated files with a matching SPDX header — otherwise `patch-lint`
|
|
130
|
+
// flags them with `missing-license-header` on every subsequent export
|
|
131
|
+
// when the project is not MPL-2.0 (the eval finding: a 0BSD-licensed
|
|
132
|
+
// fork's first export failed `lint` on its own generated branding).
|
|
127
133
|
const brandingConfig = {
|
|
128
134
|
name: config.name,
|
|
129
135
|
vendor: config.vendor,
|
|
130
136
|
appId: config.appId,
|
|
131
137
|
binaryName: config.binaryName,
|
|
138
|
+
...(config.license !== undefined ? { license: config.license } : {}),
|
|
132
139
|
};
|
|
133
140
|
if (!(await isBrandingSetup(paths.engine, brandingConfig))) {
|
|
134
141
|
const brandingSpinner = spinner('Setting up branding...');
|
|
@@ -43,23 +43,39 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
43
43
|
try {
|
|
44
44
|
const lockStat = await stat(lockPath);
|
|
45
45
|
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
// Check PID FIRST, independent of age. Before this ordering change the
|
|
47
|
+
// function age-gated everything: a lock younger than `staleMs` (default
|
|
48
|
+
// 5 minutes) was never removed even when its PID file pointed at a
|
|
49
|
+
// process that had already exited. That's the exact situation an
|
|
50
|
+
// operator lands in after SIGINT'ing `furnace preview` — the signal
|
|
51
|
+
// handler calls `process.exit`, `withFileLock`'s `finally { rm }`
|
|
52
|
+
// never runs, and the next command has to either wait 5 minutes for
|
|
53
|
+
// the staleness gate or manually remove the lock directory. With the
|
|
54
|
+
// PID-first check, an explicitly-dead owner unblocks immediately.
|
|
55
|
+
const pidCheck = await readLockOwnerPid(lockPath);
|
|
56
|
+
if (pidCheck.present) {
|
|
57
|
+
if (!isProcessAlive(pidCheck.pid)) {
|
|
58
|
+
const staleMessage = onStaleLockMessage?.(ageMs);
|
|
59
|
+
if (staleMessage) {
|
|
60
|
+
warn(staleMessage);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
verbose(`Lock at ${lockPath} owner PID ${pidCheck.pid} is no longer running — removing (age: ${Math.round(ageMs / 1000)}s)`);
|
|
64
|
+
}
|
|
65
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
66
|
+
return true;
|
|
59
67
|
}
|
|
68
|
+
// PID is alive — respect it regardless of age. A slow `mach build`
|
|
69
|
+
// legitimately holds the lock past the stale threshold and we don't
|
|
70
|
+
// want to race-remove it.
|
|
71
|
+
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${pidCheck.pid} is still running — not removing`);
|
|
72
|
+
return false;
|
|
60
73
|
}
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
// No readable PID file. Fall back to the age-only heuristic so locks
|
|
75
|
+
// written by earlier FireForge releases (which may not have written a
|
|
76
|
+
// PID file) still clear after the staleness window elapses.
|
|
77
|
+
if (ageMs <= staleMs) {
|
|
78
|
+
return false;
|
|
63
79
|
}
|
|
64
80
|
const staleMessage = onStaleLockMessage?.(ageMs);
|
|
65
81
|
if (staleMessage) {
|
|
@@ -78,6 +94,24 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
78
94
|
throw toError(error);
|
|
79
95
|
}
|
|
80
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads the owner PID from a lock directory's PID file. Returns `{ present:
|
|
99
|
+
* false }` when the PID file is missing or the content does not parse as a
|
|
100
|
+
* finite integer (caller falls back to the age-only staleness heuristic).
|
|
101
|
+
*/
|
|
102
|
+
async function readLockOwnerPid(lockPath) {
|
|
103
|
+
try {
|
|
104
|
+
const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
|
|
105
|
+
const pid = parseInt(pidContent.trim(), 10);
|
|
106
|
+
if (Number.isFinite(pid)) {
|
|
107
|
+
return { present: true, pid };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// PID file missing or unreadable — treat as absent.
|
|
112
|
+
}
|
|
113
|
+
return { present: false };
|
|
114
|
+
}
|
|
81
115
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
82
116
|
export async function withFileLock(lockPath, operation, options = {}) {
|
|
83
117
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
@@ -74,6 +74,23 @@ export declare function rollbackActiveOperationsForSignal(signal: FurnaceShutdow
|
|
|
74
74
|
* touch this directly.
|
|
75
75
|
*/
|
|
76
76
|
export declare function getFurnaceLockPath(root: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Forcibly removes the furnace lock directory for every active operation.
|
|
79
|
+
*
|
|
80
|
+
* The bin-layer signal handler calls `process.exit` after rollback, which
|
|
81
|
+
* short-circuits Node's normal unwinding — `withFileLock`'s `finally { rm
|
|
82
|
+
* }` never runs, so the lock directory survives the process. The next
|
|
83
|
+
* FireForge command then has to either wait out the staleness window or
|
|
84
|
+
* have the operator remove the lock manually. This sweeper runs inside
|
|
85
|
+
* the signal-handler pipeline BEFORE `process.exit`, so the lock is gone
|
|
86
|
+
* by the time the next command starts.
|
|
87
|
+
*
|
|
88
|
+
* Errors are logged and swallowed: we do not want a slow I/O failure at
|
|
89
|
+
* shutdown to prevent the process from exiting. The doctor-side stale
|
|
90
|
+
* lock check (`src/commands/doctor-furnace.ts`) is the backup path for
|
|
91
|
+
* any lock that escapes this sweep.
|
|
92
|
+
*/
|
|
93
|
+
export declare function forceReleaseFurnaceLocksForActiveOperations(): Promise<void>;
|
|
77
94
|
/**
|
|
78
95
|
* Runs a furnace-mutating body under the apply-wide lock and registers it
|
|
79
96
|
* with the process-wide SIGINT/SIGTERM rollback pathway. The lock prevents
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { FurnaceError } from '../errors/furnace.js';
|
|
4
5
|
import { toError } from '../utils/errors.js';
|
|
5
|
-
import { warn } from '../utils/logger.js';
|
|
6
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
6
7
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
7
8
|
import { withFileLock } from './file-lock.js';
|
|
8
9
|
import { loadFurnaceState, updateFurnaceState } from './furnace-config.js';
|
|
@@ -136,6 +137,34 @@ async function persistPendingRepair(root, operation, reason) {
|
|
|
136
137
|
export function getFurnaceLockPath(root) {
|
|
137
138
|
return join(root, FIREFORGE_DIR, FURNACE_LOCK_FILENAME);
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Forcibly removes the furnace lock directory for every active operation.
|
|
142
|
+
*
|
|
143
|
+
* The bin-layer signal handler calls `process.exit` after rollback, which
|
|
144
|
+
* short-circuits Node's normal unwinding — `withFileLock`'s `finally { rm
|
|
145
|
+
* }` never runs, so the lock directory survives the process. The next
|
|
146
|
+
* FireForge command then has to either wait out the staleness window or
|
|
147
|
+
* have the operator remove the lock manually. This sweeper runs inside
|
|
148
|
+
* the signal-handler pipeline BEFORE `process.exit`, so the lock is gone
|
|
149
|
+
* by the time the next command starts.
|
|
150
|
+
*
|
|
151
|
+
* Errors are logged and swallowed: we do not want a slow I/O failure at
|
|
152
|
+
* shutdown to prevent the process from exiting. The doctor-side stale
|
|
153
|
+
* lock check (`src/commands/doctor-furnace.ts`) is the backup path for
|
|
154
|
+
* any lock that escapes this sweep.
|
|
155
|
+
*/
|
|
156
|
+
export async function forceReleaseFurnaceLocksForActiveOperations() {
|
|
157
|
+
const paths = new Set([...activeOperations.values()].map((op) => getFurnaceLockPath(op.root)));
|
|
158
|
+
for (const lockPath of paths) {
|
|
159
|
+
try {
|
|
160
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
161
|
+
verbose(`Removed furnace lock at ${lockPath} during signal teardown`);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
verbose(`Could not remove furnace lock at ${lockPath} during signal teardown: ${toError(error).message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
139
168
|
/**
|
|
140
169
|
* Runs a furnace-mutating body under the apply-wide lock and registers it
|
|
141
170
|
* with the process-wide SIGINT/SIGTERM rollback pathway. The lock prevents
|
|
@@ -61,7 +61,39 @@ export declare function stripCssBlockComments(content: string): string;
|
|
|
61
61
|
export declare function hasRelativeModuleImport(mjsContent: string): boolean;
|
|
62
62
|
/** Detects whether a module defines a custom element at runtime. */
|
|
63
63
|
export declare function hasCustomElementDefineCall(mjsContent: string): boolean;
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Detects whether the module's `customElements.define(...)` call includes a
|
|
66
|
+
* literal `extends:` option (third argument). That shape is the marker for
|
|
67
|
+
* a customized built-in element — the class extends a specific
|
|
68
|
+
* `HTMLxxxElement` rather than the autonomous `MozLitElement` path.
|
|
69
|
+
*
|
|
70
|
+
* Firefox's own widgets use this pattern for toolkit anchors (e.g.
|
|
71
|
+
* `moz-support-link` extends `HTMLAnchorElement` with
|
|
72
|
+
* `customElements.define("moz-support-link", ..., { extends: "a" })`), and
|
|
73
|
+
* the validator's `not-moz-lit-element` check must allow them through or
|
|
74
|
+
* `furnace override` of a valid upstream component fails its own
|
|
75
|
+
* `furnace validate` pass with nothing the operator can fix.
|
|
76
|
+
*/
|
|
77
|
+
export declare function hasCustomElementExtendsOption(mjsContent: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Checks whether a declared component class extends a valid element base.
|
|
80
|
+
*
|
|
81
|
+
* Two shapes are accepted:
|
|
82
|
+
*
|
|
83
|
+
* 1. Autonomous custom element: `class X extends MozLitElement` — the
|
|
84
|
+
* default FireForge pattern for fork-authored components and most
|
|
85
|
+
* toolkit widgets.
|
|
86
|
+
* 2. Customized built-in: `class X extends HTML<Something>Element` paired
|
|
87
|
+
* with a `customElements.define(..., ..., { extends: "<tagname>" })`
|
|
88
|
+
* call. Firefox's `moz-support-link`, `moz-button-group` tabbing
|
|
89
|
+
* widgets, and a handful of other toolkit components use this form;
|
|
90
|
+
* `furnace override` of those components writes the original source
|
|
91
|
+
* verbatim and the validator must not reject them.
|
|
92
|
+
*
|
|
93
|
+
* A class that extends a plain `HTMLElement` WITHOUT a `extends:` option
|
|
94
|
+
* is still rejected — that's the legitimate `not-moz-lit-element` case
|
|
95
|
+
* the rule was originally designed to catch.
|
|
96
|
+
*/
|
|
65
97
|
export declare function classExtendsMozLitElement(mjsContent: string): boolean;
|
|
66
98
|
/** Collects CSS custom property references used via var(--token-name). */
|
|
67
99
|
export declare function collectCssVariableReferences(cssContent: string): string[];
|
|
@@ -269,7 +269,46 @@ export function hasRelativeModuleImport(mjsContent) {
|
|
|
269
269
|
export function hasCustomElementDefineCall(mjsContent) {
|
|
270
270
|
return /customElements\.define\s*\(/.test(mjsContent);
|
|
271
271
|
}
|
|
272
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Detects whether the module's `customElements.define(...)` call includes a
|
|
274
|
+
* literal `extends:` option (third argument). That shape is the marker for
|
|
275
|
+
* a customized built-in element — the class extends a specific
|
|
276
|
+
* `HTMLxxxElement` rather than the autonomous `MozLitElement` path.
|
|
277
|
+
*
|
|
278
|
+
* Firefox's own widgets use this pattern for toolkit anchors (e.g.
|
|
279
|
+
* `moz-support-link` extends `HTMLAnchorElement` with
|
|
280
|
+
* `customElements.define("moz-support-link", ..., { extends: "a" })`), and
|
|
281
|
+
* the validator's `not-moz-lit-element` check must allow them through or
|
|
282
|
+
* `furnace override` of a valid upstream component fails its own
|
|
283
|
+
* `furnace validate` pass with nothing the operator can fix.
|
|
284
|
+
*/
|
|
285
|
+
export function hasCustomElementExtendsOption(mjsContent) {
|
|
286
|
+
// Match `customElements.define(..., { ..., extends: "..." })`. Tolerant of
|
|
287
|
+
// whitespace, line breaks, and other object properties. The `[^)]*` stops
|
|
288
|
+
// the inner greedy match at the closing define call paren so a later
|
|
289
|
+
// `define` call on a different custom element in the same module does
|
|
290
|
+
// not bleed its options in.
|
|
291
|
+
return /customElements\.define\s*\([^)]*\bextends\s*:\s*["'`]/.test(mjsContent);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Checks whether a declared component class extends a valid element base.
|
|
295
|
+
*
|
|
296
|
+
* Two shapes are accepted:
|
|
297
|
+
*
|
|
298
|
+
* 1. Autonomous custom element: `class X extends MozLitElement` — the
|
|
299
|
+
* default FireForge pattern for fork-authored components and most
|
|
300
|
+
* toolkit widgets.
|
|
301
|
+
* 2. Customized built-in: `class X extends HTML<Something>Element` paired
|
|
302
|
+
* with a `customElements.define(..., ..., { extends: "<tagname>" })`
|
|
303
|
+
* call. Firefox's `moz-support-link`, `moz-button-group` tabbing
|
|
304
|
+
* widgets, and a handful of other toolkit components use this form;
|
|
305
|
+
* `furnace override` of those components writes the original source
|
|
306
|
+
* verbatim and the validator must not reject them.
|
|
307
|
+
*
|
|
308
|
+
* A class that extends a plain `HTMLElement` WITHOUT a `extends:` option
|
|
309
|
+
* is still rejected — that's the legitimate `not-moz-lit-element` case
|
|
310
|
+
* the rule was originally designed to catch.
|
|
311
|
+
*/
|
|
273
312
|
export function classExtendsMozLitElement(mjsContent) {
|
|
274
313
|
const hasClassDeclaration = /class\s+\w+\s+extends\s+/.test(mjsContent);
|
|
275
314
|
if (!hasClassDeclaration) {
|
|
@@ -278,7 +317,19 @@ export function classExtendsMozLitElement(mjsContent) {
|
|
|
278
317
|
// structural issues.
|
|
279
318
|
return true;
|
|
280
319
|
}
|
|
281
|
-
|
|
320
|
+
if (/class\s+\w+\s+extends\s+MozLitElement\b/.test(mjsContent)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
// Customized built-in: extend a specific HTMLxxxElement AND the define
|
|
324
|
+
// call carries an `extends:` option. Both halves are required — a class
|
|
325
|
+
// that extends `HTMLAnchorElement` without the matching define option is
|
|
326
|
+
// almost certainly an author mistake, and a define call with `extends:`
|
|
327
|
+
// without a matching class is unreachable.
|
|
328
|
+
const extendsCustomizedBuiltin = /class\s+\w+\s+extends\s+HTML[A-Z]\w*Element\b/.test(mjsContent);
|
|
329
|
+
if (extendsCustomizedBuiltin && hasCustomElementExtendsOption(mjsContent)) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
282
333
|
}
|
|
283
334
|
/** Collects CSS custom property references used via var(--token-name). */
|
|
284
335
|
export function collectCssVariableReferences(cssContent) {
|
package/dist/src/core/git.js
CHANGED
|
@@ -83,6 +83,15 @@ async function stageAllFilesChunked(dir, options = {}) {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Interval between heartbeat progress messages during the monolithic
|
|
88
|
+
* `git add -A`. On a fresh ~600 MB Firefox tree the monolithic add runs
|
|
89
|
+
* 60–120 seconds, during which git emits nothing to stdout/stderr. Without
|
|
90
|
+
* a heartbeat the CLI spinner stays pinned on "Indexing Firefox source …"
|
|
91
|
+
* for the full window, looks hung, and in the eval scenario operators
|
|
92
|
+
* SIGINT'd mid-way assuming the process had stalled.
|
|
93
|
+
*/
|
|
94
|
+
const GIT_ADD_HEARTBEAT_MS = 15_000;
|
|
86
95
|
/**
|
|
87
96
|
* Stages all files in the repository.
|
|
88
97
|
* Tries a monolithic `git add -A` first; if that times out, falls back to
|
|
@@ -90,19 +99,39 @@ async function stageAllFilesChunked(dir, options = {}) {
|
|
|
90
99
|
*/
|
|
91
100
|
export async function stageAllFiles(dir, options = {}) {
|
|
92
101
|
const timeout = options.timeout ?? GIT_ADD_TIMEOUT_MS;
|
|
102
|
+
const reportProgress = options.onProgress;
|
|
103
|
+
const heartbeatStartedAt = Date.now();
|
|
104
|
+
// Periodic heartbeat so non-TTY log scrapers (CI, tail -f) AND operators
|
|
105
|
+
// watching a spinner both see that the add is still making progress
|
|
106
|
+
// rather than a dead process. Each tick reports elapsed seconds so the
|
|
107
|
+
// expected 1–3 minute window (see `download.ts`' info banner) is
|
|
108
|
+
// observable as it unfolds.
|
|
109
|
+
const heartbeatTimer = reportProgress
|
|
110
|
+
? setInterval(() => {
|
|
111
|
+
const elapsedS = Math.round((Date.now() - heartbeatStartedAt) / 1000);
|
|
112
|
+
reportProgress(`Indexing Firefox source (still staging, ${elapsedS}s elapsed)`);
|
|
113
|
+
}, GIT_ADD_HEARTBEAT_MS)
|
|
114
|
+
: null;
|
|
115
|
+
heartbeatTimer?.unref();
|
|
93
116
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
try {
|
|
118
|
+
await git(['add', '-A'], dir, { timeout, env: GIT_ADD_ENV });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!isTimeoutError(error)) {
|
|
123
|
+
throw await maybeWrapIndexLockError(dir, error);
|
|
124
|
+
}
|
|
125
|
+
options.onProgress?.('Monolithic git add timed out; falling back to chunked staging...');
|
|
100
126
|
}
|
|
101
|
-
|
|
127
|
+
// The killed process may have left an index lock
|
|
128
|
+
await cleanupIndexLock(dir);
|
|
129
|
+
await stageAllFilesChunked(dir, options);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (heartbeatTimer)
|
|
133
|
+
clearInterval(heartbeatTimer);
|
|
102
134
|
}
|
|
103
|
-
// The killed process may have left an index lock
|
|
104
|
-
await cleanupIndexLock(dir);
|
|
105
|
-
await stageAllFilesChunked(dir, options);
|
|
106
135
|
}
|
|
107
136
|
/**
|
|
108
137
|
* Initializes a new git repository with an orphan branch.
|
|
@@ -57,6 +57,22 @@ export const MACH_ERROR_HINTS = [
|
|
|
57
57
|
'remove any `pub type basic_string___self_view = …<_CharT>;` line from ' +
|
|
58
58
|
'`<objdir>/release/build/gecko-profiler-*/out/gecko/bindings.rs`.',
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
// When `mach build` fails mid-compile, mach's own shutdown pipeline still
|
|
62
|
+
// runs its trailing "Config object not found by mach. / Configure
|
|
63
|
+
// complete! / Be sure to run |mach build|..." summary on the way out.
|
|
64
|
+
// Those three lines are plain upstream mach output, printed AFTER the
|
|
65
|
+
// non-zero exit code has already been established, and they look
|
|
66
|
+
// deceptively like a success banner — the eval's Darwin 25 log had
|
|
67
|
+
// operators double-checking whether `make` had actually failed. We do
|
|
68
|
+
// not own those lines, but we can give the operator a specific nudge
|
|
69
|
+
// that they are cosmetic post-failure output rather than a mixed
|
|
70
|
+
// success/failure signal.
|
|
71
|
+
pattern: /Config object not found by mach\.[\s\S]*?Configure complete!/,
|
|
72
|
+
hint: 'Ignore the trailing "Config object not found by mach. / Configure complete!" block — ' +
|
|
73
|
+
"that is mach's post-failure configure summary printed after the build already failed, " +
|
|
74
|
+
'not a sign the build succeeded. The real failure is the error above this block.',
|
|
75
|
+
},
|
|
60
76
|
];
|
|
61
77
|
/**
|
|
62
78
|
* Scans captured stderr for known mach errors and returns matching hints.
|
package/dist/src/core/mach.js
CHANGED
|
@@ -111,13 +111,22 @@ export async function bootstrapWithOutput(engineDir) {
|
|
|
111
111
|
return runMachInheritCapture(['bootstrap', '--application-choice', 'browser'], engineDir);
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* Prints any matched {@link MachErrorHint} hints for the captured
|
|
114
|
+
* Prints any matched {@link MachErrorHint} hints for the captured mach output.
|
|
115
115
|
* No-op when nothing matches. Always called before a non-zero exit propagates
|
|
116
116
|
* so the hint sits immediately below the raw mach error in the operator's
|
|
117
117
|
* terminal.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
*
|
|
119
|
+
* The scanner is passed the concatenation of stderr AND stdout because mach
|
|
120
|
+
* streams its subcommand output through a timestamp-prefixing wrapper that
|
|
121
|
+
* writes both streams to whatever FD the subprocess chose — in practice,
|
|
122
|
+
* `rustc` errors from `mach build` can land on stdout rather than stderr,
|
|
123
|
+
* and the eval run's Darwin 25 `_CharT` hint pattern matched the captured
|
|
124
|
+
* text but our pre-0.16 code only fed `result.stderr` into the scanner, so
|
|
125
|
+
* the hint never fired.
|
|
126
|
+
*/
|
|
127
|
+
function surfaceMachErrorHints(result) {
|
|
128
|
+
const combined = `${result.stderr}\n${result.stdout}`;
|
|
129
|
+
const hints = explainMachError(combined);
|
|
121
130
|
if (hints.length === 0)
|
|
122
131
|
return;
|
|
123
132
|
for (const hint of hints) {
|
|
@@ -139,7 +148,7 @@ export async function build(engineDir, jobs) {
|
|
|
139
148
|
}
|
|
140
149
|
const result = await runMachInheritCapture(args, engineDir);
|
|
141
150
|
if (result.exitCode !== 0) {
|
|
142
|
-
surfaceMachErrorHints(result
|
|
151
|
+
surfaceMachErrorHints(result);
|
|
143
152
|
}
|
|
144
153
|
return result.exitCode;
|
|
145
154
|
}
|
|
@@ -152,7 +161,7 @@ export async function build(engineDir, jobs) {
|
|
|
152
161
|
export async function buildUI(engineDir) {
|
|
153
162
|
const result = await runMachInheritCapture(['build', 'faster'], engineDir);
|
|
154
163
|
if (result.exitCode !== 0) {
|
|
155
|
-
surfaceMachErrorHints(result
|
|
164
|
+
surfaceMachErrorHints(result);
|
|
156
165
|
}
|
|
157
166
|
return result.exitCode;
|
|
158
167
|
}
|
|
@@ -119,6 +119,22 @@ function getUnregistrableAdvice(filePath) {
|
|
|
119
119
|
'"fireforge wire <name> --dom <path>" to insert the #include, or add the directive manually ' +
|
|
120
120
|
'in the top-level chrome document.');
|
|
121
121
|
}
|
|
122
|
+
// xpcshell.toml manifests scaffolded by `furnace create --test-style
|
|
123
|
+
// xpcshell` or `furnace chrome-doc create --with-tests` live under
|
|
124
|
+
// `browser/base/content/test/<binary-name>-xpcshell/<component>/`. The
|
|
125
|
+
// scaffold intentionally does NOT auto-register — wiring
|
|
126
|
+
// `XPCSHELL_TESTS_MANIFESTS` requires a deliberate choice about which
|
|
127
|
+
// moz.build should own the entry. `register` surfaces that same guidance
|
|
128
|
+
// instead of falling through to browser.toml-centric advice, which was
|
|
129
|
+
// the eval finding (operator saw "register browser.toml" hint for a
|
|
130
|
+
// path that was xpcshell-shaped and contained no browser.toml).
|
|
131
|
+
if (filePath.endsWith('/xpcshell.toml')) {
|
|
132
|
+
return (`xpcshell.toml manifests are not auto-registered by "fireforge register". ` +
|
|
133
|
+
`Add "XPCSHELL_TESTS_MANIFESTS += ['${basename(filePath)}']" to the ` +
|
|
134
|
+
`moz.build that covers ${filePath.replace(/\/xpcshell\.toml$/, '/')}, ` +
|
|
135
|
+
'or let `furnace create --test-style xpcshell` print the warning that names the canonical wiring location. ' +
|
|
136
|
+
'Browser-chrome tests (browser.toml) are auto-registered via "fireforge register <path>/browser.toml".');
|
|
137
|
+
}
|
|
122
138
|
const testMatch = filePath.match(/^browser\/base\/content\/test\/([^/]+)\/(?!browser\.toml$).+$/);
|
|
123
139
|
if (testMatch) {
|
|
124
140
|
const dir = testMatch[1];
|