@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- 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 +46 -1
- package/dist/src/commands/export.js +52 -113
- 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 +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- 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-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.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 +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- 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 +74 -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 +191 -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-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -12
- package/dist/src/core/git-file-ops.js +84 -3
- 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 +22 -1
- package/dist/src/core/mach.js +27 -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.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- 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.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- 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 +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- 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 +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- 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 +122 -0
- package/dist/src/types/config.d.ts +11 -2
- 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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Maintains `compilerOptions.paths` entries in a consumer-owned jsconfig
|
|
4
|
+
* so typed cross-module imports of multi-file Furnace components work
|
|
5
|
+
* (field report D3).
|
|
6
|
+
*
|
|
7
|
+
* When a main widget imports a sibling helper via its deployed chrome URL
|
|
8
|
+
* (`chrome://global/content/elements/<helper>.mjs`), a wildcard module
|
|
9
|
+
* shim swallows the import: value imports degrade to `any` and
|
|
10
|
+
* `import(...).SomeType` typedefs fail with TS2694. The fix is a `paths`
|
|
11
|
+
* mapping from the chrome URL to the real workspace source. Furnace
|
|
12
|
+
* already owns the jar.mn side of that mapping, so it can maintain the
|
|
13
|
+
* jsconfig side automatically on every deploy.
|
|
14
|
+
*
|
|
15
|
+
* Ownership contract: only entries whose key starts with
|
|
16
|
+
* `chrome://global/content/elements/` AND whose mapped path resolves into
|
|
17
|
+
* the Furnace custom-components workspace are managed (added, updated,
|
|
18
|
+
* pruned). Everything else in the jsconfig — including hand-written
|
|
19
|
+
* `paths` entries pointing elsewhere — is preserved verbatim. No
|
|
20
|
+
* `baseUrl` is required or written: relative `paths` resolve against the
|
|
21
|
+
* config file's directory.
|
|
22
|
+
*/
|
|
23
|
+
import { readdir } from 'node:fs/promises';
|
|
24
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
25
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
26
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
27
|
+
import { info } from '../utils/logger.js';
|
|
28
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
29
|
+
import { getFurnacePaths } from './furnace-config.js';
|
|
30
|
+
/** Chrome URL prefix under which registered custom-element files deploy. */
|
|
31
|
+
const CHROME_ELEMENTS_URL_PREFIX = 'chrome://global/content/elements/';
|
|
32
|
+
/**
|
|
33
|
+
* Computes the desired managed `paths` entries: one per `.mjs` file of
|
|
34
|
+
* every registered custom component, keyed by its deployed chrome URL and
|
|
35
|
+
* mapped to the workspace source relative to the jsconfig directory.
|
|
36
|
+
*
|
|
37
|
+
* Workspace sources (not deployed engine copies) are the mapping target —
|
|
38
|
+
* they are the files developers edit, and they exist even when the engine
|
|
39
|
+
* has not been deployed yet.
|
|
40
|
+
*/
|
|
41
|
+
async function computeDesiredChromePathEntries(config, customDir, jsconfigAbsPath) {
|
|
42
|
+
const jsconfigDir = dirname(jsconfigAbsPath);
|
|
43
|
+
const entries = {};
|
|
44
|
+
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
45
|
+
if (!customConfig.register)
|
|
46
|
+
continue;
|
|
47
|
+
const componentDir = join(customDir, name);
|
|
48
|
+
if (!(await pathExists(componentDir)))
|
|
49
|
+
continue;
|
|
50
|
+
const files = await readdir(componentDir);
|
|
51
|
+
for (const file of files.sort()) {
|
|
52
|
+
if (!file.endsWith('.mjs'))
|
|
53
|
+
continue;
|
|
54
|
+
// Emit a `./`-prefixed relative value. TypeScript treats a bare
|
|
55
|
+
// `paths` value (`moz-widget/moz-widget.mjs`) as non-relative and
|
|
56
|
+
// rejects it without `baseUrl` (TS5090); a `./`-prefixed value
|
|
57
|
+
// resolves against the jsconfig directory with no `baseUrl` (which
|
|
58
|
+
// TS6 deprecates, TS5101). `../`-prefixed paths are already relative
|
|
59
|
+
// and left untouched.
|
|
60
|
+
const rel = normalizePathSlashes(relative(jsconfigDir, join(componentDir, file)));
|
|
61
|
+
const sourcePath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
62
|
+
entries[`${CHROME_ELEMENTS_URL_PREFIX}${file}`] = [sourcePath];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compares two `paths` values treating a leading `./` as insignificant, so
|
|
69
|
+
* the reconciler does not churn between `./x` and bare `x` forms (either
|
|
70
|
+
* direction). Used to decide whether a managed entry is stale.
|
|
71
|
+
*/
|
|
72
|
+
function samePathValue(a, b) {
|
|
73
|
+
const strip = (p) => (p.startsWith('./') ? p.slice(2) : p);
|
|
74
|
+
return strip(a) === strip(b);
|
|
75
|
+
}
|
|
76
|
+
/** True when `key`/`value` is a Furnace-managed chrome-elements mapping. */
|
|
77
|
+
function isManagedEntry(key, value, jsconfigDir, customDir) {
|
|
78
|
+
if (!key.startsWith(CHROME_ELEMENTS_URL_PREFIX))
|
|
79
|
+
return false;
|
|
80
|
+
if (!Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string')
|
|
81
|
+
return false;
|
|
82
|
+
const target = resolve(jsconfigDir, value[0]);
|
|
83
|
+
return target.startsWith(resolve(customDir) + '/') || target === resolve(customDir);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reconciles the managed `compilerOptions.paths` entries of the configured
|
|
87
|
+
* jsconfig against the current Furnace workspace. Idempotent; writes only
|
|
88
|
+
* when something actually changes; dry-run returns the diff without
|
|
89
|
+
* writing.
|
|
90
|
+
*
|
|
91
|
+
* The consumer owns the jsconfig file: a missing file is an error with
|
|
92
|
+
* guidance rather than a silent scaffold, and JSONC (comments/trailing
|
|
93
|
+
* commas) is unsupported for the managed file — `readJson` is a strict
|
|
94
|
+
* JSON parser, so the error message says so explicitly.
|
|
95
|
+
*
|
|
96
|
+
* @param root - Project root directory
|
|
97
|
+
* @param config - Loaded Furnace configuration (must carry `typecheckJsconfig`)
|
|
98
|
+
* @param options - `dryRun` skips the write but still reports the diff
|
|
99
|
+
*/
|
|
100
|
+
export async function syncFurnaceJsconfigPaths(root, config, options) {
|
|
101
|
+
const result = { added: [], updated: [], pruned: [], changed: false };
|
|
102
|
+
const jsconfigRel = config.typecheckJsconfig;
|
|
103
|
+
if (!jsconfigRel)
|
|
104
|
+
return result;
|
|
105
|
+
const jsconfigAbs = resolve(root, jsconfigRel);
|
|
106
|
+
if (!(await pathExists(jsconfigAbs))) {
|
|
107
|
+
throw new FurnaceError(`furnace.json sets "typecheckJsconfig": "${jsconfigRel}", but the file does not exist. ` +
|
|
108
|
+
'Create the jsconfig (it stays consumer-owned; Furnace only maintains the ' +
|
|
109
|
+
`"compilerOptions.paths" entries under ${CHROME_ELEMENTS_URL_PREFIX}*), ` +
|
|
110
|
+
'or remove the setting.');
|
|
111
|
+
}
|
|
112
|
+
let jsconfig;
|
|
113
|
+
try {
|
|
114
|
+
jsconfig = await readJson(jsconfigAbs);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw new FurnaceError(`Could not parse ${jsconfigRel} as JSON: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
118
|
+
'Furnace manages paths entries only in plain-JSON jsconfig files — JSONC comments ' +
|
|
119
|
+
'and trailing commas are not supported for the managed file.');
|
|
120
|
+
}
|
|
121
|
+
const furnacePaths = getFurnacePaths(root);
|
|
122
|
+
const desired = await computeDesiredChromePathEntries(config, furnacePaths.customDir, jsconfigAbs);
|
|
123
|
+
const jsconfigDir = dirname(jsconfigAbs);
|
|
124
|
+
const currentPaths = { ...(jsconfig.compilerOptions?.paths ?? {}) };
|
|
125
|
+
const nextPaths = {};
|
|
126
|
+
// Preserve every unmanaged entry verbatim; prune managed entries that are
|
|
127
|
+
// no longer desired; update managed entries whose target moved.
|
|
128
|
+
for (const [key, value] of Object.entries(currentPaths)) {
|
|
129
|
+
if (!isManagedEntry(key, value, jsconfigDir, furnacePaths.customDir)) {
|
|
130
|
+
nextPaths[key] = value;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const want = desired[key];
|
|
134
|
+
if (want === undefined) {
|
|
135
|
+
result.pruned.push(key);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Treat `./x` and bare `x` as equal so a previously-synced bare value (or
|
|
139
|
+
// a hand-written `./` prefix) is not rewritten as "stale" on every run.
|
|
140
|
+
// The existing value is kept verbatim when equivalent — no churn either
|
|
141
|
+
// way; only a genuinely different target updates (to the `./` form).
|
|
142
|
+
if (!samePathValue(value[0] ?? '', want[0] ?? '')) {
|
|
143
|
+
result.updated.push(key);
|
|
144
|
+
nextPaths[key] = want;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
nextPaths[key] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const [key, want] of Object.entries(desired)) {
|
|
151
|
+
if (!(key in nextPaths)) {
|
|
152
|
+
result.added.push(key);
|
|
153
|
+
nextPaths[key] = want;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
result.changed = result.added.length > 0 || result.updated.length > 0 || result.pruned.length > 0;
|
|
157
|
+
if (!result.changed || options?.dryRun === true)
|
|
158
|
+
return result;
|
|
159
|
+
const nextJsconfig = {
|
|
160
|
+
...jsconfig,
|
|
161
|
+
compilerOptions: {
|
|
162
|
+
...(jsconfig.compilerOptions ?? {}),
|
|
163
|
+
paths: nextPaths,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
await writeJson(jsconfigAbs, nextJsconfig);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Computes jsconfig `paths` drift for `furnace validate`: managed entries
|
|
171
|
+
* that are missing or stale relative to the current workspace. Read-only —
|
|
172
|
+
* delegates to {@link syncFurnaceJsconfigPaths} in dry-run mode.
|
|
173
|
+
*/
|
|
174
|
+
export async function findJsconfigPathsDrift(root, config) {
|
|
175
|
+
return syncFurnaceJsconfigPaths(root, config, { dryRun: true });
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Runs the jsconfig paths sync after a successful deploy/sync and reports
|
|
179
|
+
* the diff. No-op when `typecheckJsconfig` is unset. Shared by
|
|
180
|
+
* `furnace deploy` and `furnace sync` so both report identically.
|
|
181
|
+
*/
|
|
182
|
+
export async function reportJsconfigPathsSync(root, config, dryRun) {
|
|
183
|
+
if (!config.typecheckJsconfig)
|
|
184
|
+
return;
|
|
185
|
+
const sync = await syncFurnaceJsconfigPaths(root, config, { dryRun });
|
|
186
|
+
if (!sync.changed)
|
|
187
|
+
return;
|
|
188
|
+
info(`${dryRun ? '[dry-run] Would update' : 'Updated'} ${config.typecheckJsconfig} chrome-module paths: ` +
|
|
189
|
+
`+${sync.added.length} added, ~${sync.updated.length} updated, -${sync.pruned.length} pruned`);
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=furnace-jsconfig.js.map
|
|
@@ -61,20 +61,6 @@ 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
|
-
/**
|
|
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
64
|
/**
|
|
79
65
|
* Checks whether a declared component class extends a valid element base.
|
|
80
66
|
*
|
|
@@ -114,3 +100,19 @@ export declare function getTokenPrefixContext(tagName: string, type: ComponentTy
|
|
|
114
100
|
inheritedOverrideVars: Set<string>;
|
|
115
101
|
runtimeVariables: Set<string>;
|
|
116
102
|
}>;
|
|
103
|
+
/**
|
|
104
|
+
* Flags engine-side files that a previous deploy of `tagName` left behind
|
|
105
|
+
* after their workspace source was renamed or removed (field report D1).
|
|
106
|
+
*
|
|
107
|
+
* Detection keys on the furnace state file: every `appliedChecksums` entry
|
|
108
|
+
* under `custom/<tagName>/` whose workspace source no longer exists but
|
|
109
|
+
* whose engine target is still present is an orphan — the next deploy will
|
|
110
|
+
* prune it, but until then jar.mn and the deployed directory disagree with
|
|
111
|
+
* the workspace, and a re-export could capture the stale state into a patch.
|
|
112
|
+
*
|
|
113
|
+
* Custom components only: override undeploys restore the upstream baseline
|
|
114
|
+
* rather than deleting files, so "orphan" has no meaning there.
|
|
115
|
+
*/
|
|
116
|
+
export declare function findOrphanedEngineFiles(root: string, config: FurnaceConfig, tagName: string, state: {
|
|
117
|
+
appliedChecksums?: Record<string, string>;
|
|
118
|
+
}, ftlDir: string): Promise<ValidationIssue[]>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
4
|
import { getProjectPaths } from './config.js';
|
|
5
|
+
import { extractComponentChecksums } from './furnace-checksum-utils.js';
|
|
5
6
|
/** Creates a normalized validation issue object. */
|
|
6
7
|
export function createIssue(component, severity, check, message) {
|
|
7
8
|
return { component, severity, check, message };
|
|
@@ -282,7 +283,7 @@ export function hasCustomElementDefineCall(mjsContent) {
|
|
|
282
283
|
* `furnace override` of a valid upstream component fails its own
|
|
283
284
|
* `furnace validate` pass with nothing the operator can fix.
|
|
284
285
|
*/
|
|
285
|
-
|
|
286
|
+
function hasCustomElementExtendsOption(mjsContent) {
|
|
286
287
|
// Match `customElements.define(..., { ..., extends: "..." })`. Tolerant of
|
|
287
288
|
// whitespace, line breaks, and other object properties. The `[^)]*` stops
|
|
288
289
|
// the inner greedy match at the closing define call paren so a later
|
|
@@ -395,4 +396,42 @@ export async function getTokenPrefixContext(tagName, type, config, root) {
|
|
|
395
396
|
runtimeVariables,
|
|
396
397
|
};
|
|
397
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Flags engine-side files that a previous deploy of `tagName` left behind
|
|
401
|
+
* after their workspace source was renamed or removed (field report D1).
|
|
402
|
+
*
|
|
403
|
+
* Detection keys on the furnace state file: every `appliedChecksums` entry
|
|
404
|
+
* under `custom/<tagName>/` whose workspace source no longer exists but
|
|
405
|
+
* whose engine target is still present is an orphan — the next deploy will
|
|
406
|
+
* prune it, but until then jar.mn and the deployed directory disagree with
|
|
407
|
+
* the workspace, and a re-export could capture the stale state into a patch.
|
|
408
|
+
*
|
|
409
|
+
* Custom components only: override undeploys restore the upstream baseline
|
|
410
|
+
* rather than deleting files, so "orphan" has no meaning there.
|
|
411
|
+
*/
|
|
412
|
+
export async function findOrphanedEngineFiles(root, config, tagName, state, ftlDir) {
|
|
413
|
+
const customConfig = config.custom[tagName];
|
|
414
|
+
if (!customConfig)
|
|
415
|
+
return [];
|
|
416
|
+
const previous = extractComponentChecksums(state.appliedChecksums, 'custom', tagName);
|
|
417
|
+
const fileNames = Object.keys(previous);
|
|
418
|
+
if (fileNames.length === 0)
|
|
419
|
+
return [];
|
|
420
|
+
const { engine: engineDir, componentsDir } = getProjectPaths(root);
|
|
421
|
+
const componentDir = join(componentsDir, 'custom', tagName);
|
|
422
|
+
const issues = [];
|
|
423
|
+
for (const fileName of fileNames) {
|
|
424
|
+
if (await pathExists(join(componentDir, fileName)))
|
|
425
|
+
continue;
|
|
426
|
+
const enginePath = fileName.endsWith('.ftl')
|
|
427
|
+
? join(engineDir, ftlDir, fileName)
|
|
428
|
+
: join(engineDir, customConfig.targetPath, fileName);
|
|
429
|
+
if (!(await pathExists(enginePath)))
|
|
430
|
+
continue;
|
|
431
|
+
issues.push(createIssue(tagName, 'warning', 'orphaned-engine-file', `Engine file ${fileName} was deployed by a previous apply but its workspace source ` +
|
|
432
|
+
`is gone (renamed or deleted). The deployed copy${customConfig.register ? ' and any stale jar.mn entry' : ''} ` +
|
|
433
|
+
`will linger until the next deploy prunes it. Run "fireforge furnace deploy ${tagName}".`));
|
|
434
|
+
}
|
|
435
|
+
return issues;
|
|
436
|
+
}
|
|
398
437
|
//# sourceMappingURL=furnace-validate-helpers.js.map
|
|
@@ -9,6 +9,7 @@ import { stripJsComments } from '../utils/regex.js';
|
|
|
9
9
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
10
10
|
import { getFurnacePaths } from './furnace-config.js';
|
|
11
11
|
import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
|
|
12
|
+
import { expandCssFragments, listFragmentIncludes } from './furnace-css-fragments.js';
|
|
12
13
|
import { isTagInCorrectCustomElementsPlacement } from './furnace-registration-validate.js';
|
|
13
14
|
import { getTokensCssPath } from './token-manager.js';
|
|
14
15
|
/**
|
|
@@ -96,7 +97,21 @@ export async function checkRegistrationConsistency(root, name, config, ftlDir) {
|
|
|
96
97
|
status.filesInSync = false;
|
|
97
98
|
continue;
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
+
// Deploy writes CSS-with-include-directives in fragment-expanded form,
|
|
101
|
+
// so the drift oracle must compare the *expanded* source — otherwise a
|
|
102
|
+
// freshly deployed component would read as permanently drifted, and a
|
|
103
|
+
// fragment edit would never read as drifted at all.
|
|
104
|
+
let srcContent = await readText(srcPath);
|
|
105
|
+
if (entry.name.endsWith('.css') && listFragmentIncludes(srcContent).length > 0) {
|
|
106
|
+
try {
|
|
107
|
+
srcContent = (await expandCssFragments(srcContent, furnacePaths.sharedDir)).expanded;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Missing fragment: validate reports it as `missing-fragment`;
|
|
111
|
+
// for drift purposes fall back to the raw source so the compare
|
|
112
|
+
// still happens deterministically.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
100
115
|
const destContent = await readText(destPath);
|
|
101
116
|
const srcHash = createHash('sha256').update(srcContent).digest('hex');
|
|
102
117
|
const destHash = createHash('sha256').update(destContent).digest('hex');
|
|
@@ -3,10 +3,13 @@ import { readdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { pathExists } from '../utils/fs.js';
|
|
5
5
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
6
|
-
import { getFurnacePaths, loadFurnaceConfig } from './furnace-config.js';
|
|
7
|
-
import { xpcshellTestParentDir } from './furnace-constants.js';
|
|
6
|
+
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState } from './furnace-config.js';
|
|
7
|
+
import { resolveFtlDir, xpcshellTestParentDir } from './furnace-constants.js';
|
|
8
|
+
import { validateCssFragments } from './furnace-css-fragments.js';
|
|
8
9
|
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
10
|
+
import { findJsconfigPathsDrift } from './furnace-jsconfig.js';
|
|
9
11
|
import { validateAccessibility, validateCompatibility, validateJarMnEntries, validateRegistrationPatterns, validateStructure, validateTokenLink, } from './furnace-validate-checks.js';
|
|
12
|
+
import { findOrphanedEngineFiles } from './furnace-validate-helpers.js';
|
|
10
13
|
import { findOverrideBaseVersionDrift, } from './furnace-version-drift.js';
|
|
11
14
|
function buildOverrideVersionDriftIssues(config, currentVersion, tagName) {
|
|
12
15
|
return findOverrideBaseVersionDrift(config, currentVersion)
|
|
@@ -55,6 +58,25 @@ export async function validateComponent(componentDir, tagName, type, config, roo
|
|
|
55
58
|
if (root) {
|
|
56
59
|
issues.push(...(await validateTokenLink(componentDir, tagName, root, config?.tokenPrefix, config?.tokenHostDocuments)));
|
|
57
60
|
}
|
|
61
|
+
// CSS fragment checks (field report D2): missing fragment files are
|
|
62
|
+
// structural errors; stale deployed expansions are drift the next deploy
|
|
63
|
+
// refreshes.
|
|
64
|
+
if (type === 'custom') {
|
|
65
|
+
const furnacePaths = getFurnacePaths(root ?? join(componentDir, '..', '..', '..'));
|
|
66
|
+
const engineTargetDir = root && config?.custom[tagName]
|
|
67
|
+
? join(getProjectPaths(root).engine, config.custom[tagName].targetPath)
|
|
68
|
+
: undefined;
|
|
69
|
+
issues.push(...(await validateCssFragments(componentDir, tagName, furnacePaths.sharedDir, engineTargetDir)));
|
|
70
|
+
}
|
|
71
|
+
// Engine-side orphan detection (field report D1): files a previous deploy
|
|
72
|
+
// placed in the engine whose workspace source has since been renamed or
|
|
73
|
+
// deleted. Surfaces as drift even when every current workspace file is
|
|
74
|
+
// in sync, which is exactly the gap that let stale jar.mn lines reach a
|
|
75
|
+
// later re-export.
|
|
76
|
+
if (root && config && type === 'custom') {
|
|
77
|
+
const state = await loadFurnaceState(root);
|
|
78
|
+
issues.push(...(await findOrphanedEngineFiles(root, config, tagName, state, resolveFtlDir(config.ftlBasePath))));
|
|
79
|
+
}
|
|
58
80
|
// When root is provided and this is a custom component with registration,
|
|
59
81
|
// also run registration pattern and jar.mn validation for this component.
|
|
60
82
|
// Skipped when an outer orchestrator (validateAllComponents) will run the
|
|
@@ -202,6 +224,36 @@ export async function validateAllComponents(root) {
|
|
|
202
224
|
// other transient fs issue should never cascade into false
|
|
203
225
|
// "orphan" reports.
|
|
204
226
|
}
|
|
227
|
+
// jsconfig chrome-module paths drift (field report D3): when
|
|
228
|
+
// `typecheckJsconfig` is configured, deploy maintains a paths mapping per
|
|
229
|
+
// deployed module file; missing or stale entries mean typed cross-module
|
|
230
|
+
// imports are silently degrading to `any` in the consumer's typecheck.
|
|
231
|
+
if (config.typecheckJsconfig) {
|
|
232
|
+
try {
|
|
233
|
+
const drift = await findJsconfigPathsDrift(root, config);
|
|
234
|
+
if (drift.changed) {
|
|
235
|
+
const detail = [
|
|
236
|
+
...drift.added.map((key) => `missing: ${key}`),
|
|
237
|
+
...drift.updated.map((key) => `stale: ${key}`),
|
|
238
|
+
...drift.pruned.map((key) => `orphaned: ${key}`),
|
|
239
|
+
].join('; ');
|
|
240
|
+
const issue = {
|
|
241
|
+
component: 'furnace',
|
|
242
|
+
severity: 'warning',
|
|
243
|
+
check: 'jsconfig-paths-drift',
|
|
244
|
+
message: `${config.typecheckJsconfig} chrome-module paths are out of sync with the workspace (${detail}). ` +
|
|
245
|
+
'Run "fireforge furnace deploy" to update them.',
|
|
246
|
+
};
|
|
247
|
+
const existing = results.get(issue.component) ?? [];
|
|
248
|
+
existing.push(issue);
|
|
249
|
+
results.set(issue.component, existing);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Drift detection must not break validation when the jsconfig is
|
|
254
|
+
// missing or unparsable — deploy reports those cases with guidance.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
205
257
|
return results;
|
|
206
258
|
}
|
|
207
259
|
/**
|
|
@@ -63,6 +63,21 @@ export declare function git(args: string[], cwd: string, options?: {
|
|
|
63
63
|
timeout?: number;
|
|
64
64
|
env?: Record<string, string>;
|
|
65
65
|
}): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
68
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
69
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
70
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
71
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
72
|
+
*
|
|
73
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
74
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
75
|
+
* how the paths are grouped across invocations never affects the result.
|
|
76
|
+
* @param paths - Pathspecs to chunk
|
|
77
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
78
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
79
|
+
*/
|
|
80
|
+
export declare function chunkPathspecs(paths: string[], budgetBytes?: number): string[][];
|
|
66
81
|
/**
|
|
67
82
|
* Configures git performance settings for large trees.
|
|
68
83
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -72,6 +72,38 @@ export async function git(args, cwd, options) {
|
|
|
72
72
|
}
|
|
73
73
|
return result.stdout;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
77
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
78
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
79
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
80
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
81
|
+
*
|
|
82
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
83
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
84
|
+
* how the paths are grouped across invocations never affects the result.
|
|
85
|
+
* @param paths - Pathspecs to chunk
|
|
86
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
87
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
88
|
+
*/
|
|
89
|
+
export function chunkPathspecs(paths, budgetBytes = 96_000) {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let current = [];
|
|
92
|
+
let used = 0;
|
|
93
|
+
for (const path of paths) {
|
|
94
|
+
const cost = Buffer.byteLength(path) + 1;
|
|
95
|
+
if (current.length > 0 && used + cost > budgetBytes) {
|
|
96
|
+
chunks.push(current);
|
|
97
|
+
current = [];
|
|
98
|
+
used = 0;
|
|
99
|
+
}
|
|
100
|
+
current.push(path);
|
|
101
|
+
used += cost;
|
|
102
|
+
}
|
|
103
|
+
if (current.length > 0)
|
|
104
|
+
chunks.push(current);
|
|
105
|
+
return chunks;
|
|
106
|
+
}
|
|
75
107
|
/**
|
|
76
108
|
* Configures git performance settings for large trees.
|
|
77
109
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -40,6 +40,14 @@ export declare function getAllDiff(repoDir: string): Promise<string>;
|
|
|
40
40
|
* Builds a combined diff against HEAD for the provided files without touching
|
|
41
41
|
* the real git index. Tracked files use `git diff HEAD`; untracked files use
|
|
42
42
|
* synthesized new-file diffs.
|
|
43
|
+
*
|
|
44
|
+
* Performance: the work is batched into a handful of `git` invocations
|
|
45
|
+
* (one `ls-tree` to classify, one `diff` over all tracked files, one
|
|
46
|
+
* `hash-object` over all new text files) rather than the ~2 spawns per file the
|
|
47
|
+
* previous per-file loop issued — that fan-out dominated the cold-run cost on a
|
|
48
|
+
* Firefox-sized checkout (~700 serial spawns, ~99s). Binary, directory, and
|
|
49
|
+
* recursion paths stay per-file because they are rare and (for binary) mutate
|
|
50
|
+
* the index.
|
|
43
51
|
* @param repoDir - Repository directory
|
|
44
52
|
* @param files - File paths to diff (relative to repo root)
|
|
45
53
|
* @returns Combined diff content
|