@hominis/fireforge 0.50.0 → 0.51.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 +47 -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/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- 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 +211 -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 +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -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 +4 -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 +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- 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 +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -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 +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -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 +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -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 +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- 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 +5 -1
- package/dist/src/core/typecheck-shim.js +115 -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 +20 -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 +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -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', '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,9 @@ 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, and no two
|
|
24
|
-
* same component and
|
|
23
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
24
|
+
* objects, no other keys, every field a non-empty string, and no two
|
|
25
|
+
* entries for the same component, check and tag.
|
|
25
26
|
*/
|
|
26
27
|
export function parseValidationExceptions(raw) {
|
|
27
28
|
if (!isArray(raw)) {
|
|
@@ -43,9 +44,12 @@ export function parseValidationExceptions(raw) {
|
|
|
43
44
|
check: requireNonEmptyString(entry, 'check', field),
|
|
44
45
|
reason: requireNonEmptyString(entry, 'reason', field),
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
+
if (entry['tag'] !== undefined)
|
|
48
|
+
exception.tag = requireNonEmptyString(entry, 'tag', field);
|
|
49
|
+
const identity = `${exception.component}\u0000${exception.check}\u0000${exception.tag ?? ''}`;
|
|
47
50
|
if (seen.has(identity)) {
|
|
48
|
-
|
|
51
|
+
const on = exception.tag !== undefined ? ` on <${exception.tag}>` : '';
|
|
52
|
+
throw new FurnaceError(`Furnace config: "${field}" repeats the exception for ${exception.component} [${exception.check}]${on}`);
|
|
49
53
|
}
|
|
50
54
|
seen.add(identity);
|
|
51
55
|
return exception;
|
|
@@ -54,6 +58,33 @@ export function parseValidationExceptions(raw) {
|
|
|
54
58
|
function isOptInCheck(value) {
|
|
55
59
|
return FURNACE_OPT_IN_CHECKS.includes(value);
|
|
56
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
63
|
+
* list of partner paths or globs.
|
|
64
|
+
*/
|
|
65
|
+
export function parseTokenSiblingSheets(raw) {
|
|
66
|
+
if (!isObject(raw)) {
|
|
67
|
+
throw new FurnaceError('Furnace config: "tokenSiblingSheets" must be an object');
|
|
68
|
+
}
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [pattern, partners] of Object.entries(raw)) {
|
|
71
|
+
const valid = isArray(partners) &&
|
|
72
|
+
partners.length > 0 &&
|
|
73
|
+
partners.every((partner) => isString(partner) && partner.trim() !== '');
|
|
74
|
+
if (pattern.includes('..') || !valid || partners.some((p) => String(p).includes('..'))) {
|
|
75
|
+
throw new FurnaceError(`Furnace config: "tokenSiblingSheets.${pattern}" must be a non-empty list of engine-relative paths or globs (no "..")`);
|
|
76
|
+
}
|
|
77
|
+
out[pattern] = partners;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** Parses `minComponents`: a positive integer. */
|
|
82
|
+
export function parseMinComponents(raw) {
|
|
83
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
|
|
84
|
+
throw new FurnaceError('Furnace config: "minComponents" must be a positive integer');
|
|
85
|
+
}
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
57
88
|
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
58
89
|
export function parseOptInChecks(raw) {
|
|
59
90
|
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`
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
5
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
5
6
|
import { countUnlabelledIconButtons } from './furnace-validate-icon-button.js';
|
|
6
7
|
import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Reads the composed component: `<tagName>.mjs` followed by its companion
|
|
10
|
+
* modules (`<tagName>-*.mjs`, tests and stories left out), so a template's
|
|
11
|
+
* findings do not depend on which of the component's modules it lives in.
|
|
12
|
+
* Returns undefined when the host module is missing.
|
|
13
|
+
*/
|
|
14
|
+
async function readComposedComponentSource(componentDir, tagName) {
|
|
15
|
+
const hostPath = join(componentDir, `${tagName}.mjs`);
|
|
16
|
+
if (!(await pathExists(hostPath)))
|
|
17
|
+
return undefined;
|
|
18
|
+
const entries = await readdir(componentDir).catch(() => []);
|
|
19
|
+
const companions = entries
|
|
20
|
+
.filter((name) => name.startsWith(`${tagName}-`) &&
|
|
21
|
+
name.endsWith('.mjs') &&
|
|
22
|
+
!/\.(test|spec|stories)\./.test(name))
|
|
23
|
+
.sort();
|
|
24
|
+
const sources = [await readText(hostPath)];
|
|
25
|
+
for (const name of companions)
|
|
26
|
+
sources.push(await readText(join(componentDir, name)));
|
|
27
|
+
return sources.join('\n');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates accessibility patterns in a component's composed source: its
|
|
31
|
+
* host module and every companion module (see
|
|
32
|
+
* {@link readComposedComponentSource}). Checks for ARIA roles, keyboard
|
|
33
|
+
* handlers, l10n, and focus delegation.
|
|
10
34
|
*
|
|
11
35
|
* @param componentDir - Directory holding the component's sources
|
|
12
36
|
* @param tagName - Custom element tag name, used to locate `<tagName>.mjs`
|
|
@@ -20,10 +44,9 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
|
|
|
20
44
|
* adds the icon-only `<moz-button>` naming check.
|
|
21
45
|
*/
|
|
22
46
|
export async function validateAccessibility(componentDir, tagName, customConfig, optInChecks = []) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
47
|
+
const content = await readComposedComponentSource(componentDir, tagName);
|
|
48
|
+
if (content === undefined)
|
|
25
49
|
return [];
|
|
26
|
-
const content = await readText(mjsPath);
|
|
27
50
|
const issues = [];
|
|
28
51
|
if (!hasAriaRole(content) && hasGenericInteractiveElement(content)) {
|
|
29
52
|
issues.push(createIssue(tagName, 'warning', 'no-aria-role', 'Generic interactive markup has no native semantics. Prefer native elements, or add role= when native markup cannot provide the semantics.'));
|
|
@@ -41,10 +64,13 @@ export async function validateAccessibility(componentDir, tagName, customConfig,
|
|
|
41
64
|
// activation flows through the inner element and a duplicate handler on
|
|
42
65
|
// the wrapper would either no-op or fire twice alongside the child's
|
|
43
66
|
// built-in keyboard path.
|
|
44
|
-
const
|
|
67
|
+
const syntheticTags = syntheticClickTags(content);
|
|
45
68
|
const keyboardCovered = isKeyboardCoveredByComposition(customConfig);
|
|
46
|
-
if (
|
|
47
|
-
issues.push(
|
|
69
|
+
if (syntheticTags.length > 0 && !hasKeyboardHandler && !keyboardCovered) {
|
|
70
|
+
issues.push({
|
|
71
|
+
...createIssue(tagName, 'warning', 'no-keyboard-handler', `Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup), on ${syntheticTags.map((tag) => `<${tag}>`).join(', ')}.`),
|
|
72
|
+
tags: syntheticTags,
|
|
73
|
+
});
|
|
48
74
|
}
|
|
49
75
|
if (containsHardcodedTemplateText(content)) {
|
|
50
76
|
issues.push(createIssue(tagName, 'warning', 'hardcoded-text', 'Possible hardcoded string found. Use data-l10n-id for localization.'));
|
|
@@ -164,6 +190,9 @@ const NATIVE_CLICK_INTERACTIVE_TAGS = new Set([
|
|
|
164
190
|
'moz-radio',
|
|
165
191
|
'moz-radio-group',
|
|
166
192
|
'moz-menulist',
|
|
193
|
+
// Toolkit's panel-list row renders a button that takes Enter and Space
|
|
194
|
+
// and dispatches click.
|
|
195
|
+
'panel-item',
|
|
167
196
|
]);
|
|
168
197
|
/**
|
|
169
198
|
* Returns true when `customConfig` declares that the component's keyboard
|
|
@@ -185,28 +214,33 @@ function isKeyboardCoveredByComposition(customConfig) {
|
|
|
185
214
|
return composes.some((tag) => NATIVE_CLICK_INTERACTIVE_TAGS.has(tag));
|
|
186
215
|
}
|
|
187
216
|
/**
|
|
188
|
-
* Returns
|
|
189
|
-
*
|
|
190
|
-
* keyboard activation and therefore needs an explicit key
|
|
191
|
-
* Enter/Space.
|
|
192
|
-
* interactive element. Those already fire `click` on keyboard
|
|
217
|
+
* Returns the tags (in first-seen order) of every *synthetic* interactive
|
|
218
|
+
* element carrying an `@click=${...}` handler (e.g. `<div @click>`), which
|
|
219
|
+
* lacks native keyboard activation and therefore needs an explicit key
|
|
220
|
+
* handler for Enter/Space. Empty when every `@click` handler sits on a
|
|
221
|
+
* native interactive element. Those already fire `click` on keyboard
|
|
222
|
+
* activation.
|
|
193
223
|
*/
|
|
194
|
-
function
|
|
224
|
+
function syntheticClickTags(content) {
|
|
195
225
|
const pattern = /@click\s*=\s*\$\{/g;
|
|
226
|
+
const tags = [];
|
|
196
227
|
let match;
|
|
197
228
|
while ((match = pattern.exec(content)) !== null) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
const tag = syntheticClickTag(content, match.index);
|
|
230
|
+
if (tag !== undefined && !tags.includes(tag))
|
|
231
|
+
tags.push(tag);
|
|
201
232
|
}
|
|
202
|
-
return
|
|
233
|
+
return tags;
|
|
203
234
|
}
|
|
235
|
+
/** The tag a synthetic-element `@click` sits on, `?` when it cannot be told. */
|
|
236
|
+
const UNKNOWN_TAG = '?';
|
|
204
237
|
/**
|
|
205
238
|
* Given the offset of an `@click=${...}` occurrence, walks backwards to find
|
|
206
|
-
* the opening `<tag` that owns it and
|
|
207
|
-
* interactive element (
|
|
239
|
+
* the opening `<tag` that owns it and returns that tag when it is a
|
|
240
|
+
* synthetic interactive element (warning needed), or undefined when it is a
|
|
241
|
+
* native one (no warning needed).
|
|
208
242
|
*/
|
|
209
|
-
function
|
|
243
|
+
function syntheticClickTag(content, clickIndex) {
|
|
210
244
|
// Find the nearest preceding `<` that starts a tag (skip `</` closers).
|
|
211
245
|
let i = clickIndex - 1;
|
|
212
246
|
let tagOpenIndex = -1;
|
|
@@ -219,15 +253,15 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
|
219
253
|
// which shouldn't happen for a well-formed template but we defensively
|
|
220
254
|
// treat it as synthetic to preserve the prior-behaviour warning.
|
|
221
255
|
if (content[i] === '>') {
|
|
222
|
-
return
|
|
256
|
+
return UNKNOWN_TAG;
|
|
223
257
|
}
|
|
224
258
|
i--;
|
|
225
259
|
}
|
|
226
260
|
if (tagOpenIndex < 0)
|
|
227
|
-
return
|
|
261
|
+
return UNKNOWN_TAG;
|
|
228
262
|
const tagMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(content.slice(tagOpenIndex));
|
|
229
263
|
if (!tagMatch?.[1])
|
|
230
|
-
return
|
|
264
|
+
return UNKNOWN_TAG;
|
|
231
265
|
const tagName = tagMatch[1].toLowerCase();
|
|
232
266
|
if (tagName === 'a') {
|
|
233
267
|
// Bare <a> (no href) is non-interactive, so require a keyboard handler.
|
|
@@ -235,11 +269,11 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
|
|
|
235
269
|
// attribute text in between for an href attribute.
|
|
236
270
|
const tagEnd = content.indexOf('>', tagOpenIndex);
|
|
237
271
|
if (tagEnd < 0)
|
|
238
|
-
return
|
|
272
|
+
return 'a';
|
|
239
273
|
const attrs = content.slice(tagOpenIndex, tagEnd);
|
|
240
|
-
return
|
|
274
|
+
return /\shref\s*=/.test(attrs) ? undefined : 'a';
|
|
241
275
|
}
|
|
242
|
-
return
|
|
276
|
+
return NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName) ? undefined : tagName;
|
|
243
277
|
}
|
|
244
278
|
/** ASCII punctuation that reads as decoration rather than localizable text. */
|
|
245
279
|
const SYMBOL_ONLY_ASCII = '+-*=<>|/\\^~@#&!?%';
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Applies `furnace.json#validationExceptions[]` to a `furnace validate`
|
|
3
3
|
* result. An exception suppresses every finding of its check on its
|
|
4
|
-
* component
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* component, or with `tag`, a finding whose element tags it (and its
|
|
5
|
+
* sibling tag entries) cover. One that suppresses nothing is stale: the
|
|
6
|
+
* finding was fixed (or renamed) and the review record outlived it, so it
|
|
7
|
+
* is reported as a warning naming the entry to delete, which keeps the
|
|
8
|
+
* list honest under `--max-warnings 0`.
|
|
9
|
+
*
|
|
10
|
+
* An exception never suppresses an error: an error blocks apply, and a
|
|
11
|
+
* review record cannot make a broken component deployable. The error stays
|
|
12
|
+
* counted and the matching entry is reported as
|
|
13
|
+
* `validation-exception-on-error`.
|
|
8
14
|
*/
|
|
9
15
|
import type { FurnaceValidationException, ValidationIssue } from '../types/furnace.js';
|
|
10
16
|
/** The check name of the warning a stale exception produces. */
|
|
11
17
|
export declare const STALE_VALIDATION_EXCEPTION_CHECK = "stale-validation-exception";
|
|
18
|
+
/** The check name of the error an exception matching an error produces. */
|
|
19
|
+
export declare const EXCEPTION_ON_ERROR_CHECK = "validation-exception-on-error";
|
|
12
20
|
/** A finding an exception suppressed, with the exception's reason. */
|
|
13
21
|
export interface ExceptedValidationIssue extends ValidationIssue {
|
|
14
22
|
reason: string;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { createIssue } from './furnace-validate-helpers.js';
|
|
2
2
|
/** The check name of the warning a stale exception produces. */
|
|
3
3
|
export const STALE_VALIDATION_EXCEPTION_CHECK = 'stale-validation-exception';
|
|
4
|
+
/** The check name of the error an exception matching an error produces. */
|
|
5
|
+
export const EXCEPTION_ON_ERROR_CHECK = 'validation-exception-on-error';
|
|
4
6
|
function matches(exception, issue) {
|
|
5
7
|
return exception.component === issue.component && exception.check === issue.check;
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The exceptions that suppress a warning: an untagged one alone, or tagged
|
|
11
|
+
* ones that together cover every tag the finding names.
|
|
12
|
+
*/
|
|
13
|
+
function coveringExceptions(issue, candidates) {
|
|
14
|
+
const untagged = candidates.find((entry) => entry.tag === undefined);
|
|
15
|
+
if (untagged !== undefined)
|
|
16
|
+
return [untagged];
|
|
17
|
+
const tags = issue.tags ?? [];
|
|
18
|
+
if (tags.length === 0)
|
|
19
|
+
return [];
|
|
20
|
+
const covering = tags.map((tag) => candidates.find((entry) => entry.tag === tag));
|
|
21
|
+
return covering.every((entry) => entry !== undefined) ? [...new Set(covering)] : [];
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* Splits findings into counted and excepted, and reports stale exceptions.
|
|
9
25
|
*
|
|
@@ -18,14 +34,35 @@ export function applyValidationExceptions(issues, exceptions, judged) {
|
|
|
18
34
|
const outcome = { issues: [], excepted: [], stale: [] };
|
|
19
35
|
const list = exceptions ?? [];
|
|
20
36
|
const used = new Set();
|
|
37
|
+
const onError = new Set();
|
|
21
38
|
for (const issue of issues) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
39
|
+
const candidates = list.filter((entry) => matches(entry, issue));
|
|
40
|
+
if (issue.severity === 'error') {
|
|
41
|
+
for (const entry of candidates) {
|
|
42
|
+
used.add(entry);
|
|
43
|
+
onError.add(entry);
|
|
44
|
+
}
|
|
24
45
|
outcome.issues.push(issue);
|
|
25
46
|
continue;
|
|
26
47
|
}
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
const covering = coveringExceptions(issue, candidates);
|
|
49
|
+
if (covering.length === 0) {
|
|
50
|
+
// A tagged exception that covers some of the tags is not stale: its
|
|
51
|
+
// element still carries the finding, beside one it does not cover.
|
|
52
|
+
for (const entry of candidates) {
|
|
53
|
+
if (entry.tag !== undefined && issue.tags?.includes(entry.tag) === true)
|
|
54
|
+
used.add(entry);
|
|
55
|
+
}
|
|
56
|
+
outcome.issues.push(issue);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of covering)
|
|
60
|
+
used.add(entry);
|
|
61
|
+
outcome.excepted.push({ ...issue, reason: covering.map((entry) => entry.reason).join('; ') });
|
|
62
|
+
}
|
|
63
|
+
for (const exception of onError) {
|
|
64
|
+
outcome.issues.push(createIssue(exception.component, 'error', EXCEPTION_ON_ERROR_CHECK, `furnace.json validationExceptions entry for [${exception.check}] matches an error, ` +
|
|
65
|
+
'which an exception cannot suppress. Fix the error, then delete the entry.'));
|
|
29
66
|
}
|
|
30
67
|
for (const exception of list) {
|
|
31
68
|
if (used.has(exception) || !judged(exception.component))
|
|
@@ -18,10 +18,39 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
|
|
|
18
18
|
const NAMING_ATTRIBUTES = ['data-l10n-id', 'aria-label', '.arialabel'];
|
|
19
19
|
/** Attributes that give the button a visible label, so it is not icon-only. */
|
|
20
20
|
const LABEL_ATTRIBUTES = ['label', '.label'];
|
|
21
|
+
/**
|
|
22
|
+
* Whether the button's content has a non-blank character outside tags.
|
|
23
|
+
* Scans instead of stripping tags, so nested or unbalanced markup cannot
|
|
24
|
+
* leave a fragment behind that reads as text, and a `>` inside a quoted
|
|
25
|
+
* attribute value does not end its tag.
|
|
26
|
+
*/
|
|
27
|
+
function hasVisibleText(markup) {
|
|
28
|
+
let inTag = false;
|
|
29
|
+
let quote;
|
|
30
|
+
for (const char of markup) {
|
|
31
|
+
if (quote !== undefined) {
|
|
32
|
+
if (char === quote)
|
|
33
|
+
quote = undefined;
|
|
34
|
+
}
|
|
35
|
+
else if (inTag) {
|
|
36
|
+
if (char === '"' || char === "'")
|
|
37
|
+
quote = char;
|
|
38
|
+
else if (char === '>')
|
|
39
|
+
inTag = false;
|
|
40
|
+
}
|
|
41
|
+
else if (char === '<') {
|
|
42
|
+
inTag = true;
|
|
43
|
+
}
|
|
44
|
+
else if (char.trim() !== '') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
21
50
|
function isIconOnly(attrs, innerText) {
|
|
22
51
|
if (LABEL_ATTRIBUTES.some((name) => attrs.has(name)))
|
|
23
52
|
return false;
|
|
24
|
-
if (innerText
|
|
53
|
+
if (hasVisibleText(innerText))
|
|
25
54
|
return false;
|
|
26
55
|
const type = attrs.get('type') ?? '';
|
|
27
56
|
return attrs.has('iconsrc') || attrs.has('.iconsrc') || /(^|\s)icon(\s|$)/.test(type);
|
|
@@ -17,7 +17,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
17
17
|
import { appendFile, link, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
20
|
-
import { ensureDir } from '../utils/fs.js';
|
|
20
|
+
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
21
21
|
import { info, verbose } from '../utils/logger.js';
|
|
22
22
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
23
23
|
import { isValidHistoryPath } from './config-validate-history.js';
|
|
@@ -131,6 +131,13 @@ export async function migrateLegacyHistoryLog(patchesDir, target) {
|
|
|
131
131
|
await link(claimed, target);
|
|
132
132
|
}
|
|
133
133
|
catch (error) {
|
|
134
|
+
// Windows can report two racing renames of one file as both
|
|
135
|
+
// succeeding, the file ending under the other claimant's name. That
|
|
136
|
+
// claimant holds the records and moves them, so this one stands down.
|
|
137
|
+
if (getNodeErrorCode(error) === 'ENOENT' && !(await pathExists(claimed))) {
|
|
138
|
+
verbose('History log migration lost the claim to a concurrent command');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
134
141
|
// EEXIST: both logs exist. Anything else (a filesystem without hard
|
|
135
142
|
// links, a target on another device): the append below creates the
|
|
136
143
|
// target when it is missing, so it covers that case too.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefixes every line of a chunked stream, for output from several
|
|
3
|
+
* concurrent harness instances (`fireforge test --jobs`) that share one
|
|
4
|
+
* terminal and one run log. Without a prefix, interleaved lines from two
|
|
5
|
+
* browsers cannot be attributed after the fact.
|
|
6
|
+
*/
|
|
7
|
+
/** A stateful line prefixer for one stream. */
|
|
8
|
+
export interface LinePrefixer {
|
|
9
|
+
/** Returns `chunk` with the prefix inserted at the start of each line. */
|
|
10
|
+
transform(chunk: string): string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a prefixer. Line state carries across chunks, so a line split
|
|
14
|
+
* between two chunks gets the prefix exactly once.
|
|
15
|
+
*
|
|
16
|
+
* @param prefix - Text to insert, e.g. `[job 2] `
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLinePrefixer(prefix: string): LinePrefixer;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Prefixes every line of a chunked stream, for output from several
|
|
4
|
+
* concurrent harness instances (`fireforge test --jobs`) that share one
|
|
5
|
+
* terminal and one run log. Without a prefix, interleaved lines from two
|
|
6
|
+
* browsers cannot be attributed after the fact.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Creates a prefixer. Line state carries across chunks, so a line split
|
|
10
|
+
* between two chunks gets the prefix exactly once.
|
|
11
|
+
*
|
|
12
|
+
* @param prefix - Text to insert, e.g. `[job 2] `
|
|
13
|
+
*/
|
|
14
|
+
export function createLinePrefixer(prefix) {
|
|
15
|
+
let atLineStart = true;
|
|
16
|
+
return {
|
|
17
|
+
transform(chunk) {
|
|
18
|
+
if (chunk.length === 0)
|
|
19
|
+
return chunk;
|
|
20
|
+
let out = '';
|
|
21
|
+
let start = 0;
|
|
22
|
+
for (let index = chunk.indexOf('\n'); index !== -1; index = chunk.indexOf('\n', start)) {
|
|
23
|
+
out += `${atLineStart ? prefix : ''}${chunk.slice(start, index + 1)}`;
|
|
24
|
+
atLineStart = true;
|
|
25
|
+
start = index + 1;
|
|
26
|
+
}
|
|
27
|
+
if (start < chunk.length) {
|
|
28
|
+
out += `${atLineStart ? prefix : ''}${chunk.slice(start)}`;
|
|
29
|
+
atLineStart = false;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface MachOptions {
|
|
|
47
47
|
onProcessGroup?: (pgid: number) => void;
|
|
48
48
|
/** Exec-layer hook, see {@link ProcessGroupOptions.postCloseSweep}. Capture-only. */
|
|
49
49
|
postCloseSweep?: () => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Prefix for every echoed and logged line (concurrent `test --jobs`
|
|
52
|
+
* instances). The capture and `onOutputChunk` stay raw. Capture-only.
|
|
53
|
+
*/
|
|
54
|
+
outputPrefix?: string | undefined;
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* Result of running a mach command while capturing streamed output.
|
|
@@ -263,6 +268,8 @@ export interface MachTestSuiteOptions {
|
|
|
263
268
|
* the harness launched. See {@link MachOptions}.
|
|
264
269
|
*/
|
|
265
270
|
teardown?: Pick<MachOptions, 'onOutputChunk' | 'onProcessGroup' | 'postCloseSweep'> | undefined;
|
|
271
|
+
/** Line prefix for echo and run log, see {@link MachOptions.outputPrefix}. */
|
|
272
|
+
outputPrefix?: string | undefined;
|
|
266
273
|
}
|
|
267
274
|
/**
|
|
268
275
|
* Runs one mach test command (`mach test` / `mach xpcshell-test` / `mach
|