@hominis/fireforge 0.30.0 → 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 +26 -1
- package/README.md +22 -5
- 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 +119 -78
- 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 +3 -13
- package/dist/src/core/lint-cache.js +11 -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
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
const sourcePath = normalizePathSlashes(relative(jsconfigDir, join(componentDir, file)));
|
|
55
|
+
entries[`${CHROME_ELEMENTS_URL_PREFIX}${file}`] = [sourcePath];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
/** True when `key`/`value` is a Furnace-managed chrome-elements mapping. */
|
|
61
|
+
function isManagedEntry(key, value, jsconfigDir, customDir) {
|
|
62
|
+
if (!key.startsWith(CHROME_ELEMENTS_URL_PREFIX))
|
|
63
|
+
return false;
|
|
64
|
+
if (!Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string')
|
|
65
|
+
return false;
|
|
66
|
+
const target = resolve(jsconfigDir, value[0]);
|
|
67
|
+
return target.startsWith(resolve(customDir) + '/') || target === resolve(customDir);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reconciles the managed `compilerOptions.paths` entries of the configured
|
|
71
|
+
* jsconfig against the current Furnace workspace. Idempotent; writes only
|
|
72
|
+
* when something actually changes; dry-run returns the diff without
|
|
73
|
+
* writing.
|
|
74
|
+
*
|
|
75
|
+
* The consumer owns the jsconfig file: a missing file is an error with
|
|
76
|
+
* guidance rather than a silent scaffold, and JSONC (comments/trailing
|
|
77
|
+
* commas) is unsupported for the managed file — `readJson` is a strict
|
|
78
|
+
* JSON parser, so the error message says so explicitly.
|
|
79
|
+
*
|
|
80
|
+
* @param root - Project root directory
|
|
81
|
+
* @param config - Loaded Furnace configuration (must carry `typecheckJsconfig`)
|
|
82
|
+
* @param options - `dryRun` skips the write but still reports the diff
|
|
83
|
+
*/
|
|
84
|
+
export async function syncFurnaceJsconfigPaths(root, config, options) {
|
|
85
|
+
const result = { added: [], updated: [], pruned: [], changed: false };
|
|
86
|
+
const jsconfigRel = config.typecheckJsconfig;
|
|
87
|
+
if (!jsconfigRel)
|
|
88
|
+
return result;
|
|
89
|
+
const jsconfigAbs = resolve(root, jsconfigRel);
|
|
90
|
+
if (!(await pathExists(jsconfigAbs))) {
|
|
91
|
+
throw new FurnaceError(`furnace.json sets "typecheckJsconfig": "${jsconfigRel}", but the file does not exist. ` +
|
|
92
|
+
'Create the jsconfig (it stays consumer-owned; Furnace only maintains the ' +
|
|
93
|
+
`"compilerOptions.paths" entries under ${CHROME_ELEMENTS_URL_PREFIX}*), ` +
|
|
94
|
+
'or remove the setting.');
|
|
95
|
+
}
|
|
96
|
+
let jsconfig;
|
|
97
|
+
try {
|
|
98
|
+
jsconfig = await readJson(jsconfigAbs);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new FurnaceError(`Could not parse ${jsconfigRel} as JSON: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
102
|
+
'Furnace manages paths entries only in plain-JSON jsconfig files — JSONC comments ' +
|
|
103
|
+
'and trailing commas are not supported for the managed file.');
|
|
104
|
+
}
|
|
105
|
+
const furnacePaths = getFurnacePaths(root);
|
|
106
|
+
const desired = await computeDesiredChromePathEntries(config, furnacePaths.customDir, jsconfigAbs);
|
|
107
|
+
const jsconfigDir = dirname(jsconfigAbs);
|
|
108
|
+
const currentPaths = { ...(jsconfig.compilerOptions?.paths ?? {}) };
|
|
109
|
+
const nextPaths = {};
|
|
110
|
+
// Preserve every unmanaged entry verbatim; prune managed entries that are
|
|
111
|
+
// no longer desired; update managed entries whose target moved.
|
|
112
|
+
for (const [key, value] of Object.entries(currentPaths)) {
|
|
113
|
+
if (!isManagedEntry(key, value, jsconfigDir, furnacePaths.customDir)) {
|
|
114
|
+
nextPaths[key] = value;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const want = desired[key];
|
|
118
|
+
if (want === undefined) {
|
|
119
|
+
result.pruned.push(key);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (value[0] !== want[0]) {
|
|
123
|
+
result.updated.push(key);
|
|
124
|
+
nextPaths[key] = want;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
nextPaths[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [key, want] of Object.entries(desired)) {
|
|
131
|
+
if (!(key in nextPaths)) {
|
|
132
|
+
result.added.push(key);
|
|
133
|
+
nextPaths[key] = want;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
result.changed = result.added.length > 0 || result.updated.length > 0 || result.pruned.length > 0;
|
|
137
|
+
if (!result.changed || options?.dryRun === true)
|
|
138
|
+
return result;
|
|
139
|
+
const nextJsconfig = {
|
|
140
|
+
...jsconfig,
|
|
141
|
+
compilerOptions: {
|
|
142
|
+
...(jsconfig.compilerOptions ?? {}),
|
|
143
|
+
paths: nextPaths,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
await writeJson(jsconfigAbs, nextJsconfig);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Computes jsconfig `paths` drift for `furnace validate`: managed entries
|
|
151
|
+
* that are missing or stale relative to the current workspace. Read-only —
|
|
152
|
+
* delegates to {@link syncFurnaceJsconfigPaths} in dry-run mode.
|
|
153
|
+
*/
|
|
154
|
+
export async function findJsconfigPathsDrift(root, config) {
|
|
155
|
+
return syncFurnaceJsconfigPaths(root, config, { dryRun: true });
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Runs the jsconfig paths sync after a successful deploy/sync and reports
|
|
159
|
+
* the diff. No-op when `typecheckJsconfig` is unset. Shared by
|
|
160
|
+
* `furnace deploy` and `furnace sync` so both report identically.
|
|
161
|
+
*/
|
|
162
|
+
export async function reportJsconfigPathsSync(root, config, dryRun) {
|
|
163
|
+
if (!config.typecheckJsconfig)
|
|
164
|
+
return;
|
|
165
|
+
const sync = await syncFurnaceJsconfigPaths(root, config, { dryRun });
|
|
166
|
+
if (!sync.changed)
|
|
167
|
+
return;
|
|
168
|
+
info(`${dryRun ? '[dry-run] Would update' : 'Updated'} ${config.typecheckJsconfig} chrome-module paths: ` +
|
|
169
|
+
`+${sync.added.length} added, ~${sync.updated.length} updated, -${sync.pruned.length} pruned`);
|
|
170
|
+
}
|
|
171
|
+
//# 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
|
/**
|
|
@@ -5,18 +5,6 @@ import type { GitStatusEntry } from './git-base.js';
|
|
|
5
5
|
* @param filePath - Path to the file (relative to repo)
|
|
6
6
|
*/
|
|
7
7
|
export declare function restoreTrackedPath(repoDir: string, filePath: string): Promise<void>;
|
|
8
|
-
/**
|
|
9
|
-
* Removes an untracked path from disk.
|
|
10
|
-
* @param repoDir - Repository directory
|
|
11
|
-
* @param filePath - Path to the file (relative to repo)
|
|
12
|
-
*/
|
|
13
|
-
export declare function removeUntrackedPath(repoDir: string, filePath: string): Promise<void>;
|
|
14
|
-
/**
|
|
15
|
-
* Removes a path that is present only in the index/worktree and not in HEAD.
|
|
16
|
-
* @param repoDir - Repository directory
|
|
17
|
-
* @param filePath - Path to remove
|
|
18
|
-
*/
|
|
19
|
-
export declare function removeAddedPath(repoDir: string, filePath: string): Promise<void>;
|
|
20
8
|
/**
|
|
21
9
|
* Discards a status entry according to its git state.
|
|
22
10
|
* @param repoDir - Repository directory
|
|
@@ -19,7 +19,7 @@ export async function restoreTrackedPath(repoDir, filePath) {
|
|
|
19
19
|
* @param repoDir - Repository directory
|
|
20
20
|
* @param filePath - Path to the file (relative to repo)
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
async function removeUntrackedPath(repoDir, filePath) {
|
|
23
23
|
const fullPath = join(repoDir, filePath);
|
|
24
24
|
await removeFile(fullPath);
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export async function removeUntrackedPath(repoDir, filePath) {
|
|
|
28
28
|
* @param repoDir - Repository directory
|
|
29
29
|
* @param filePath - Path to remove
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
async function removeAddedPath(repoDir, filePath) {
|
|
32
32
|
await ensureGit();
|
|
33
33
|
await git(['reset', 'HEAD', '--', filePath], repoDir);
|
|
34
34
|
await removeUntrackedPath(repoDir, filePath);
|
|
@@ -2,7 +2,6 @@ import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
|
2
2
|
import type { FireForgeConfig } from '../types/config.js';
|
|
3
3
|
import { type PatchQueueContext } from './patch-lint.js';
|
|
4
4
|
export declare const LINT_CACHE_SCHEMA_VERSION = 1;
|
|
5
|
-
export declare const LINT_IMPLEMENTATION_VERSION = 1;
|
|
6
5
|
export interface PerPatchLintCacheEntry {
|
|
7
6
|
key: string;
|
|
8
7
|
patchFilename: string;
|
|
@@ -21,24 +20,16 @@ export interface PerPatchLintCacheKeyInput {
|
|
|
21
20
|
existingFiles: string[];
|
|
22
21
|
config: FireForgeConfig;
|
|
23
22
|
queueContext: PatchQueueContext;
|
|
23
|
+
engineHeadSha?: string;
|
|
24
24
|
packageVersion?: string;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
/** Computes a SHA-256 hex digest for text or binary content. */
|
|
30
|
-
export declare function sha256Hex(content: string | Buffer): string;
|
|
31
|
-
/** Computes a stable SHA-256 digest for JSON-compatible data. */
|
|
32
|
-
export declare function stableHash(value: JsonValue): string;
|
|
33
|
-
/** Returns the repo-local per-patch lint cache file path. */
|
|
34
|
-
export declare function getPerPatchLintCachePath(projectRoot: string): string;
|
|
26
|
+
/** Returns the engine git HEAD identity used to guard diff-derived cache hits. */
|
|
27
|
+
export declare function getPerPatchLintCacheHeadSha(engineDir: string): Promise<string>;
|
|
35
28
|
/**
|
|
36
29
|
* Builds the complete per-patch lint cache key for one lintable patch.
|
|
37
30
|
* The key includes source, metadata, config, engine state, and ownership inputs.
|
|
38
31
|
*/
|
|
39
32
|
export declare function buildPerPatchLintCacheKey(input: PerPatchLintCacheKeyInput): Promise<string>;
|
|
40
|
-
/** Creates an empty cache document using the current cache schema. */
|
|
41
|
-
export declare function createEmptyPerPatchLintCache(): PerPatchLintCacheFile;
|
|
42
33
|
/** Loads the per-patch lint cache, treating missing or invalid files as empty. */
|
|
43
34
|
export declare function loadPerPatchLintCache(projectRoot: string): Promise<PerPatchLintCacheFile>;
|
|
44
35
|
/** Persists the per-patch lint cache atomically through the shared JSON writer. */
|
|
@@ -49,4 +40,3 @@ export declare function clearPerPatchLintCache(projectRoot: string): Promise<voi
|
|
|
49
40
|
export declare function getCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string): PatchLintIssue[] | undefined;
|
|
50
41
|
/** Stores per-patch lint issues after a successful lint calculation. */
|
|
51
42
|
export declare function setCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, issues: PatchLintIssue[]): void;
|
|
52
|
-
export {};
|
|
@@ -5,9 +5,10 @@ import { join, resolve } from 'node:path';
|
|
|
5
5
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
6
6
|
import { getPackageVersion } from '../utils/package-root.js';
|
|
7
7
|
import { getFurnacePaths } from './furnace-config.js';
|
|
8
|
+
import { git } from './git-base.js';
|
|
8
9
|
import { collectNewFileCreatorsByPath } from './patch-lint.js';
|
|
9
10
|
export const LINT_CACHE_SCHEMA_VERSION = 1;
|
|
10
|
-
|
|
11
|
+
const LINT_IMPLEMENTATION_VERSION = 1;
|
|
11
12
|
const LINT_CACHE_DIRNAME = 'lint-cache';
|
|
12
13
|
const PER_PATCH_CACHE_FILENAME = 'per-patch-v1.json';
|
|
13
14
|
function stableJson(value) {
|
|
@@ -25,17 +26,21 @@ function stableJson(value) {
|
|
|
25
26
|
.join(',')}}`;
|
|
26
27
|
}
|
|
27
28
|
/** Computes a SHA-256 hex digest for text or binary content. */
|
|
28
|
-
|
|
29
|
+
function sha256Hex(content) {
|
|
29
30
|
return createHash('sha256').update(content).digest('hex');
|
|
30
31
|
}
|
|
31
32
|
/** Computes a stable SHA-256 digest for JSON-compatible data. */
|
|
32
|
-
|
|
33
|
+
function stableHash(value) {
|
|
33
34
|
return sha256Hex(stableJson(value));
|
|
34
35
|
}
|
|
35
36
|
/** Returns the repo-local per-patch lint cache file path. */
|
|
36
|
-
|
|
37
|
+
function getPerPatchLintCachePath(projectRoot) {
|
|
37
38
|
return join(projectRoot, '.fireforge', LINT_CACHE_DIRNAME, PER_PATCH_CACHE_FILENAME);
|
|
38
39
|
}
|
|
40
|
+
/** Returns the engine git HEAD identity used to guard diff-derived cache hits. */
|
|
41
|
+
export async function getPerPatchLintCacheHeadSha(engineDir) {
|
|
42
|
+
return (await git(['rev-parse', 'HEAD'], engineDir)).trim();
|
|
43
|
+
}
|
|
39
44
|
async function fileHash(path) {
|
|
40
45
|
if (!(await pathExists(path))) {
|
|
41
46
|
return { exists: false };
|
|
@@ -90,6 +95,7 @@ export async function buildPerPatchLintCacheKey(input) {
|
|
|
90
95
|
};
|
|
91
96
|
return stableHash({
|
|
92
97
|
cacheSchemaVersion: LINT_CACHE_SCHEMA_VERSION,
|
|
98
|
+
engineHeadSha: input.engineHeadSha ?? null,
|
|
93
99
|
lintImplementationVersion: LINT_IMPLEMENTATION_VERSION,
|
|
94
100
|
packageVersion: input.packageVersion ?? getPackageVersion(),
|
|
95
101
|
patchFile: await fileHash(join(input.patchesDir, input.patch.filename)),
|
|
@@ -102,7 +108,7 @@ export async function buildPerPatchLintCacheKey(input) {
|
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
110
|
/** Creates an empty cache document using the current cache schema. */
|
|
105
|
-
|
|
111
|
+
function createEmptyPerPatchLintCache() {
|
|
106
112
|
return { schemaVersion: LINT_CACHE_SCHEMA_VERSION, entries: {} };
|
|
107
113
|
}
|
|
108
114
|
function isCacheEntry(value) {
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -193,5 +193,9 @@ export declare function watchWithOutput(engineDir: string, options?: {
|
|
|
193
193
|
export declare function test(engineDir: string, testPaths?: string[], args?: string[]): Promise<number>;
|
|
194
194
|
/**
|
|
195
195
|
* Runs mach test while capturing streamed output for better diagnostics.
|
|
196
|
+
*
|
|
197
|
+
* @param env - Optional extra environment variables for the mach process
|
|
198
|
+
* (merged over `process.env` by the exec layer). Used by
|
|
199
|
+
* `fireforge test --perf-samples` to publish the artifact-path contract.
|
|
196
200
|
*/
|
|
197
|
-
export declare function testWithOutput(engineDir: string, testPaths?: string[], args?: string[]): Promise<MachCommandResult>;
|
|
201
|
+
export declare function testWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string>): Promise<MachCommandResult>;
|
package/dist/src/core/mach.js
CHANGED
|
@@ -312,8 +312,12 @@ export async function test(engineDir, testPaths = [], args = []) {
|
|
|
312
312
|
}
|
|
313
313
|
/**
|
|
314
314
|
* Runs mach test while capturing streamed output for better diagnostics.
|
|
315
|
+
*
|
|
316
|
+
* @param env - Optional extra environment variables for the mach process
|
|
317
|
+
* (merged over `process.env` by the exec layer). Used by
|
|
318
|
+
* `fireforge test --perf-samples` to publish the artifact-path contract.
|
|
315
319
|
*/
|
|
316
|
-
export async function testWithOutput(engineDir, testPaths = [], args = []) {
|
|
317
|
-
return runMachCapture(['test', ...testPaths, ...args], engineDir);
|
|
320
|
+
export async function testWithOutput(engineDir, testPaths = [], args = [], env) {
|
|
321
|
+
return runMachCapture(['test', ...testPaths, ...args], engineDir, env ? { env } : {});
|
|
318
322
|
}
|
|
319
323
|
//# sourceMappingURL=mach.js.map
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manifest registration barrel — re-exports all registration targets
|
|
3
|
-
* and
|
|
3
|
+
* and the shared RegisterResult interface (which lives in
|
|
4
|
+
* `register-result.ts` so the leaf modules can import it without
|
|
5
|
+
* creating a cycle through this barrel).
|
|
4
6
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
export interface RegisterResult {
|
|
9
|
-
/** The manifest file that was modified */
|
|
10
|
-
manifest: string;
|
|
11
|
-
/** The entry that was inserted */
|
|
12
|
-
entry: string;
|
|
13
|
-
/** The entry after which the new entry was inserted (for user display) */
|
|
14
|
-
previousEntry?: string | undefined;
|
|
15
|
-
/** Whether the entry already existed (skipped) */
|
|
16
|
-
skipped: boolean;
|
|
17
|
-
/** Whether --after target was not found and fell back to alphabetical */
|
|
18
|
-
afterFallback?: boolean | undefined;
|
|
19
|
-
}
|
|
7
|
+
import type { RegisterResult } from './register-result.js';
|
|
8
|
+
export type { RegisterResult } from './register-result.js';
|
|
20
9
|
export { registerBrowserContent } from './register-browser-content.js';
|
|
21
10
|
export { registerFireForgeModule } from './register-module.js';
|
|
22
11
|
export { registerSharedCSS } from './register-shared-css.js';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Manifest registration barrel — re-exports all registration targets
|
|
4
|
-
* and
|
|
4
|
+
* and the shared RegisterResult interface (which lives in
|
|
5
|
+
* `register-result.ts` so the leaf modules can import it without
|
|
6
|
+
* creating a cycle through this barrel).
|
|
5
7
|
*/
|
|
6
8
|
import { join } from 'node:path';
|
|
7
9
|
import { GeneralError } from '../errors/base.js';
|