@hominis/fireforge 0.17.0 → 0.18.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 +37 -0
- package/README.md +40 -20
- package/dist/src/commands/build.js +18 -4
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +9 -11
- package/dist/src/commands/export-all.js +11 -3
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/remove.js +8 -0
- package/dist/src/commands/furnace/rename.js +23 -4
- package/dist/src/commands/lint.js +19 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/test.js +53 -12
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/wire.js +16 -0
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/mach.d.ts +12 -6
- package/dist/src/core/mach.js +12 -6
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +89 -14
- package/dist/src/core/patch-manifest-consistency.js +15 -2
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
5
5
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
6
6
|
import { hasChanges, isGitRepository } from '../core/git.js';
|
|
7
7
|
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
|
-
import { getWorkingTreeStatus } from '../core/git-status.js';
|
|
8
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
10
|
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
11
11
|
import { buildPatchQueueContext, collectNewFileCreatorsByPath, detectNewFilesInDiff, } from '../core/patch-lint.js';
|
|
@@ -41,7 +41,14 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
|
|
|
41
41
|
const prefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
42
42
|
if (prefixes.size === 0)
|
|
43
43
|
return new Set();
|
|
44
|
-
|
|
44
|
+
// Expand collapsed `?? dir/` entries before matching against Furnace
|
|
45
|
+
// prefixes — otherwise a Furnace-introduced directory slips past the
|
|
46
|
+
// filter and later lands in the non-Furnace path list that feeds the
|
|
47
|
+
// aggregate diff, where `getDiffForFilesAgainstHead` crashes with
|
|
48
|
+
// EISDIR (eval finding: export-all unusable on a fresh project with
|
|
49
|
+
// Furnace scaffolding).
|
|
50
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
51
|
+
const changedFiles = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
45
52
|
const furnaceManagedFiles = changedFiles
|
|
46
53
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
47
54
|
.filter((file) => [...prefixes].some((prefix) => file.startsWith(prefix)));
|
|
@@ -129,7 +136,8 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
129
136
|
// output shape aligned with the single-file `export` command.
|
|
130
137
|
let diff;
|
|
131
138
|
if (furnaceExcluded.size > 0) {
|
|
132
|
-
const
|
|
139
|
+
const rawChanged = await getWorkingTreeStatus(paths.engine);
|
|
140
|
+
const allChanged = await expandUntrackedDirectoryEntries(paths.engine, rawChanged);
|
|
133
141
|
const nonFurnacePaths = [
|
|
134
142
|
...new Set(allChanged
|
|
135
143
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
@@ -17,8 +17,14 @@ import type { SpinnerHandle } from '../utils/logger.js';
|
|
|
17
17
|
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
18
18
|
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
19
19
|
* bundle that genuinely cannot be split.
|
|
20
|
+
* @param patchTier - Optional explicit tier override (threaded from
|
|
21
|
+
* `PatchMetadata.tier`). Forces the branding-tier thresholds when
|
|
22
|
+
* set, independent of the auto-detect allowlist. When the branding
|
|
23
|
+
* tier is applied (either via this opt-in or the auto-detect), a
|
|
24
|
+
* single `info()` line surfaces the choice so the tier decision is
|
|
25
|
+
* visible rather than silent.
|
|
20
26
|
*/
|
|
21
|
-
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string
|
|
27
|
+
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<void>;
|
|
22
28
|
/**
|
|
23
29
|
* Resolves patch metadata interactively or from flags, with shared validation.
|
|
24
30
|
* @param options - Export command options
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { confirm, select, text } from '@clack/prompts';
|
|
4
4
|
import { addLicenseHeaderToFile, getLicenseHeader } from '../core/license-headers.js';
|
|
5
5
|
import { findAllPatchesForFiles } from '../core/patch-export.js';
|
|
6
|
-
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, } from '../core/patch-lint.js';
|
|
6
|
+
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
7
7
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
8
8
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
9
|
import { pathExists, readText } from '../utils/fs.js';
|
|
@@ -24,9 +24,27 @@ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../ut
|
|
|
24
24
|
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
25
25
|
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
26
26
|
* bundle that genuinely cannot be split.
|
|
27
|
+
* @param patchTier - Optional explicit tier override (threaded from
|
|
28
|
+
* `PatchMetadata.tier`). Forces the branding-tier thresholds when
|
|
29
|
+
* set, independent of the auto-detect allowlist. When the branding
|
|
30
|
+
* tier is applied (either via this opt-in or the auto-detect), a
|
|
31
|
+
* single `info()` line surfaces the choice so the tier decision is
|
|
32
|
+
* visible rather than silent.
|
|
27
33
|
*/
|
|
28
|
-
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks) {
|
|
29
|
-
|
|
34
|
+
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks, patchTier) {
|
|
35
|
+
// Compute the tier decision independently of the lint pipeline so the
|
|
36
|
+
// decision can be surfaced even when the rule body emitted no issues
|
|
37
|
+
// (e.g. a branding patch under the soft threshold still benefits from
|
|
38
|
+
// operators knowing which tier governed the run). The same helper is
|
|
39
|
+
// reused inside `lintPatchSize`, so the surfaced tier and the tier
|
|
40
|
+
// that actually drove the thresholds never drift.
|
|
41
|
+
const tierDecision = resolvePatchSizeTier(filesAffected, patchTier);
|
|
42
|
+
if (tierDecision.tier === 'branding') {
|
|
43
|
+
info(tierDecision.source === 'explicit'
|
|
44
|
+
? 'Lint: branding threshold tier applied via patches.json `tier: "branding"` opt-in.'
|
|
45
|
+
: 'Lint: branding threshold tier applied (patch is all under browser/branding/ plus registration siblings).');
|
|
46
|
+
}
|
|
47
|
+
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx, ignoreChecks, patchTier);
|
|
30
48
|
if (issues.length === 0)
|
|
31
49
|
return;
|
|
32
50
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
@@ -79,17 +79,14 @@ async function copyOverrideFiles(engineDir, srcDir, destDir, componentName, hasF
|
|
|
79
79
|
return copiedFiles;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Writes override metadata to disk and updates furnace.json with the new
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* @param details - Source component metadata from the engine scan
|
|
89
|
-
* @param firefoxVersion - Firefox version recorded in the workspace config
|
|
90
|
-
* @param config - Mutable Furnace config object to update
|
|
82
|
+
* Writes override metadata to disk and updates furnace.json with the new
|
|
83
|
+
* override entry. Re-reads the current on-disk furnace.json inside the
|
|
84
|
+
* operation lock and splices the new entry onto the fresh state so two
|
|
85
|
+
* concurrent `furnace override` commands cannot race their read-modify
|
|
86
|
+
* -write cycles into a single surviving entry (eval 2: parallel overrides
|
|
87
|
+
* both reported success but furnace.json kept only the second writer).
|
|
91
88
|
*/
|
|
92
|
-
async function saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, firefoxVersion,
|
|
89
|
+
async function saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, firefoxVersion, journal, baseCommit) {
|
|
93
90
|
const overrideJson = {
|
|
94
91
|
type: overrideType,
|
|
95
92
|
description,
|
|
@@ -100,14 +97,27 @@ async function saveOverrideConfig(projectRoot, destDir, componentName, overrideT
|
|
|
100
97
|
const overrideJsonPath = join(destDir, 'override.json');
|
|
101
98
|
await snapshotFile(journal, overrideJsonPath);
|
|
102
99
|
await writeJson(overrideJsonPath, overrideJson);
|
|
103
|
-
|
|
100
|
+
// Re-read the current furnace.json inside the lock. The outer caller
|
|
101
|
+
// loaded a snapshot before entering `runFurnaceMutation`, but another
|
|
102
|
+
// furnace mutation (override / init / sync) may have landed in between
|
|
103
|
+
// — writing back the stale snapshot would drop that concurrent write.
|
|
104
|
+
const freshConfig = await loadAuthoringFurnaceConfig(projectRoot);
|
|
105
|
+
freshConfig.overrides[componentName] = {
|
|
104
106
|
type: overrideType,
|
|
105
107
|
description,
|
|
106
108
|
basePath: details.sourcePath,
|
|
107
109
|
baseVersion: firefoxVersion,
|
|
108
110
|
...(baseCommit ? { baseCommit } : {}),
|
|
109
111
|
};
|
|
110
|
-
|
|
112
|
+
// Promote from the stock bucket here, against the fresh state, so the
|
|
113
|
+
// stock→override transition survives even when another concurrent
|
|
114
|
+
// override already rewrote furnace.json between the outer read and
|
|
115
|
+
// this write.
|
|
116
|
+
const stockIndex = freshConfig.stock.indexOf(componentName);
|
|
117
|
+
if (stockIndex !== -1) {
|
|
118
|
+
freshConfig.stock.splice(stockIndex, 1);
|
|
119
|
+
}
|
|
120
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
111
121
|
}
|
|
112
122
|
/**
|
|
113
123
|
* Performs the transactional mutation phase of furnace override under the
|
|
@@ -123,7 +133,7 @@ async function performOverrideMutations(args) {
|
|
|
123
133
|
try {
|
|
124
134
|
const filesCopied = await copyOverrideFiles(args.engineDir, args.srcDir, args.destDir, args.componentName, args.details.hasFTL, args.overrideType, args.ftlDir, journal);
|
|
125
135
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
126
|
-
await saveOverrideConfig(args.projectRoot, args.destDir, args.componentName, args.overrideType, args.description, args.details, args.firefoxVersion,
|
|
136
|
+
await saveOverrideConfig(args.projectRoot, args.destDir, args.componentName, args.overrideType, args.description, args.details, args.firefoxVersion, journal, args.baseCommit);
|
|
127
137
|
return filesCopied;
|
|
128
138
|
}
|
|
129
139
|
catch (error) {
|
|
@@ -3,6 +3,7 @@ import { readdir, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { confirm } from '@clack/prompts';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
6
|
+
import { removeCustomFtlJarMnEntry } from '../../core/furnace-apply-ftl.js';
|
|
6
7
|
import { extractComponentChecksums, getOverrideEngineTargetPath, isOverrideCopyCandidate, restoreOverrideFileToBaseline, } from '../../core/furnace-apply-helpers.js';
|
|
7
8
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
8
9
|
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
@@ -353,6 +354,13 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
353
354
|
await removeFile(ftlPath);
|
|
354
355
|
info(`Deleted localized file engine/${ftlRel}`);
|
|
355
356
|
}
|
|
357
|
+
// Drop the locale jar.mn chrome registration that `applyCustomFtlFile`
|
|
358
|
+
// wrote during deploy — otherwise the engine is left with a
|
|
359
|
+
// `locale/.../${name}.ftl` entry pointing at a file we just
|
|
360
|
+
// deleted. 2026-04-21 eval (Finding #1): `furnace remove` left
|
|
361
|
+
// `browser/locales/jar.mn` referencing the missing FTL, which
|
|
362
|
+
// would break the next package-manifest validation.
|
|
363
|
+
await removeCustomFtlJarMnEntry(paths.engine, `${name}.ftl`, ftlDir, customConfig, journal);
|
|
356
364
|
}
|
|
357
365
|
}
|
|
358
366
|
let testCleanupFailures = [];
|
|
@@ -3,9 +3,9 @@ import { readdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
5
|
import { getFurnacePaths, loadFurnaceConfig, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
|
-
import { isComponentSourceFile, resolveFtlDir, tagNameToClassName, } from '../../core/furnace-constants.js';
|
|
6
|
+
import { isComponentSourceFile, resolveFtlChromeSubPath, resolveFtlDir, resolveFtlLocaleJarMnPath, tagNameToClassName, } from '../../core/furnace-constants.js';
|
|
7
7
|
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
8
|
-
import { addCustomElementRegistration, addJarMnEntries, removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
8
|
+
import { addCustomElementRegistration, addJarMnEntries, addLocaleFtlJarMnEntry, removeCustomElementRegistration, removeJarMnEntries, removeLocaleFtlJarMnEntry, } from '../../core/furnace-registration.js';
|
|
9
9
|
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
10
10
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
11
11
|
import { getStoriesDir } from '../../core/furnace-stories.js';
|
|
@@ -272,7 +272,8 @@ async function performRenameMutations(args) {
|
|
|
272
272
|
// 3. Update engine registrations (custom components only)
|
|
273
273
|
if (isCustom && config.custom[newName]?.register && (await pathExists(args.engineDir))) {
|
|
274
274
|
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
275
|
-
|
|
275
|
+
const isLocalized = config.custom[newName].localized;
|
|
276
|
+
await updateEngineRegistrations(args.engineDir, oldName, newName, newDir, ftlDir, isLocalized, journal);
|
|
276
277
|
}
|
|
277
278
|
// 4. Re-key furnace-state.json checksums from old name to new name
|
|
278
279
|
await rekeyStateChecksums(args.projectRoot, componentType, oldName, newName);
|
|
@@ -358,7 +359,7 @@ async function rekeyStateChecksums(projectRoot, componentType, oldName, newName)
|
|
|
358
359
|
return result;
|
|
359
360
|
});
|
|
360
361
|
}
|
|
361
|
-
async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ftlDir, journal) {
|
|
362
|
+
async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ftlDir, isLocalized, journal) {
|
|
362
363
|
const customElementsPath = join(engineDir, 'toolkit/content/customElements.js');
|
|
363
364
|
const jarMnPath = join(engineDir, 'toolkit/content/jar.mn');
|
|
364
365
|
if (await pathExists(customElementsPath)) {
|
|
@@ -386,6 +387,24 @@ async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ft
|
|
|
386
387
|
await writeText(newFtlPath, ftlContent);
|
|
387
388
|
await removeFile(oldFtlPath);
|
|
388
389
|
}
|
|
390
|
+
// Re-wire the locale jar.mn chrome registration when the component is
|
|
391
|
+
// localized. Before this, `updateEngineRegistrations` renamed the .ftl
|
|
392
|
+
// file on disk but left the locale jar.mn pointing at
|
|
393
|
+
// `locale/.../${oldName}.ftl`, so `furnace validate` passed while the
|
|
394
|
+
// engine still carried a stale registration for the now-missing file
|
|
395
|
+
// (eval finding: stale old-name registration after rename).
|
|
396
|
+
if (isLocalized) {
|
|
397
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
398
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
399
|
+
if (chromeSubPath !== undefined && localeJarRel !== undefined) {
|
|
400
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
401
|
+
if (await pathExists(localeJarAbs)) {
|
|
402
|
+
await snapshotFile(journal, localeJarAbs);
|
|
403
|
+
await removeLocaleFtlJarMnEntry(engineDir, localeJarRel, oldName, chromeSubPath);
|
|
404
|
+
await addLocaleFtlJarMnEntry(engineDir, localeJarRel, newName, chromeSubPath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
389
408
|
}
|
|
390
409
|
/**
|
|
391
410
|
* Renames a custom or override component atomically: updates directory name,
|
|
@@ -5,9 +5,9 @@ import { isBrandingManagedPath } from '../core/branding.js';
|
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { getStatusWithCodes, hasChanges, isGitRepository } from '../core/git.js';
|
|
7
7
|
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
|
-
import {
|
|
8
|
+
import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
|
-
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
10
|
+
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
11
11
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
12
12
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
13
13
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -103,10 +103,17 @@ async function resolveLintDiff(engineDir, files, binaryName) {
|
|
|
103
103
|
// survive in the patch queue as-is. The exclusion mirrors the
|
|
104
104
|
// `branding` bucket in `fireforge status` so the two views stay
|
|
105
105
|
// consistent.
|
|
106
|
+
//
|
|
107
|
+
// `expandUntrackedDirectoryEntries` promotes collapsed `?? dir/`
|
|
108
|
+
// status rows to individual file entries before the diff pass.
|
|
109
|
+
// Without it, a patch that introduces a new directory shows up as
|
|
110
|
+
// `?? browser/modules/<fork>/` and `getDiffForFilesAgainstHead`
|
|
111
|
+
// crashed with EISDIR reading the directory as if it were a file
|
|
112
|
+
// (eval finding: aggregate lint unusable on a real imported queue).
|
|
106
113
|
if (binaryName) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const allPaths = [...new Set(
|
|
114
|
+
const rawStatus = await getWorkingTreeStatus(engineDir);
|
|
115
|
+
const expanded = await expandUntrackedDirectoryEntries(engineDir, rawStatus);
|
|
116
|
+
const allPaths = [...new Set(expanded.map((entry) => entry.file))];
|
|
110
117
|
const nonBrandingPaths = allPaths.filter((path) => !isBrandingManagedPath(path, binaryName));
|
|
111
118
|
const excludedCount = allPaths.length - nonBrandingPaths.length;
|
|
112
119
|
if (excludedCount > 0) {
|
|
@@ -315,7 +322,13 @@ async function lintPerPatch(projectRoot, paths) {
|
|
|
315
322
|
if (!diff.trim())
|
|
316
323
|
continue;
|
|
317
324
|
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
318
|
-
const
|
|
325
|
+
const decision = resolvePatchSizeTier(existing, patch.tier);
|
|
326
|
+
if (decision.tier === 'branding') {
|
|
327
|
+
info(decision.source === 'explicit'
|
|
328
|
+
? `${patch.filename}: branding threshold tier applied via patches.json \`tier: "branding"\` opt-in.`
|
|
329
|
+
: `${patch.filename}: branding threshold tier applied (all files under browser/branding/ plus registration siblings).`);
|
|
330
|
+
}
|
|
331
|
+
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier);
|
|
319
332
|
for (const issue of patchIssues) {
|
|
320
333
|
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
321
334
|
}
|
|
@@ -39,7 +39,10 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
39
39
|
}
|
|
40
40
|
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
41
41
|
if (!target) {
|
|
42
|
-
|
|
42
|
+
const available = manifest.patches
|
|
43
|
+
.map((p) => p.name && p.name !== p.filename ? `${p.filename} (name: ${p.name})` : p.filename)
|
|
44
|
+
.join(', ');
|
|
45
|
+
throw new InvalidArgumentError(`Patch "${identifier}" not found. Accepted identifiers: ordinal (e.g. 2), filename (e.g. 002-ui-foo.patch), or manifest name (e.g. ui-foo). Available: ${available}`, identifier);
|
|
43
46
|
}
|
|
44
47
|
// Build the full queue context once so we can scan each patch's newFiles
|
|
45
48
|
// without re-parsing for the dependency check below.
|
|
@@ -270,7 +270,10 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
270
270
|
}
|
|
271
271
|
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
272
272
|
if (!target) {
|
|
273
|
-
|
|
273
|
+
const available = manifest.patches
|
|
274
|
+
.map((p) => p.name && p.name !== p.filename ? `${p.filename} (name: ${p.name})` : p.filename)
|
|
275
|
+
.join(', ');
|
|
276
|
+
throw new InvalidArgumentError(`Patch "${identifier}" not found. Accepted identifiers: ordinal (e.g. 2), filename (e.g. 002-ui-foo.patch), or manifest name (e.g. ui-foo). Available: ${available}`, identifier);
|
|
274
277
|
}
|
|
275
278
|
const { destinationOrder, anchorFilename } = resolveDestination(target, manifest.patches, options);
|
|
276
279
|
const renameMap = computeRenameMap(manifest.patches, target, destinationOrder);
|
|
@@ -75,8 +75,10 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
75
75
|
// `lintIgnore` threads through so a shrink of an advisory-noisy-but-
|
|
76
76
|
// intentional patch (branding bundle, localised-resource pack) does not
|
|
77
77
|
// have to choose between `--skip-lint` (blunt) and the full rebase path.
|
|
78
|
+
// `target.tier` threads the explicit branding-threshold opt-in for
|
|
79
|
+
// the branding patch that also touches a non-allowlisted sibling.
|
|
78
80
|
const ignoreChecks = target.lintIgnore?.length ? new Set(target.lintIgnore) : undefined;
|
|
79
|
-
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks);
|
|
81
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks, target.tier);
|
|
80
82
|
// Project the cross-patch context: replace the target entry with its
|
|
81
83
|
// would-be shrunken self (new diff + new newFiles + new
|
|
82
84
|
// modifiedFileAdditions). The projected entry must repopulate both
|
|
@@ -201,8 +201,11 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
201
201
|
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
202
202
|
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
203
203
|
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
204
|
+
// The paired `patch.tier` threads the explicit branding-threshold
|
|
205
|
+
// opt-in the same way, for the branding patch that also touches a
|
|
206
|
+
// non-allowlisted registration sibling.
|
|
204
207
|
const ignoreChecks = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
205
|
-
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks);
|
|
208
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks, patch.tier);
|
|
206
209
|
if (isDryRun) {
|
|
207
210
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
208
211
|
}
|
|
@@ -45,6 +45,17 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
const result = await registerFile(projectRoot, engineRelativePath, options.dryRun, options.after);
|
|
47
47
|
if (options.dryRun) {
|
|
48
|
+
// 2026-04-21 eval (Finding #8): dry-run always said "Would
|
|
49
|
+
// register" even when the rule's idempotency check already knew
|
|
50
|
+
// the entry was present, so automation read the plan as "work to
|
|
51
|
+
// do" and the following real run then reported "Already
|
|
52
|
+
// registered". Surface the idempotency decision in dry-run too so
|
|
53
|
+
// the plan mirrors the real command's outcome.
|
|
54
|
+
if (result.skipped) {
|
|
55
|
+
info(`[dry-run] Already registered: ${engineRelativePath} in ${result.manifest}`);
|
|
56
|
+
outro('Dry run complete');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
info(`[dry-run] Would register ${engineRelativePath}`);
|
|
49
60
|
info(` manifest: ${result.manifest}`);
|
|
50
61
|
info(` entry: ${result.entry}`);
|
|
@@ -10,7 +10,7 @@ import { operatorAlreadySetAppPath, resolveXpcshellAppdirArg, } from '../core/xp
|
|
|
10
10
|
import { GeneralError } from '../errors/base.js';
|
|
11
11
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
12
12
|
import { pathExists } from '../utils/fs.js';
|
|
13
|
-
import { info, intro, outro, spinner, warn } from '../utils/logger.js';
|
|
13
|
+
import { info, intro, outro, spinner, success, warn } from '../utils/logger.js';
|
|
14
14
|
import { pickDefined } from '../utils/options.js';
|
|
15
15
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
16
16
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
@@ -47,6 +47,32 @@ function hasStaleBuildArtifactsSignal(output) {
|
|
|
47
47
|
return (/chrome:\/\/branding\/locale\/brand\.properties/i.test(output) ||
|
|
48
48
|
/browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Fork-module-not-registered signal. 2026-04-21 eval Finding #14:
|
|
52
|
+
* a hominis test failed with `Failed to load resource:///modules/hominis/
|
|
53
|
+
* HominisStore.sys.mjs`. The branding pattern happened to also match
|
|
54
|
+
* because the test harness printed a branding warning during its
|
|
55
|
+
* teardown, and the stale-build branch won by precedence — telling the
|
|
56
|
+
* operator to rebuild when the real fix is to register the module in
|
|
57
|
+
* the fork's `browser/modules/<binary>/moz.build`. Match a
|
|
58
|
+
* `resource:///modules/<binaryName>/` pattern so fork-owned module
|
|
59
|
+
* failures surface the right diagnosis.
|
|
60
|
+
*/
|
|
61
|
+
function hasForkModuleSignal(output, binaryName) {
|
|
62
|
+
const pattern = new RegExp(`Failed to load resource:\\/\\/\\/modules\\/${binaryName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\/`, 'i');
|
|
63
|
+
return pattern.test(output);
|
|
64
|
+
}
|
|
65
|
+
function buildForkModuleMessage(binaryName) {
|
|
66
|
+
return (`Test failed to load a fork-owned module at resource:///modules/${binaryName}/*.sys.mjs.\n\n` +
|
|
67
|
+
'This is almost always a module-registration issue, not a stale build. The fork module directory is missing an entry that maps its file into the resource URI tree, so `ChromeUtils.importESModule` cannot resolve it.\n\n' +
|
|
68
|
+
'Check that:\n' +
|
|
69
|
+
` - browser/modules/${binaryName}/moz.build lists the missing module in EXTRA_JS_MODULES.\n` +
|
|
70
|
+
` - browser/modules/moz.build references the ${binaryName}/ subdirectory (DIRS += [...]).\n` +
|
|
71
|
+
' - The last `fireforge build` (or `fireforge build --ui`) completed successfully against the current manifests. If the registration is new, the UI-faster build path may not pick it up — a full build may be required.\n\n' +
|
|
72
|
+
'Use `fireforge register browser/modules/' +
|
|
73
|
+
binaryName +
|
|
74
|
+
'/<file>.sys.mjs` to add the EXTRA_JS_MODULES entry if it is missing.');
|
|
75
|
+
}
|
|
50
76
|
// Detects the broader xpcshell symptom where every `resource:///modules/...`
|
|
51
77
|
// import fails — the signature of xpcshell running with the wrong app-dir on
|
|
52
78
|
// a manifest that sets `firefox-appdir = "browser"`. Checked AFTER the
|
|
@@ -87,13 +113,22 @@ function buildMochitestHttp3ServerMessage() {
|
|
|
87
113
|
" - The `BROWSER_CHROME_MANIFESTS` entry for your fork's chrome.manifest is registered.\n\n" +
|
|
88
114
|
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
89
115
|
}
|
|
90
|
-
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted) {
|
|
116
|
+
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName) {
|
|
91
117
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
92
118
|
return;
|
|
93
119
|
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
94
120
|
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
95
121
|
throw new GeneralError(buildUnknownTestMessage(normalizedPaths));
|
|
96
122
|
}
|
|
123
|
+
// Fork-owned module load failures must beat the branding stale-build
|
|
124
|
+
// branch: 2026-04-21 eval (Finding #14) saw a hominis test fail with
|
|
125
|
+
// `Failed to load resource:///modules/hominis/HominisStore.sys.mjs`
|
|
126
|
+
// while the harness teardown printed a branding warning that the old
|
|
127
|
+
// stale-build pattern matched, so the operator was told to rebuild
|
|
128
|
+
// when the real fix is to register the missing module.
|
|
129
|
+
if (hasForkModuleSignal(combinedOutput, binaryName)) {
|
|
130
|
+
throw new GeneralError(buildForkModuleMessage(binaryName));
|
|
131
|
+
}
|
|
97
132
|
// Branding-specific stale-build signals keep priority over the broader
|
|
98
133
|
// xpcshell-appdir hint: when `chrome://branding/locale/brand.properties`
|
|
99
134
|
// fails to resolve, the fix really is "rebuild", not "pass --app-path".
|
|
@@ -157,8 +192,8 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
157
192
|
if (options.build) {
|
|
158
193
|
await prepareBuildEnvironment(projectRoot, paths, projectConfig);
|
|
159
194
|
const s = spinner('Running incremental build...');
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
195
|
+
const buildResult = await buildUI(paths.engine);
|
|
196
|
+
if (buildResult.exitCode !== 0) {
|
|
162
197
|
s.error('Pre-test build failed');
|
|
163
198
|
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
164
199
|
}
|
|
@@ -200,13 +235,19 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
200
235
|
if (!preflight.ok) {
|
|
201
236
|
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
202
237
|
}
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
238
|
+
// Belt-and-suspenders: write the PASS footer via `success()`
|
|
239
|
+
// AND `outro()` AND a direct stdout write. The eval
|
|
240
|
+
// reproducibly captured the intro + info line but nothing
|
|
241
|
+
// after the preflight returned, which we believe is a
|
|
242
|
+
// non-TTY clack rendering quirk that occasionally swallows
|
|
243
|
+
// the last log line before process exit. `success()` routes
|
|
244
|
+
// through a different clack entry point than `info()`, and
|
|
245
|
+
// `process.stdout.write` bypasses clack entirely so the
|
|
246
|
+
// PASS status is always visible in the captured output.
|
|
247
|
+
const summary = `Marionette preflight: PASS (${preflight.durationMs}ms)`;
|
|
248
|
+
success(summary);
|
|
249
|
+
outro('Test completed');
|
|
250
|
+
process.stdout.write(`${summary}\n`);
|
|
210
251
|
return;
|
|
211
252
|
}
|
|
212
253
|
if (!preflight.ok) {
|
|
@@ -256,7 +297,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
256
297
|
catch (error) {
|
|
257
298
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
258
299
|
}
|
|
259
|
-
handleNonZeroTestExit(result, normalizedPaths, appdirInjection);
|
|
300
|
+
handleNonZeroTestExit(result, normalizedPaths, appdirInjection, projectConfig.binaryName);
|
|
260
301
|
}
|
|
261
302
|
/**
|
|
262
303
|
* Resolves and (when applicable) appends an `--app-path=<abs>` arg to
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isGitRepository } from '../core/git.js';
|
|
6
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
6
7
|
import { measureTokenCoverage } from '../core/token-coverage.js';
|
|
7
8
|
import { getTokensCssPath } from '../core/token-manager.js';
|
|
8
9
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -23,8 +24,14 @@ export async function tokenCoverageCommand(projectRoot) {
|
|
|
23
24
|
}
|
|
24
25
|
const config = await loadConfig(projectRoot);
|
|
25
26
|
const tokensCssPath = getTokensCssPath(config.binaryName);
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Expand collapsed `?? dir/` untracked entries so untracked CSS files
|
|
28
|
+
// inside a new patch-added directory are included in coverage. Before
|
|
29
|
+
// this, an imported fork that added a new CSS tree saw "No modified
|
|
30
|
+
// CSS files" because `git status --porcelain` collapsed the directory
|
|
31
|
+
// and the file-extension filter could not see the .css inside.
|
|
32
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
33
|
+
const expandedStatus = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
34
|
+
const statusCssFiles = expandedStatus
|
|
28
35
|
.filter((f) => f.file.endsWith('.css') && f.file !== tokensCssPath)
|
|
29
36
|
.map((f) => f.file);
|
|
30
37
|
// Also scan CSS files deployed by Furnace custom components. Deployed
|
|
@@ -226,6 +226,15 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
226
226
|
// dry-run is meant to preview the mutation plan without requiring
|
|
227
227
|
// the subscript to already exist, matching the "plan before write"
|
|
228
228
|
// pattern operators rely on for setup scripts).
|
|
229
|
+
//
|
|
230
|
+
// Dry-run keeps the existence check advisory rather than fatal: the
|
|
231
|
+
// "wire first, create file after" workflow is a legitimate use of
|
|
232
|
+
// preview, but operators who run dry-run over a typo were surprised
|
|
233
|
+
// when the real command then refused with `Subscript file not
|
|
234
|
+
// found`. 2026-04-23 eval (Finding in eval 2): dry-run produced a
|
|
235
|
+
// plausible plan and the non-dry-run invocation then errored. The
|
|
236
|
+
// info line surfaces the mismatch in preview mode so the operator
|
|
237
|
+
// can act on the warning before re-running without --dry-run.
|
|
229
238
|
if (!options.dryRun) {
|
|
230
239
|
const paths = getProjectPaths(projectRoot);
|
|
231
240
|
const subscriptPath = join(paths.engine, subscriptDir, `${name}.js`);
|
|
@@ -234,6 +243,13 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
234
243
|
'Create the file in engine/ before wiring.', 'name');
|
|
235
244
|
}
|
|
236
245
|
}
|
|
246
|
+
else {
|
|
247
|
+
const paths = getProjectPaths(projectRoot);
|
|
248
|
+
const subscriptPath = join(paths.engine, subscriptDir, `${name}.js`);
|
|
249
|
+
if (!(await pathExists(subscriptPath))) {
|
|
250
|
+
info(`Note: ${subscriptDir}/${name}.js does not exist yet — the real wire command will require it before writing. Create the file before re-running without --dry-run.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
237
253
|
if (options.dryRun) {
|
|
238
254
|
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
|
|
239
255
|
return;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
3
|
import { GeneralError } from '../errors/base.js';
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
|
+
import { verbose } from '../utils/logger.js';
|
|
5
6
|
import { toRootRelativePath } from '../utils/paths.js';
|
|
6
|
-
import { getProjectPaths } from './config.js';
|
|
7
|
+
import { getProjectPaths, loadConfig } from './config.js';
|
|
7
8
|
import { createRollbackJournal, restoreRollbackJournal, snapshotFile } from './furnace-rollback.js';
|
|
8
9
|
import { registerBrowserContent } from './manifest-register.js';
|
|
9
10
|
import { DEFAULT_DOM_TARGET } from './wire-dom-fragment.js';
|
|
@@ -63,18 +64,34 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
63
64
|
await snapshotFile(journal, join(engineDir, effectiveDomTargetPath));
|
|
64
65
|
}
|
|
65
66
|
await snapshotFile(journal, join(engineDir, 'browser/base/jar.mn'));
|
|
67
|
+
// Compute the project-scoped patch-lint marker (`// <BINARY>:`) so
|
|
68
|
+
// every wire mutator can stamp it into the emitted comment block.
|
|
69
|
+
// Without this, `lintModificationComments` trips
|
|
70
|
+
// `missing-modification-comment` on wire-generated edits the next
|
|
71
|
+
// time the operator exports — the same tool wrote the code and a
|
|
72
|
+
// sibling tool then rejected it (eval 1 Finding #9). A broken config
|
|
73
|
+
// should not block the wire, so the fallback marker keeps the
|
|
74
|
+
// previous lint-friendly default when the config cannot be loaded.
|
|
75
|
+
let marker = 'FIREFORGE:';
|
|
76
|
+
try {
|
|
77
|
+
const config = await loadConfig(root);
|
|
78
|
+
marker = `${config.binaryName.toUpperCase()}:`;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
verbose(`Using default wire marker because fireforge.json could not be loaded: ${toError(error).message}`);
|
|
82
|
+
}
|
|
66
83
|
try {
|
|
67
84
|
// 1. Add subscript to browser-main.js
|
|
68
|
-
const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name);
|
|
85
|
+
const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name, marker);
|
|
69
86
|
// 2. Add init expression to browser-init.js (if provided)
|
|
70
87
|
let initAdded = false;
|
|
71
88
|
if (options.init) {
|
|
72
|
-
initAdded = await addInitToBrowserInit(engineDir, options.init, options.after);
|
|
89
|
+
initAdded = await addInitToBrowserInit(engineDir, options.init, options.after, marker);
|
|
73
90
|
}
|
|
74
91
|
// 3. Add destroy expression to browser-init.js onUnload() (if provided)
|
|
75
92
|
let destroyAdded = false;
|
|
76
93
|
if (options.destroy) {
|
|
77
|
-
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
94
|
+
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy, marker);
|
|
78
95
|
}
|
|
79
96
|
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
80
97
|
let domInserted = false;
|
|
@@ -94,6 +94,16 @@ export function isPackageablePath(sourcePath) {
|
|
|
94
94
|
}
|
|
95
95
|
if (BUILD_INPUT_BASENAMES.has(basename(sourcePath)))
|
|
96
96
|
return false;
|
|
97
|
+
// `.inc.xhtml` fragments are consumed via `#include` from a registered
|
|
98
|
+
// chrome document and resolved at packaging time — they never ship as
|
|
99
|
+
// a standalone packaged artifact. 2026-04-21 eval (Finding #11):
|
|
100
|
+
// `fireforge build --ui` after `wire --dom` flagged the wired
|
|
101
|
+
// `*.inc.xhtml` as "missing packaged artifact" even though
|
|
102
|
+
// `register` correctly refuses to register it and the operator
|
|
103
|
+
// followed the documented workflow. Mirror the same carve-out the
|
|
104
|
+
// register rules apply.
|
|
105
|
+
if (sourcePath.endsWith('.inc.xhtml'))
|
|
106
|
+
return false;
|
|
97
107
|
for (const ext of PACKAGEABLE_EXTENSIONS) {
|
|
98
108
|
if (sourcePath.endsWith(ext))
|
|
99
109
|
return true;
|