@hominis/fireforge 0.30.1 → 0.31.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 +25 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +5 -15
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +36 -0
- package/dist/src/commands/export.js +47 -112
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +1 -1
- package/dist/src/commands/lint-per-patch.js +110 -81
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +96 -84
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +58 -0
- package/dist/src/commands/test-run.js +88 -0
- package/dist/src/commands/test.js +169 -257
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +48 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +171 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-file-ops.d.ts +0 -12
- package/dist/src/core/git-file-ops.js +2 -2
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +5 -1
- package/dist/src/core/mach.js +6 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.js +53 -7
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.js +132 -125
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.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-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
- package/dist/src/core/test-xpcshell-retry.js +4 -2
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +0 -21
- package/dist/src/core/typecheck-shim.js +26 -4
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +105 -0
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -144,39 +144,12 @@ async function ensureSubscriptSourceExists(projectRoot, subscriptDir, name, dryR
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* @param options - Command options
|
|
147
|
+
* Validates the `--dom` fragment argument and computes its engine-root-
|
|
148
|
+
* relative path. Accepts absolute, repo-root-relative (`engine/...`), and
|
|
149
|
+
* engine-relative inputs; rejects missing files and paths escaping
|
|
150
|
+
* engine/. Returns undefined when `--dom` was not supplied.
|
|
152
151
|
*/
|
|
153
|
-
|
|
154
|
-
intro('Wire');
|
|
155
|
-
validateWireName(name);
|
|
156
|
-
if (options.after !== undefined) {
|
|
157
|
-
// --after references an existing init block by its subscript name, so
|
|
158
|
-
// it must follow the same naming rules as `name` itself. Without this
|
|
159
|
-
// check, a caller could sneak a path-traversal segment in through
|
|
160
|
-
// --after and have it forwarded unchanged to the lookup layer.
|
|
161
|
-
validateWireName(options.after);
|
|
162
|
-
}
|
|
163
|
-
// Validate init/destroy expressions BEFORE the dry-run/real fork so
|
|
164
|
-
// both paths enforce the same contract. Pre-0.16.0, validation only
|
|
165
|
-
// ran inside `addInitToBrowserInit`/`addDestroyToBrowserInit` (the
|
|
166
|
-
// real-execution path), so `--dry-run --init 'void 0'` succeeded and
|
|
167
|
-
// rendered a plausible-looking preview even though the real run would
|
|
168
|
-
// reject the same arguments. Dropping `void 0` into the template
|
|
169
|
-
// silently (or breaking out of the string literal) was already
|
|
170
|
-
// prevented downstream — this hoist just makes the failure surface
|
|
171
|
-
// identical in preview mode.
|
|
172
|
-
if (options.init !== undefined) {
|
|
173
|
-
validateWireExpression(options.init, 'init expression');
|
|
174
|
-
}
|
|
175
|
-
if (options.destroy !== undefined) {
|
|
176
|
-
validateWireExpression(options.destroy, 'destroy expression');
|
|
177
|
-
}
|
|
178
|
-
consumeParserFallbackEvents();
|
|
179
|
-
const subscriptDir = await resolveWireSubscriptDir(projectRoot, options);
|
|
152
|
+
async function resolveDomFragmentPath(projectRoot, dom) {
|
|
180
153
|
// Validate DOM fragment file exists and compute path relative to engine root.
|
|
181
154
|
//
|
|
182
155
|
// Accepts three shapes:
|
|
@@ -193,12 +166,11 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
193
166
|
// packaging-breaking nonsense. For absolute inputs this pre-existing
|
|
194
167
|
// contract was fine — `toRootRelativePath` handles absolute candidates
|
|
195
168
|
// correctly — so we only strip the prefix when the input is relative.
|
|
196
|
-
|
|
197
|
-
|
|
169
|
+
if (!dom)
|
|
170
|
+
return undefined;
|
|
171
|
+
{
|
|
198
172
|
const paths = getProjectPaths(projectRoot);
|
|
199
|
-
const domCandidate = isExplicitAbsolutePath(
|
|
200
|
-
? options.dom
|
|
201
|
-
: stripEnginePrefix(options.dom);
|
|
173
|
+
const domCandidate = isExplicitAbsolutePath(dom) ? dom : stripEnginePrefix(dom);
|
|
202
174
|
// `pathExists` resolves relative paths against CWD, so an engine-
|
|
203
175
|
// relative `domCandidate` (e.g. `browser/base/content/foo.inc.xhtml`)
|
|
204
176
|
// would be probed inside the operator's shell directory rather than
|
|
@@ -213,50 +185,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
213
185
|
? domCandidate
|
|
214
186
|
: join(paths.engine, domCandidate);
|
|
215
187
|
if (!(await pathExists(domProbePath))) {
|
|
216
|
-
throw new InvalidArgumentError(`DOM fragment file not found: ${
|
|
188
|
+
throw new InvalidArgumentError(`DOM fragment file not found: ${dom}`, 'dom');
|
|
217
189
|
}
|
|
218
190
|
if (!isPathInsideRoot(paths.engine, domCandidate)) {
|
|
219
|
-
throw new InvalidArgumentError(`DOM fragment file must stay within engine/: ${
|
|
191
|
+
throw new InvalidArgumentError(`DOM fragment file must stay within engine/: ${dom}`, 'dom');
|
|
220
192
|
}
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
// Resolve the chrome document the `#include` directive will land in.
|
|
224
|
-
// Only consulted when `--dom` is supplied — we still resolve it here so
|
|
225
|
-
// the dry-run plan can print the target accurately.
|
|
226
|
-
//
|
|
227
|
-
// `stripEnginePrefix` is applied so `--target engine/browser/base/browser.xhtml`
|
|
228
|
-
// and `--target browser/base/browser.xhtml` are treated identically,
|
|
229
|
-
// matching the `--dom` normalization above. Absolute `--target` paths
|
|
230
|
-
// stay absolute (the containment check downstream rejects them).
|
|
231
|
-
const normalizedTarget = options.target !== undefined && !isExplicitAbsolutePath(options.target)
|
|
232
|
-
? stripEnginePrefix(options.target)
|
|
233
|
-
: options.target;
|
|
234
|
-
if (normalizedTarget !== undefined && !isContainedRelativePath(normalizedTarget)) {
|
|
235
|
-
throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target ?? ''}`, 'target');
|
|
193
|
+
return toRootRelativePath(paths.engine, domCandidate);
|
|
236
194
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
// Dry-run keeps the existence check advisory rather than fatal: the
|
|
247
|
-
// "wire first, create file after" workflow is a legitimate use of
|
|
248
|
-
// preview, but operators who run dry-run over a typo were surprised
|
|
249
|
-
// when the real command then refused with `Subscript file not
|
|
250
|
-
// found`. 2026-04-23 eval (Finding in eval 2): dry-run produced a
|
|
251
|
-
// plausible plan and the non-dry-run invocation then errored. The
|
|
252
|
-
// info line surfaces the mismatch in preview mode so the operator
|
|
253
|
-
// can act on the warning before re-running without --dry-run.
|
|
254
|
-
await ensureSubscriptSourceExists(projectRoot, subscriptDir, name, options.dryRun ?? false);
|
|
255
|
-
if (options.dryRun) {
|
|
256
|
-
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const result = await wireSubscript(projectRoot, name, {
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Builds the wireSubscript option bag from the command flags, omitting
|
|
198
|
+
* every key whose flag is absent (exactOptionalPropertyTypes) and the
|
|
199
|
+
* defaults the lower layer already assumes.
|
|
200
|
+
*/
|
|
201
|
+
function buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscriptDir) {
|
|
202
|
+
return {
|
|
260
203
|
...(options.init !== undefined ? { init: options.init } : {}),
|
|
261
204
|
...(options.destroy !== undefined ? { destroy: options.destroy } : {}),
|
|
262
205
|
...(domFilePath !== undefined ? { domFilePath } : {}),
|
|
@@ -264,12 +207,25 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
264
207
|
...(options.after !== undefined ? { after: options.after } : {}),
|
|
265
208
|
...(subscriptDir !== DEFAULT_BROWSER_SUBSCRIPT_DIR ? { subscriptDir } : {}),
|
|
266
209
|
dryRun: false,
|
|
267
|
-
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Surfaces any legacy parser fallbacks the wiring run consumed, so the
|
|
214
|
+
* operator knows which files were mutated by the regex path rather than
|
|
215
|
+
* the AST path.
|
|
216
|
+
*/
|
|
217
|
+
function reportParserFallbacks() {
|
|
268
218
|
const parserFallbacks = consumeParserFallbackEvents();
|
|
269
219
|
if (parserFallbacks.length > 0) {
|
|
270
220
|
const contexts = [...new Set(parserFallbacks.map((event) => event.context))];
|
|
271
221
|
info(`Legacy parser fallback was used for ${contexts.length} file${contexts.length === 1 ? '' : 's'}: ${contexts.join(', ')}`);
|
|
272
222
|
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Prints the per-mutation success/skip rows for a completed (non-dry-run)
|
|
226
|
+
* wire invocation.
|
|
227
|
+
*/
|
|
228
|
+
function reportWireResult(result, name, options, domFilePath, domTargetPath) {
|
|
273
229
|
if (result.subscriptAdded) {
|
|
274
230
|
success(`Added loadSubScript for ${name}.js to browser-main.js`);
|
|
275
231
|
}
|
|
@@ -306,6 +262,81 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
306
262
|
else {
|
|
307
263
|
success(`Registered ${name}.js in ${result.jarMnResult.manifest}`);
|
|
308
264
|
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Wires a chrome subscript into the browser.
|
|
268
|
+
*
|
|
269
|
+
* @param projectRoot - Root directory of the project
|
|
270
|
+
* @param name - Subscript name (without .js extension)
|
|
271
|
+
* @param options - Command options
|
|
272
|
+
*/
|
|
273
|
+
export async function wireCommand(projectRoot, name, options = {}) {
|
|
274
|
+
intro('Wire');
|
|
275
|
+
validateWireName(name);
|
|
276
|
+
if (options.after !== undefined) {
|
|
277
|
+
// --after references an existing init block by its subscript name, so
|
|
278
|
+
// it must follow the same naming rules as `name` itself. Without this
|
|
279
|
+
// check, a caller could sneak a path-traversal segment in through
|
|
280
|
+
// --after and have it forwarded unchanged to the lookup layer.
|
|
281
|
+
validateWireName(options.after);
|
|
282
|
+
}
|
|
283
|
+
// Validate init/destroy expressions BEFORE the dry-run/real fork so
|
|
284
|
+
// both paths enforce the same contract. Pre-0.16.0, validation only
|
|
285
|
+
// ran inside `addInitToBrowserInit`/`addDestroyToBrowserInit` (the
|
|
286
|
+
// real-execution path), so `--dry-run --init 'void 0'` succeeded and
|
|
287
|
+
// rendered a plausible-looking preview even though the real run would
|
|
288
|
+
// reject the same arguments. Dropping `void 0` into the template
|
|
289
|
+
// silently (or breaking out of the string literal) was already
|
|
290
|
+
// prevented downstream — this hoist just makes the failure surface
|
|
291
|
+
// identical in preview mode.
|
|
292
|
+
if (options.init !== undefined) {
|
|
293
|
+
validateWireExpression(options.init, 'init expression');
|
|
294
|
+
}
|
|
295
|
+
if (options.destroy !== undefined) {
|
|
296
|
+
validateWireExpression(options.destroy, 'destroy expression');
|
|
297
|
+
}
|
|
298
|
+
consumeParserFallbackEvents();
|
|
299
|
+
const subscriptDir = await resolveWireSubscriptDir(projectRoot, options);
|
|
300
|
+
const domFilePath = await resolveDomFragmentPath(projectRoot, options.dom);
|
|
301
|
+
// Resolve the chrome document the `#include` directive will land in.
|
|
302
|
+
// Only consulted when `--dom` is supplied — we still resolve it here so
|
|
303
|
+
// the dry-run plan can print the target accurately.
|
|
304
|
+
//
|
|
305
|
+
// `stripEnginePrefix` is applied so `--target engine/browser/base/browser.xhtml`
|
|
306
|
+
// and `--target browser/base/browser.xhtml` are treated identically,
|
|
307
|
+
// matching the `--dom` normalization above. Absolute `--target` paths
|
|
308
|
+
// stay absolute (the containment check downstream rejects them).
|
|
309
|
+
const normalizedTarget = options.target !== undefined && !isExplicitAbsolutePath(options.target)
|
|
310
|
+
? stripEnginePrefix(options.target)
|
|
311
|
+
: options.target;
|
|
312
|
+
if (normalizedTarget !== undefined && !isContainedRelativePath(normalizedTarget)) {
|
|
313
|
+
throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target ?? ''}`, 'target');
|
|
314
|
+
}
|
|
315
|
+
const domTargetPath = await resolveDomTargetPath(projectRoot, normalizedTarget);
|
|
316
|
+
if (domFilePath) {
|
|
317
|
+
await assertDomTargetIsWireable(projectRoot, domFilePath, domTargetPath);
|
|
318
|
+
}
|
|
319
|
+
// Verify the subscript file exists in engine/ (skip for dry-run:
|
|
320
|
+
// dry-run is meant to preview the mutation plan without requiring
|
|
321
|
+
// the subscript to already exist, matching the "plan before write"
|
|
322
|
+
// pattern operators rely on for setup scripts).
|
|
323
|
+
//
|
|
324
|
+
// Dry-run keeps the existence check advisory rather than fatal: the
|
|
325
|
+
// "wire first, create file after" workflow is a legitimate use of
|
|
326
|
+
// preview, but operators who run dry-run over a typo were surprised
|
|
327
|
+
// when the real command then refused with `Subscript file not
|
|
328
|
+
// found`. 2026-04-23 eval (Finding in eval 2): dry-run produced a
|
|
329
|
+
// plausible plan and the non-dry-run invocation then errored. The
|
|
330
|
+
// info line surfaces the mismatch in preview mode so the operator
|
|
331
|
+
// can act on the warning before re-running without --dry-run.
|
|
332
|
+
await ensureSubscriptSourceExists(projectRoot, subscriptDir, name, options.dryRun ?? false);
|
|
333
|
+
if (options.dryRun) {
|
|
334
|
+
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const result = await wireSubscript(projectRoot, name, buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscriptDir));
|
|
338
|
+
reportParserFallbacks();
|
|
339
|
+
reportWireResult(result, name, options, domFilePath, domTargetPath);
|
|
309
340
|
outro('Wiring complete');
|
|
310
341
|
}
|
|
311
342
|
/** Registers the wire command on the CLI program. */
|
|
@@ -35,16 +35,13 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { stat } from 'node:fs/promises';
|
|
37
37
|
import { basename, join } from 'node:path';
|
|
38
|
-
import { toError } from '../utils/errors.js';
|
|
39
38
|
import { pathExists } from '../utils/fs.js';
|
|
40
39
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
41
40
|
import { detectPlatformGate } from './build-audit-platform.js';
|
|
42
41
|
import { collectSameBasenameCandidates, findRegisteredTarget, resolveArtifactByRegistration, } from './build-audit-registration.js';
|
|
43
42
|
import { countTrailingSegmentMatches, isTestPath, resolveBestArtifact, } from './build-audit-resolve.js';
|
|
44
43
|
import { resolveArtifactByKnownTransform } from './build-audit-transforms.js';
|
|
45
|
-
import {
|
|
46
|
-
import { git } from './git-base.js';
|
|
47
|
-
import { getUntrackedFiles } from './git-status.js';
|
|
44
|
+
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
48
45
|
/** Path extensions that are conventionally packaged into the Firefox bundle. */
|
|
49
46
|
const PACKAGEABLE_EXTENSIONS = [
|
|
50
47
|
'.js',
|
|
@@ -114,47 +111,6 @@ export function isPackageablePath(sourcePath) {
|
|
|
114
111
|
}
|
|
115
112
|
return false;
|
|
116
113
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Collects engine-relative paths changed since the baseline's HEAD SHA.
|
|
119
|
-
* Always includes modified + untracked workdir paths. When the baseline is
|
|
120
|
-
* missing or the engine has no HEAD yet, falls back to workdir-only diffs.
|
|
121
|
-
*/
|
|
122
|
-
async function collectChangedFiles(engineDir, baseline) {
|
|
123
|
-
const collected = new Set();
|
|
124
|
-
if (baseline?.engineHeadSha) {
|
|
125
|
-
try {
|
|
126
|
-
const output = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
|
|
127
|
-
for (const line of output.split('\n')) {
|
|
128
|
-
const trimmed = line.trim();
|
|
129
|
-
if (trimmed)
|
|
130
|
-
collected.add(trimmed);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (!isMissingHeadError(error)) {
|
|
135
|
-
verbose(`Audit: could not diff against baseline SHA — ${toError(error).message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
if (await hasChanges(engineDir)) {
|
|
141
|
-
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
142
|
-
for (const line of worktreeDiff.split('\n')) {
|
|
143
|
-
const trimmed = line.trim();
|
|
144
|
-
if (trimmed)
|
|
145
|
-
collected.add(trimmed);
|
|
146
|
-
}
|
|
147
|
-
const untracked = await getUntrackedFiles(engineDir);
|
|
148
|
-
for (const file of untracked) {
|
|
149
|
-
collected.add(file);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
verbose(`Audit: could not enumerate workdir changes — ${toError(error).message}`);
|
|
155
|
-
}
|
|
156
|
-
return [...collected].sort();
|
|
157
|
-
}
|
|
158
114
|
/*
|
|
159
115
|
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
160
116
|
* when zero or multiple match. The ambiguous case is already rejected
|
|
@@ -500,7 +456,7 @@ export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
|
|
|
500
456
|
}
|
|
501
457
|
const testsRoot = await resolveTestsRoot(engineDir);
|
|
502
458
|
const testsPackaged = await hasPackagedTestsMarker(testsRoot);
|
|
503
|
-
const changed = await
|
|
459
|
+
const changed = await collectChangedEnginePaths(engineDir, baseline, 'Audit');
|
|
504
460
|
if (changed.length === 0) {
|
|
505
461
|
return summary;
|
|
506
462
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type of the last-build baseline marker, split out of `build-baseline.ts`
|
|
3
|
+
* so its consumers (`build-audit`, `build-prepare`, `test-stale-check`) can
|
|
4
|
+
* import the shape without importing the I/O module — `build-baseline.ts`
|
|
5
|
+
* itself depends on `build-audit.ts` at runtime, and a type import back
|
|
6
|
+
* into it would make the dependency graph cyclic.
|
|
7
|
+
*/
|
|
8
|
+
/** Shape of the on-disk baseline marker. */
|
|
9
|
+
export interface BuildBaseline {
|
|
10
|
+
/** SHA of engine HEAD at the time the build succeeded. */
|
|
11
|
+
engineHeadSha: string;
|
|
12
|
+
/**
|
|
13
|
+
* ISO-8601 timestamp of when the baseline was recorded. Informational —
|
|
14
|
+
* downstream code keys off `engineHeadSha` for diffs, but the timestamp
|
|
15
|
+
* helps operators reason about stale markers.
|
|
16
|
+
*/
|
|
17
|
+
builtAt: string;
|
|
18
|
+
/**
|
|
19
|
+
* The binaryName used at build time. Captured so the dist-tree audit
|
|
20
|
+
* can resolve the expected bundle root under obj-star/dist/ even when
|
|
21
|
+
* the project has since been renamed.
|
|
22
|
+
*/
|
|
23
|
+
binaryName: string;
|
|
24
|
+
/**
|
|
25
|
+
* Content hash per packageable engine path that was dirty at build
|
|
26
|
+
* time (modified-against-HEAD or untracked). Used by
|
|
27
|
+
* `checkStaleBuildForTest` to distinguish "this file's content was
|
|
28
|
+
* already in `dist/` when the build completed" from "this file has
|
|
29
|
+
* been edited since". Missing on baselines written before 0.16.0; the
|
|
30
|
+
* stale-check falls back to the path-only comparison in that case,
|
|
31
|
+
* so older baselines retain their existing behavior.
|
|
32
|
+
*
|
|
33
|
+
* Keys are engine-relative POSIX paths. Values are hex-encoded
|
|
34
|
+
* SHA-256 digests of the file contents at the moment the baseline
|
|
35
|
+
* was recorded.
|
|
36
|
+
*/
|
|
37
|
+
packageableFingerprints?: Record<string, string>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Type of the last-build baseline marker, split out of `build-baseline.ts`
|
|
4
|
+
* so its consumers (`build-audit`, `build-prepare`, `test-stale-check`) can
|
|
5
|
+
* import the shape without importing the I/O module — `build-baseline.ts`
|
|
6
|
+
* itself depends on `build-audit.ts` at runtime, and a type import back
|
|
7
|
+
* into it would make the dependency graph cyclic.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=build-baseline-types.js.map
|
|
@@ -15,37 +15,7 @@
|
|
|
15
15
|
* on successful build completion; a failed build does not update it, so a
|
|
16
16
|
* subsequent run still audits against the last known-good tree.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
export interface BuildBaseline {
|
|
20
|
-
/** SHA of engine HEAD at the time the build succeeded. */
|
|
21
|
-
engineHeadSha: string;
|
|
22
|
-
/**
|
|
23
|
-
* ISO-8601 timestamp of when the baseline was recorded. Informational —
|
|
24
|
-
* downstream code keys off `engineHeadSha` for diffs, but the timestamp
|
|
25
|
-
* helps operators reason about stale markers.
|
|
26
|
-
*/
|
|
27
|
-
builtAt: string;
|
|
28
|
-
/**
|
|
29
|
-
* The binaryName used at build time. Captured so the dist-tree audit
|
|
30
|
-
* can resolve the expected bundle root under obj-star/dist/ even when
|
|
31
|
-
* the project has since been renamed.
|
|
32
|
-
*/
|
|
33
|
-
binaryName: string;
|
|
34
|
-
/**
|
|
35
|
-
* Content hash per packageable engine path that was dirty at build
|
|
36
|
-
* time (modified-against-HEAD or untracked). Used by
|
|
37
|
-
* `checkStaleBuildForTest` to distinguish "this file's content was
|
|
38
|
-
* already in `dist/` when the build completed" from "this file has
|
|
39
|
-
* been edited since". Missing on baselines written before 0.16.0; the
|
|
40
|
-
* stale-check falls back to the path-only comparison in that case,
|
|
41
|
-
* so older baselines retain their existing behavior.
|
|
42
|
-
*
|
|
43
|
-
* Keys are engine-relative POSIX paths. Values are hex-encoded
|
|
44
|
-
* SHA-256 digests of the file contents at the moment the baseline
|
|
45
|
-
* was recorded.
|
|
46
|
-
*/
|
|
47
|
-
packageableFingerprints?: Record<string, string>;
|
|
48
|
-
}
|
|
18
|
+
import type { BuildBaseline } from './build-baseline-types.js';
|
|
49
19
|
/** Name of the last-build marker file under `.fireforge/`. */
|
|
50
20
|
export declare const BUILD_BASELINE_FILENAME = "last-build.json";
|
|
51
21
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
4
4
|
*/
|
|
5
5
|
import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
|
|
6
|
-
import type { BuildBaseline } from './build-baseline.js';
|
|
6
|
+
import type { BuildBaseline } from './build-baseline-types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Result of the build preparation phase.
|
|
9
9
|
*/
|
|
@@ -9,13 +9,11 @@ import { toError } from '../utils/errors.js';
|
|
|
9
9
|
import { pathExists } from '../utils/fs.js';
|
|
10
10
|
import { info, spinner, verbose, warn } from '../utils/logger.js';
|
|
11
11
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
12
|
+
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
12
13
|
import { applyAllComponents } from './furnace-apply.js';
|
|
13
14
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
14
15
|
import { runFurnaceMutation } from './furnace-operation.js';
|
|
15
16
|
import { cleanStories } from './furnace-stories.js';
|
|
16
|
-
import { hasChanges, isMissingHeadError } from './git.js';
|
|
17
|
-
import { git } from './git-base.js';
|
|
18
|
-
import { getUntrackedFiles } from './git-status.js';
|
|
19
17
|
import { generateMozconfig, runMach } from './mach.js';
|
|
20
18
|
/** Path fragments of files whose edits invalidate the recursive-make backend. */
|
|
21
19
|
const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
|
|
@@ -30,47 +28,6 @@ export function isBackendInvalidatingFile(path) {
|
|
|
30
28
|
}
|
|
31
29
|
return false;
|
|
32
30
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Collects engine-relative paths of files changed since the baseline's HEAD
|
|
35
|
-
* SHA plus any workdir modifications. Defensive — git failures surface as
|
|
36
|
-
* verbose lines and return the files collected so far. An empty result
|
|
37
|
-
* means "no drift we can prove" rather than "no drift occurred".
|
|
38
|
-
*/
|
|
39
|
-
async function collectBackendRelevantChanges(engineDir, baseline) {
|
|
40
|
-
const collected = new Set();
|
|
41
|
-
if (baseline.engineHeadSha) {
|
|
42
|
-
try {
|
|
43
|
-
const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
|
|
44
|
-
for (const line of diff.split('\n')) {
|
|
45
|
-
const trimmed = line.trim();
|
|
46
|
-
if (trimmed)
|
|
47
|
-
collected.add(trimmed);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (!isMissingHeadError(error)) {
|
|
52
|
-
verbose(`Auto-configure: could not diff engine against baseline — ${toError(error).message}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
if (await hasChanges(engineDir)) {
|
|
58
|
-
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
59
|
-
for (const line of worktreeDiff.split('\n')) {
|
|
60
|
-
const trimmed = line.trim();
|
|
61
|
-
if (trimmed)
|
|
62
|
-
collected.add(trimmed);
|
|
63
|
-
}
|
|
64
|
-
for (const file of await getUntrackedFiles(engineDir)) {
|
|
65
|
-
collected.add(file);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
verbose(`Auto-configure: could not enumerate workdir changes — ${toError(error).message}`);
|
|
71
|
-
}
|
|
72
|
-
return [...collected];
|
|
73
|
-
}
|
|
74
31
|
/**
|
|
75
32
|
* Runs the shared pre-flight steps for build and package commands:
|
|
76
33
|
* 1. Cleans Furnace stories from engine (prevents leaking into production)
|
|
@@ -101,7 +58,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
101
58
|
// work against a stale recursive-make backend.
|
|
102
59
|
let reconfigured = false;
|
|
103
60
|
if (options.previousBaseline) {
|
|
104
|
-
const changed = await
|
|
61
|
+
const changed = await collectChangedEnginePaths(paths.engine, options.previousBaseline, 'Auto-configure');
|
|
105
62
|
const invalidating = changed.filter(isBackendInvalidatingFile);
|
|
106
63
|
if (invalidating.length > 0) {
|
|
107
64
|
info(`Backend config changed; running backend regeneration first (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched).`);
|
|
@@ -8,14 +8,6 @@ export declare const CONFIG_FILENAME = "fireforge.json";
|
|
|
8
8
|
export declare const FIREFORGE_DIR = ".fireforge";
|
|
9
9
|
/** Name of the state file */
|
|
10
10
|
export declare const STATE_FILENAME = "state.json";
|
|
11
|
-
/** Name of the engine directory */
|
|
12
|
-
export declare const ENGINE_DIR = "engine";
|
|
13
|
-
/** Name of the patches directory */
|
|
14
|
-
export declare const PATCHES_DIR = "patches";
|
|
15
|
-
/** Name of the configs directory */
|
|
16
|
-
export declare const CONFIGS_DIR = "configs";
|
|
17
|
-
/** Name of the source directory */
|
|
18
|
-
export declare const SRC_DIR = "src";
|
|
19
11
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
20
12
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
|
|
21
13
|
/** Supported config paths that can be read or set without --force. */
|
|
@@ -10,13 +10,13 @@ export const FIREFORGE_DIR = '.fireforge';
|
|
|
10
10
|
/** Name of the state file */
|
|
11
11
|
export const STATE_FILENAME = 'state.json';
|
|
12
12
|
/** Name of the engine directory */
|
|
13
|
-
|
|
13
|
+
const ENGINE_DIR = 'engine';
|
|
14
14
|
/** Name of the patches directory */
|
|
15
|
-
|
|
15
|
+
const PATCHES_DIR = 'patches';
|
|
16
16
|
/** Name of the configs directory */
|
|
17
|
-
|
|
17
|
+
const CONFIGS_DIR = 'configs';
|
|
18
18
|
/** Name of the source directory */
|
|
19
|
-
|
|
19
|
+
const SRC_DIR = 'src';
|
|
20
20
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
21
21
|
export const SUPPORTED_CONFIG_ROOT_KEYS = [
|
|
22
22
|
'name',
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
* Project state file management (.fireforge/state.json).
|
|
3
3
|
*/
|
|
4
4
|
import type { FireForgeState } from '../types/config.js';
|
|
5
|
-
/**
|
|
6
|
-
* Validates a parsed project state object and returns a typed FireForgeState.
|
|
7
|
-
* @param data - Parsed JSON state data
|
|
8
|
-
* @returns Validated FireForgeState
|
|
9
|
-
*/
|
|
10
|
-
export declare function validateFireForgeState(data: unknown): FireForgeState;
|
|
11
5
|
/**
|
|
12
6
|
* Loads the fireforge state, or returns defaults if it doesn't exist.
|
|
13
7
|
* @param root - Root directory of the project
|
|
@@ -70,7 +70,7 @@ function sanitizeProjectState(data) {
|
|
|
70
70
|
* @param data - Parsed JSON state data
|
|
71
71
|
* @returns Validated FireForgeState
|
|
72
72
|
*/
|
|
73
|
-
|
|
73
|
+
function validateFireForgeState(data) {
|
|
74
74
|
const result = sanitizeProjectState(data);
|
|
75
75
|
if (result.issues.length > 0) {
|
|
76
76
|
throw new ConfigError(`Invalid FireForge state: ${result.issues.join('; ')}`);
|
|
@@ -28,7 +28,13 @@ function parsePatchPolicyCategory(raw, label) {
|
|
|
28
28
|
}
|
|
29
29
|
return raw;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Shared head of every patch-policy range shape: the value must be an
|
|
33
|
+
* object carrying positive integer `from`/`to` endpoints with
|
|
34
|
+
* `to >= from`. Returns the parsed record so callers can read their
|
|
35
|
+
* shape-specific fields from it.
|
|
36
|
+
*/
|
|
37
|
+
function parseRangeBounds(raw, label) {
|
|
32
38
|
let rec;
|
|
33
39
|
try {
|
|
34
40
|
rec = parseObject(raw, label);
|
|
@@ -41,6 +47,10 @@ function parsePatchPolicyRange(raw, label) {
|
|
|
41
47
|
if (to < from) {
|
|
42
48
|
throw new ConfigError(`Config field "${label}.to" must be greater than or equal to from`);
|
|
43
49
|
}
|
|
50
|
+
return { rec, from, to };
|
|
51
|
+
}
|
|
52
|
+
function parsePatchPolicyRange(raw, label) {
|
|
53
|
+
const { rec, from, to } = parseRangeBounds(raw, label);
|
|
44
54
|
return {
|
|
45
55
|
from,
|
|
46
56
|
to,
|
|
@@ -90,18 +100,7 @@ function parseReservedAllowedPatch(raw, label) {
|
|
|
90
100
|
return out;
|
|
91
101
|
}
|
|
92
102
|
function parsePatchPolicyReservedRange(raw, label) {
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
rec = parseObject(raw, label);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
throw new ConfigError(`Config field "${label}" must be an object`);
|
|
99
|
-
}
|
|
100
|
-
const from = parsePositiveRangeEndpoint(rec.raw('from'), `${label}.from`);
|
|
101
|
-
const to = parsePositiveRangeEndpoint(rec.raw('to'), `${label}.to`);
|
|
102
|
-
if (to < from) {
|
|
103
|
-
throw new ConfigError(`Config field "${label}.to" must be greater than or equal to from`);
|
|
104
|
-
}
|
|
103
|
+
const { rec, from, to } = parseRangeBounds(raw, label);
|
|
105
104
|
const allowedRaw = rec.raw('allowed');
|
|
106
105
|
if (!Array.isArray(allowedRaw)) {
|
|
107
106
|
throw new ConfigError(`Config field "${label}.allowed" must be an array`);
|