@hominis/fireforge 0.50.0 → 0.52.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 +67 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +220 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +11 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -1
- package/dist/src/core/typecheck-shim.js +139 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +29 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared holds on a FireForge directory lock.
|
|
4
|
+
*
|
|
5
|
+
* Every lock used to be exclusive, so two build-less `fireforge test` runs
|
|
6
|
+
* on one objdir, each with its own profile and (under auto ports) its own
|
|
7
|
+
* ports, still waited for each other: the gate's browser tiers ran strictly
|
|
8
|
+
* serially on a 14-core host. A shared hold lets such runs overlap while
|
|
9
|
+
* anything that writes (a build, a re-export, a test run that builds)
|
|
10
|
+
* still holds the lock alone.
|
|
11
|
+
*
|
|
12
|
+
* The scheme reuses the exclusive lock as a turnstile, so arrival order and
|
|
13
|
+
* stale-holder reaping come from the machinery that already exists:
|
|
14
|
+
*
|
|
15
|
+
* - A shared holder takes the exclusive lock in the ordinary way, records
|
|
16
|
+
* itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
|
|
17
|
+
* own), and releases the exclusive lock at once.
|
|
18
|
+
* - An exclusive holder takes the exclusive lock, then waits until no live
|
|
19
|
+
* shared holder is left. While it waits it already holds the turnstile,
|
|
20
|
+
* so no new shared holder can slip in ahead of it: a writer queued
|
|
21
|
+
* behind readers is served once they finish, never starved by newcomers.
|
|
22
|
+
*
|
|
23
|
+
* A shared holder whose process died is reaped by the same PID-first
|
|
24
|
+
* liveness check the exclusive lock uses.
|
|
25
|
+
*/
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { mkdir, readdir, rm } from 'node:fs/promises';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
30
|
+
import { verbose } from '../utils/logger.js';
|
|
31
|
+
import { isLockOwnerAlive, readLockOwner, writeLockOwner } from './file-lock-owner.js';
|
|
32
|
+
/** The directory holding a lock's shared-holder records. */
|
|
33
|
+
export function sharedHoldersDir(lockPath) {
|
|
34
|
+
return `${lockPath}.shared`;
|
|
35
|
+
}
|
|
36
|
+
/** Holder directories this process registered, for the signal sweep. */
|
|
37
|
+
const heldShares = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Counts the live shared holders of a lock, reaping any whose process is
|
|
40
|
+
* gone. A missing directory means none.
|
|
41
|
+
*
|
|
42
|
+
* @param lockPath - The lock directory
|
|
43
|
+
* @param options - `reap: false` only counts (status reads)
|
|
44
|
+
*/
|
|
45
|
+
export async function countLiveSharedHolders(lockPath, options = {}) {
|
|
46
|
+
const dir = sharedHoldersDir(lockPath);
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
53
|
+
return 0;
|
|
54
|
+
if (options.reap === false)
|
|
55
|
+
return 0;
|
|
56
|
+
throw toError(error);
|
|
57
|
+
}
|
|
58
|
+
let live = 0;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const holder = join(dir, entry);
|
|
61
|
+
const owner = await readLockOwner(holder);
|
|
62
|
+
if (owner.present && isLockOwnerAlive(owner)) {
|
|
63
|
+
live += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// A record still being written has no owner yet; only an owner that
|
|
67
|
+
// names a dead process is reaped, so a racing registration survives.
|
|
68
|
+
if (owner.present && options.reap !== false) {
|
|
69
|
+
verbose(`Reaping shared hold ${holder}: PID ${owner.pid} is gone.`);
|
|
70
|
+
await rm(holder, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return live;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Records a shared hold. Call while holding the lock's exclusive turnstile.
|
|
77
|
+
*
|
|
78
|
+
* @param lockPath - The lock directory
|
|
79
|
+
* @param metadata - Owner metadata lines (`command=…`)
|
|
80
|
+
*/
|
|
81
|
+
export async function registerSharedHold(lockPath, metadata) {
|
|
82
|
+
const holder = join(sharedHoldersDir(lockPath), randomUUID());
|
|
83
|
+
await mkdir(holder, { recursive: true });
|
|
84
|
+
await writeLockOwner(holder, randomUUID(), [...(metadata ?? []), 'mode=shared']);
|
|
85
|
+
heldShares.add(holder);
|
|
86
|
+
return {
|
|
87
|
+
async release() {
|
|
88
|
+
heldShares.delete(holder);
|
|
89
|
+
try {
|
|
90
|
+
await rm(holder, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
verbose(`Could not remove shared hold ${holder}: ${toError(error).message}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Removes every shared hold this process registered, for the signal path.
|
|
100
|
+
* Never throws.
|
|
101
|
+
*
|
|
102
|
+
* @returns The holder paths removed
|
|
103
|
+
*/
|
|
104
|
+
export async function releaseSharedHoldsForSignal() {
|
|
105
|
+
const removed = [];
|
|
106
|
+
for (const holder of [...heldShares]) {
|
|
107
|
+
heldShares.delete(holder);
|
|
108
|
+
try {
|
|
109
|
+
await rm(holder, { recursive: true, force: true });
|
|
110
|
+
removed.push(holder);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Shutdown path: nothing to do.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return removed;
|
|
117
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LOCK_PID_FILE } from './file-lock-owner.js';
|
|
2
2
|
import { type LockQueueState } from './file-lock-queue.js';
|
|
3
|
+
import { type LockMode } from './file-lock-shared.js';
|
|
3
4
|
export { LOCK_PID_FILE };
|
|
4
5
|
export { formatLockQueuePosition, type LockQueueState } from './file-lock-queue.js';
|
|
6
|
+
export type { LockMode } from './file-lock-shared.js';
|
|
5
7
|
/**
|
|
6
8
|
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
7
9
|
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
@@ -84,6 +86,11 @@ export interface FileLockOptions {
|
|
|
84
86
|
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
85
87
|
/** Extra owner-file lines for human diagnostics. Lock mechanics ignore them. */
|
|
86
88
|
ownerMetadata?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* `exclusive` (default) or `shared`. Shared holders overlap each other and
|
|
91
|
+
* exclude exclusive ones; see `file-lock-shared.ts`.
|
|
92
|
+
*/
|
|
93
|
+
mode?: LockMode | undefined;
|
|
87
94
|
}
|
|
88
95
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
89
96
|
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
@@ -97,6 +104,8 @@ export interface LockStatusSnapshot {
|
|
|
97
104
|
/** Milliseconds since the lock directory was created, when held. */
|
|
98
105
|
heldForMs?: number | undefined;
|
|
99
106
|
queueDepth: number;
|
|
107
|
+
/** Live shared holders (build-less test runs under a shared lock). */
|
|
108
|
+
sharedHolders?: number | undefined;
|
|
100
109
|
}
|
|
101
110
|
/**
|
|
102
111
|
* Inspects a lock without acquiring it. Read-only and fail-open: a missing
|
|
@@ -9,6 +9,7 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
9
9
|
import { sleep } from '../utils/sleep.js';
|
|
10
10
|
import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
|
|
11
11
|
import { readLockQueue, registerWaiter, } from './file-lock-queue.js';
|
|
12
|
+
import { countLiveSharedHolders, registerSharedHold, releaseSharedHoldsForSignal, } from './file-lock-shared.js';
|
|
12
13
|
export { LOCK_PID_FILE };
|
|
13
14
|
export { formatLockQueuePosition } from './file-lock-queue.js';
|
|
14
15
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
@@ -38,6 +39,8 @@ export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
|
38
39
|
*/
|
|
39
40
|
export async function readLockStatus(lockPath) {
|
|
40
41
|
const queue = await readLockQueue(lockPath);
|
|
42
|
+
const shared = await countLiveSharedHolders(lockPath, { reap: false });
|
|
43
|
+
const sharedPart = shared > 0 ? { sharedHolders: shared } : {};
|
|
41
44
|
let heldForMs;
|
|
42
45
|
try {
|
|
43
46
|
// Clamped: filesystem mtime resolution can round a just-created lock
|
|
@@ -46,13 +49,13 @@ export async function readLockStatus(lockPath) {
|
|
|
46
49
|
heldForMs = Math.max(0, Date.now() - (await stat(lockPath)).mtimeMs);
|
|
47
50
|
}
|
|
48
51
|
catch {
|
|
49
|
-
return { held: false, queueDepth: queue.depth };
|
|
52
|
+
return { held: false, queueDepth: queue.depth, ...sharedPart };
|
|
50
53
|
}
|
|
51
54
|
const owner = await readLockOwner(lockPath);
|
|
52
55
|
const holder = owner.present
|
|
53
56
|
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
54
57
|
: undefined;
|
|
55
|
-
return { held: true, holder, heldForMs, queueDepth: queue.depth };
|
|
58
|
+
return { held: true, holder, heldForMs, queueDepth: queue.depth, ...sharedPart };
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
58
61
|
* Reads the contended lock's owner and queue in one go. One probe per cycle
|
|
@@ -334,6 +337,7 @@ export async function forceReleaseHeldLocksForSignal() {
|
|
|
334
337
|
removed.push(lockPath);
|
|
335
338
|
}
|
|
336
339
|
}
|
|
340
|
+
removed.push(...(await releaseSharedHoldsForSignal()));
|
|
337
341
|
return removed;
|
|
338
342
|
}
|
|
339
343
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
@@ -467,8 +471,36 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
467
471
|
// `writeLockOwner` removes the just-created directory before throwing.
|
|
468
472
|
const ownerToken = randomUUID();
|
|
469
473
|
await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
|
|
474
|
+
return runWhileHeld(lockPath, ownerToken, operation, options, {
|
|
475
|
+
deadline: Math.max(deadline, Date.now() + pollMs),
|
|
476
|
+
pollMs,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Runs `operation` once the turnstile is ours: as a shared holder (turnstile
|
|
481
|
+
* released at once), or exclusively once every shared holder has drained.
|
|
482
|
+
*/
|
|
483
|
+
async function runWhileHeld(lockPath, ownerToken, operation, options, drain) {
|
|
484
|
+
if (options.mode === 'shared') {
|
|
485
|
+
// The exclusive lock was only the turnstile: record the shared hold and
|
|
486
|
+
// let the next waiter in. See file-lock-shared.ts.
|
|
487
|
+
let hold;
|
|
488
|
+
try {
|
|
489
|
+
hold = await registerSharedHold(lockPath, options.ownerMetadata);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
await releaseLock(lockPath, ownerToken);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
return await operation();
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await hold.release();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
470
501
|
heldLocks.add(lockPath);
|
|
471
502
|
try {
|
|
503
|
+
await waitForSharedHoldersToDrain(lockPath, drain.deadline, drain.pollMs);
|
|
472
504
|
return await operation();
|
|
473
505
|
}
|
|
474
506
|
finally {
|
|
@@ -476,3 +508,27 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
476
508
|
await releaseLock(lockPath, ownerToken);
|
|
477
509
|
}
|
|
478
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Holds an exclusive acquisition until every live shared holder has gone.
|
|
513
|
+
* The turnstile is already ours, so no new shared holder can join meanwhile.
|
|
514
|
+
*
|
|
515
|
+
* @throws LockContentionError when shared holders outlast the budget
|
|
516
|
+
*/
|
|
517
|
+
async function waitForSharedHoldersToDrain(lockPath, deadline, pollMs) {
|
|
518
|
+
let announced = false;
|
|
519
|
+
for (;;) {
|
|
520
|
+
const live = await countLiveSharedHolders(lockPath);
|
|
521
|
+
if (live === 0)
|
|
522
|
+
return;
|
|
523
|
+
if (Date.now() >= deadline) {
|
|
524
|
+
throw new LockContentionError(`Timed out waiting for file lock ${lockPath}: ${live} shared holder(s) (build-less ` +
|
|
525
|
+
'test runs under a shared lock) still hold it. Wait for them to finish, or retry ' +
|
|
526
|
+
'with a larger --wait-lock.');
|
|
527
|
+
}
|
|
528
|
+
if (!announced) {
|
|
529
|
+
verbose(`Waiting for ${live} shared holder(s) of ${lockPath} to finish.`);
|
|
530
|
+
announced = true;
|
|
531
|
+
}
|
|
532
|
+
await sleep(Math.max(pollMs, 100));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -17,7 +17,8 @@ import { topologicalSortCustom } from './furnace-graph-utils.js';
|
|
|
17
17
|
import { resolveFurnaceMarkerComment } from './furnace-marker.js';
|
|
18
18
|
import { recordFurnaceRollbackFailure } from './furnace-operation.js';
|
|
19
19
|
import { assertFurnaceEngineDirReady } from './furnace-precondition.js';
|
|
20
|
-
import { addJarMnEntries, removeCustomElementRegistration
|
|
20
|
+
import { addJarMnEntries, removeCustomElementRegistration } from './furnace-registration.js';
|
|
21
|
+
import { removeJarMnFileEntries } from './furnace-registration-jar-inplace.js';
|
|
21
22
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from './furnace-rollback.js';
|
|
22
23
|
import { blockingStepErrors, hasBlockingStepErrors } from './furnace-step-errors.js';
|
|
23
24
|
import { runPostApplyConsistencyChecks } from './furnace-validate-registration.js';
|
|
@@ -132,8 +133,8 @@ async function applyOverrideBatch(ctx) {
|
|
|
132
133
|
/**
|
|
133
134
|
* After undeploying deleted files, the in-engine jar.mn and
|
|
134
135
|
* customElements.js still carry entries for the removed files. Re-sync them
|
|
135
|
-
* by removing
|
|
136
|
-
*
|
|
136
|
+
* by removing only the deleted files' jar.mn lines in place and adding any
|
|
137
|
+
* live file that has none. If the .mjs itself was deleted, also drop
|
|
137
138
|
* the customElements.js registration. Snapshots are taken under the same
|
|
138
139
|
* journal so the undo path is symmetric with apply.
|
|
139
140
|
*/
|
|
@@ -144,18 +145,17 @@ async function reconcileCustomRegistrationAfterUndeploy(args) {
|
|
|
144
145
|
const deletedRegistrationFiles = deletedFiles.filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
145
146
|
if (deletedRegistrationFiles.length === 0)
|
|
146
147
|
return;
|
|
147
|
-
// jar.mn re-sync
|
|
148
|
-
//
|
|
149
|
-
//
|
|
148
|
+
// jar.mn re-sync: drop only the deleted files' lines, so the rest of the
|
|
149
|
+
// widget's block keeps its order and the owning patch carries no reorder
|
|
150
|
+
// hunks. addJarMnEntries is idempotent, so the add pass inserts only a
|
|
151
|
+
// live file that has no line yet. The journal snapshot before the
|
|
152
|
+
// mutation gives us a clean rollback target.
|
|
150
153
|
if (rollbackJournal) {
|
|
151
154
|
await snapshotFile(rollbackJournal, join(engineDir, JAR_MN));
|
|
152
155
|
}
|
|
153
|
-
await
|
|
156
|
+
await removeJarMnFileEntries(engineDir, name, deletedRegistrationFiles);
|
|
154
157
|
const liveJarFiles = Object.keys(currentChecksums).filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
155
158
|
if (liveJarFiles.length > 0) {
|
|
156
|
-
// applyCustomComponent has already added live entries, and the remove
|
|
157
|
-
// above dropped them too, so re-add now to leave jar.mn in the correct
|
|
158
|
-
// state.
|
|
159
159
|
await addJarMnEntries(engineDir, name, liveJarFiles);
|
|
160
160
|
}
|
|
161
161
|
filesAffected.push(JAR_MN);
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
|
-
* Parses `validationExceptions[]`: `{ component, check, reason }`
|
|
4
|
-
* no other keys, every field a non-empty string,
|
|
5
|
-
*
|
|
3
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, file?, reason }`
|
|
4
|
+
* objects, no other keys, every field a non-empty string, at most one of
|
|
5
|
+
* `tag` and `file` (a bare file name of the component), and no two entries
|
|
6
|
+
* for the same component, check and scope.
|
|
6
7
|
*/
|
|
7
8
|
export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
|
|
9
|
+
/**
|
|
10
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
11
|
+
* list of partner paths or globs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseTokenSiblingSheets(raw: unknown): Record<string, string[]>;
|
|
14
|
+
/** Parses `minComponents`: a positive integer. */
|
|
15
|
+
export declare function parseMinComponents(raw: unknown): number;
|
|
8
16
|
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
9
17
|
export declare function parseOptInChecks(raw: unknown): FurnaceOptInCheck[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Validators for the
|
|
4
|
-
* `validationExceptions[]
|
|
3
|
+
* Validators for the `furnace validate` and `token lint` policy fields of
|
|
4
|
+
* furnace.json: `validationExceptions[]`, `optInChecks[]`, `minComponents`
|
|
5
|
+
* and `tokenSiblingSheets`.
|
|
5
6
|
*
|
|
6
7
|
* Strict on purpose, like `buildAudit.unpackaged`: every exception is a
|
|
7
8
|
* standing suppression of a finding, so a malformed one must fail at load
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
import { FurnaceError } from '../errors/furnace.js';
|
|
11
12
|
import { FURNACE_OPT_IN_CHECKS, } from '../types/furnace.js';
|
|
12
13
|
import { isArray, isObject, isString } from '../utils/validation.js';
|
|
13
|
-
const EXCEPTION_KEYS = new Set(['component', 'check', 'reason']);
|
|
14
|
+
const EXCEPTION_KEYS = new Set(['component', 'check', 'tag', 'file', 'reason']);
|
|
14
15
|
function requireNonEmptyString(entry, key, field) {
|
|
15
16
|
const value = entry[key];
|
|
16
17
|
if (!isString(value) || value.trim().length === 0) {
|
|
@@ -19,9 +20,10 @@ function requireNonEmptyString(entry, key, field) {
|
|
|
19
20
|
return value;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
* Parses `validationExceptions[]`: `{ component, check, reason }`
|
|
23
|
-
* no other keys, every field a non-empty string,
|
|
24
|
-
*
|
|
23
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, file?, reason }`
|
|
24
|
+
* objects, no other keys, every field a non-empty string, at most one of
|
|
25
|
+
* `tag` and `file` (a bare file name of the component), and no two entries
|
|
26
|
+
* for the same component, check and scope.
|
|
25
27
|
*/
|
|
26
28
|
export function parseValidationExceptions(raw) {
|
|
27
29
|
if (!isArray(raw)) {
|
|
@@ -43,9 +45,31 @@ export function parseValidationExceptions(raw) {
|
|
|
43
45
|
check: requireNonEmptyString(entry, 'check', field),
|
|
44
46
|
reason: requireNonEmptyString(entry, 'reason', field),
|
|
45
47
|
};
|
|
46
|
-
|
|
48
|
+
if (entry['tag'] !== undefined)
|
|
49
|
+
exception.tag = requireNonEmptyString(entry, 'tag', field);
|
|
50
|
+
if (entry['file'] !== undefined) {
|
|
51
|
+
if (exception.tag !== undefined) {
|
|
52
|
+
throw new FurnaceError(`Furnace config: "${field}" may name a "tag" or a "file", not both`);
|
|
53
|
+
}
|
|
54
|
+
const file = requireNonEmptyString(entry, 'file', field);
|
|
55
|
+
if (/[\\/]/.test(file) || file === '.' || file === '..') {
|
|
56
|
+
throw new FurnaceError(`Furnace config: "${field}.file" must be a file name in the component's directory, not a path`);
|
|
57
|
+
}
|
|
58
|
+
exception.file = file;
|
|
59
|
+
}
|
|
60
|
+
const scope = exception.tag !== undefined
|
|
61
|
+
? `tag:${exception.tag}`
|
|
62
|
+
: exception.file !== undefined
|
|
63
|
+
? `file:${exception.file}`
|
|
64
|
+
: '';
|
|
65
|
+
const identity = `${exception.component}\u0000${exception.check}\u0000${scope}`;
|
|
47
66
|
if (seen.has(identity)) {
|
|
48
|
-
|
|
67
|
+
const on = exception.tag !== undefined
|
|
68
|
+
? ` on <${exception.tag}>`
|
|
69
|
+
: exception.file !== undefined
|
|
70
|
+
? ` in ${exception.file}`
|
|
71
|
+
: '';
|
|
72
|
+
throw new FurnaceError(`Furnace config: "${field}" repeats the exception for ${exception.component} [${exception.check}]${on}`);
|
|
49
73
|
}
|
|
50
74
|
seen.add(identity);
|
|
51
75
|
return exception;
|
|
@@ -54,6 +78,33 @@ export function parseValidationExceptions(raw) {
|
|
|
54
78
|
function isOptInCheck(value) {
|
|
55
79
|
return FURNACE_OPT_IN_CHECKS.includes(value);
|
|
56
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
83
|
+
* list of partner paths or globs.
|
|
84
|
+
*/
|
|
85
|
+
export function parseTokenSiblingSheets(raw) {
|
|
86
|
+
if (!isObject(raw)) {
|
|
87
|
+
throw new FurnaceError('Furnace config: "tokenSiblingSheets" must be an object');
|
|
88
|
+
}
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [pattern, partners] of Object.entries(raw)) {
|
|
91
|
+
const valid = isArray(partners) &&
|
|
92
|
+
partners.length > 0 &&
|
|
93
|
+
partners.every((partner) => isString(partner) && partner.trim() !== '');
|
|
94
|
+
if (pattern.includes('..') || !valid || partners.some((p) => String(p).includes('..'))) {
|
|
95
|
+
throw new FurnaceError(`Furnace config: "tokenSiblingSheets.${pattern}" must be a non-empty list of engine-relative paths or globs (no "..")`);
|
|
96
|
+
}
|
|
97
|
+
out[pattern] = partners;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Parses `minComponents`: a positive integer. */
|
|
102
|
+
export function parseMinComponents(raw) {
|
|
103
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
|
|
104
|
+
throw new FurnaceError('Furnace config: "minComponents" must be a positive integer');
|
|
105
|
+
}
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
57
108
|
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
58
109
|
export function parseOptInChecks(raw) {
|
|
59
110
|
if (!isArray(raw)) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { FurnaceError } from '../errors/furnace.js';
|
|
9
9
|
import { isJsonObject, isString } from '../utils/validation.js';
|
|
10
10
|
import { parseStringArray } from './furnace-config-array-utils.js';
|
|
11
|
-
import { parseOptInChecks, parseValidationExceptions, } from './furnace-config-validate-exceptions.js';
|
|
11
|
+
import { parseMinComponents, parseOptInChecks, parseTokenSiblingSheets, parseValidationExceptions, } from './furnace-config-validate-exceptions.js';
|
|
12
12
|
/**
|
|
13
13
|
* Validates an override component config object.
|
|
14
14
|
* @param data - Raw data to validate
|
|
@@ -94,6 +94,9 @@ export function applyOptionalFurnaceFields(migrated, config) {
|
|
|
94
94
|
if (migrated['platformPrefixes'] !== undefined) {
|
|
95
95
|
config.platformPrefixes = parseStringArray(migrated['platformPrefixes'], 'platformPrefixes');
|
|
96
96
|
}
|
|
97
|
+
if (migrated['tokenSiblingSheets'] !== undefined) {
|
|
98
|
+
config.tokenSiblingSheets = parseTokenSiblingSheets(migrated['tokenSiblingSheets']);
|
|
99
|
+
}
|
|
97
100
|
if (migrated['runtimeVariables'] !== undefined) {
|
|
98
101
|
config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
|
|
99
102
|
}
|
|
@@ -137,4 +140,7 @@ export function applyOptionalFurnaceFields(migrated, config) {
|
|
|
137
140
|
if (migrated['optInChecks'] !== undefined) {
|
|
138
141
|
config.optInChecks = parseOptInChecks(migrated['optInChecks']);
|
|
139
142
|
}
|
|
143
|
+
if (migrated['minComponents'] !== undefined) {
|
|
144
|
+
config.minComponents = parseMinComponents(migrated['minComponents']);
|
|
145
|
+
}
|
|
140
146
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the lines registering `files` for `tagName`, leaving every other
|
|
3
|
+
* line of the block where it is. Returns how many lines were removed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function removeJarMnFileEntries(engineDir: string, tagName: string, files: readonly string[]): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
|
|
8
|
+
* file is in `liveFiles` keeps its position and layout; one whose file is
|
|
9
|
+
* gone is dropped. Returns the renamed files that already had a line; the
|
|
10
|
+
* caller's `addJarMnEntries` pass then inserts only the rest.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renameJarMnEntriesInPlace(engineDir: string, oldTag: string, newTag: string, liveFiles: readonly string[], renameFile: (file: string) => string): Promise<string[]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* In-place edits to a widget's `toolkit/content/jar.mn` block.
|
|
4
|
+
*
|
|
5
|
+
* Undeploying one file and renaming a component used to drop every line of
|
|
6
|
+
* the widget and re-add the live ones, which re-sorted the whole block and
|
|
7
|
+
* left the owning patch carrying reorder hunks. These helpers touch only
|
|
8
|
+
* the lines that change: a deleted file's line goes, a renamed component's
|
|
9
|
+
* lines are rewritten where they stand, and anything still missing is left
|
|
10
|
+
* for `addJarMnEntries` to insert.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
14
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
15
|
+
import { JAR_MN } from './furnace-constants.js';
|
|
16
|
+
/**
|
|
17
|
+
* The file a widget jar.mn line registers: the `(widgets/<tag>/<file>)`
|
|
18
|
+
* source mapping, or the legacy `content/global/elements/<tag>.<ext>`
|
|
19
|
+
* target written before the mapping existed. Undefined when the line is
|
|
20
|
+
* not one of this tag's.
|
|
21
|
+
*/
|
|
22
|
+
function registeredFile(line, tagName) {
|
|
23
|
+
const tag = escapeRegex(tagName);
|
|
24
|
+
const source = new RegExp(`\\(widgets/${tag}/([^)]+)\\)`).exec(line);
|
|
25
|
+
if (source)
|
|
26
|
+
return source[1];
|
|
27
|
+
const legacy = new RegExp(`content/global/elements/(${tag}\\.[^\\s]+)`).exec(line);
|
|
28
|
+
return legacy?.[1];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Removes the lines registering `files` for `tagName`, leaving every other
|
|
32
|
+
* line of the block where it is. Returns how many lines were removed.
|
|
33
|
+
*/
|
|
34
|
+
export async function removeJarMnFileEntries(engineDir, tagName, files) {
|
|
35
|
+
const filePath = join(engineDir, JAR_MN);
|
|
36
|
+
if (files.length === 0 || !(await pathExists(filePath)))
|
|
37
|
+
return 0;
|
|
38
|
+
const doomed = new Set(files);
|
|
39
|
+
const lines = (await readText(filePath)).split('\n');
|
|
40
|
+
const kept = lines.filter((line) => {
|
|
41
|
+
const file = registeredFile(line, tagName);
|
|
42
|
+
return file === undefined || !doomed.has(file);
|
|
43
|
+
});
|
|
44
|
+
const removed = lines.length - kept.length;
|
|
45
|
+
if (removed > 0)
|
|
46
|
+
await writeText(filePath, kept.join('\n'));
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
|
|
51
|
+
* file is in `liveFiles` keeps its position and layout; one whose file is
|
|
52
|
+
* gone is dropped. Returns the renamed files that already had a line; the
|
|
53
|
+
* caller's `addJarMnEntries` pass then inserts only the rest.
|
|
54
|
+
*/
|
|
55
|
+
export async function renameJarMnEntriesInPlace(engineDir, oldTag, newTag, liveFiles, renameFile) {
|
|
56
|
+
const filePath = join(engineDir, JAR_MN);
|
|
57
|
+
if (!(await pathExists(filePath)))
|
|
58
|
+
return [];
|
|
59
|
+
const live = new Set(liveFiles);
|
|
60
|
+
const covered = [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const line of (await readText(filePath)).split('\n')) {
|
|
63
|
+
const file = registeredFile(line, oldTag);
|
|
64
|
+
if (file === undefined) {
|
|
65
|
+
out.push(line);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const renamed = renameFile(file);
|
|
69
|
+
if (!live.has(renamed) || covered.includes(renamed))
|
|
70
|
+
continue;
|
|
71
|
+
// Textual substitution keeps the indent, column spacing and any
|
|
72
|
+
// trailing comment the line carried.
|
|
73
|
+
out.push(line
|
|
74
|
+
.replace(`content/global/elements/${file}`, `content/global/elements/${renamed}`)
|
|
75
|
+
.replace(`(widgets/${oldTag}/${file})`, `(widgets/${newTag}/${renamed})`));
|
|
76
|
+
covered.push(renamed);
|
|
77
|
+
}
|
|
78
|
+
await writeText(filePath, out.join('\n'));
|
|
79
|
+
return covered;
|
|
80
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { CustomComponentConfig, FurnaceOptInCheck, ValidationIssue } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates accessibility patterns in a component's
|
|
4
|
-
*
|
|
3
|
+
* Validates accessibility patterns in a component's composed source: its
|
|
4
|
+
* host module and every companion module (see
|
|
5
|
+
* {@link readComposedComponentSource}). Checks for ARIA roles, keyboard
|
|
6
|
+
* handlers, l10n, and focus delegation.
|
|
5
7
|
*
|
|
6
8
|
* @param componentDir - Directory holding the component's sources
|
|
7
9
|
* @param tagName - Custom element tag name, used to locate `<tagName>.mjs`
|
|
@@ -32,6 +34,6 @@ export declare function validateAccessibility(componentDir: string, tagName: str
|
|
|
32
34
|
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
33
35
|
*
|
|
34
36
|
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
35
|
-
* findings (matches the pre-existing escape hatch).
|
|
37
|
+
* findings in that file (matches the pre-existing escape hatch).
|
|
36
38
|
*/
|
|
37
39
|
export declare function containsHardcodedTemplateText(content: string): boolean;
|