@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
|
@@ -6,20 +6,29 @@
|
|
|
6
6
|
* or referencing foreign custom properties, but say nothing about
|
|
7
7
|
* `gap: 7px`, `font-size: 13px` or `z-index: 9999`. Those are the values a
|
|
8
8
|
* design scale exists to remove, and without a rule they drift back one
|
|
9
|
-
* literal at a time. A scale names its properties, the token prefix
|
|
10
|
-
* defines its steps, and the literals that are not steps at
|
|
11
|
-
* `auto`, a percentage).
|
|
9
|
+
* literal at a time. A scale names its properties, the token prefix (or
|
|
10
|
+
* prefixes) that defines its steps, and the literals that are not steps at
|
|
11
|
+
* all (`0`, `auto`, a percentage).
|
|
12
12
|
*
|
|
13
13
|
* Judged per value atom, after peeling `var()` innermost-first so a fallback
|
|
14
14
|
* is held to the same scale as the token it backs: `var(--x-gap, 7px)` is a
|
|
15
15
|
* literal in disguise. `calc()`, `min()`, `max()` and `clamp()` are
|
|
16
16
|
* transparent, so a value built from tokens passes and one that adds a raw
|
|
17
|
-
* length does not.
|
|
18
|
-
*
|
|
17
|
+
* length does not. Inside them, `allowInCalc` names literals legal only
|
|
18
|
+
* there (a unitless multiplier), and `offsetCap` lets a declaration
|
|
19
|
+
* subtract a few px from a token (a border inset) without allowing the px
|
|
20
|
+
* value anywhere else.
|
|
21
|
+
*
|
|
22
|
+
* Declarations inside descriptor at-rules (`@font-face`, `@counter-style`,
|
|
23
|
+
* `@property`, `@page`, …) are not property declarations and are not
|
|
24
|
+
* judged: `font-weight` in `@font-face` names a face, not a type step.
|
|
19
25
|
*
|
|
20
26
|
* Runs over CSS files the patch creates, not files it modifies: an upstream
|
|
21
|
-
* stylesheet is not the fork's to put on a scale. A
|
|
22
|
-
*
|
|
27
|
+
* stylesheet is not the fork's to put on a scale. A file a scale `exclude`s
|
|
28
|
+
* is not judged against that scale. A line carrying
|
|
29
|
+
* `fireforge-ignore: token-scale` is skipped. A scale with `minFiles` also
|
|
30
|
+
* fails the queue (`token-scale-vacuous`) when fewer created files carry
|
|
31
|
+
* its properties, so an emptied scan set cannot pass by judging nothing.
|
|
23
32
|
*/
|
|
24
33
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
25
34
|
import type { PatchLintTokenScale } from '../types/config.js';
|
|
@@ -31,9 +40,18 @@ export declare function findOffScaleAtom(value: string, scale: PatchLintTokenSca
|
|
|
31
40
|
/**
|
|
32
41
|
* Lints one patch-created CSS file against the configured scales.
|
|
33
42
|
*
|
|
34
|
-
* @param file - Engine-relative path, for the issue
|
|
43
|
+
* @param file - Engine-relative path, for the issue and the `exclude` globs
|
|
35
44
|
* @param css - The file's full content
|
|
36
45
|
* @param scales - `patchLint.tokenScales`
|
|
37
46
|
* @returns One `token-scale` error per off-scale declaration
|
|
38
47
|
*/
|
|
39
48
|
export declare function lintTokenScales(file: string, css: string, scales: readonly PatchLintTokenScale[]): PatchLintIssue[];
|
|
49
|
+
/**
|
|
50
|
+
* The `minFiles` floor of every scale over the queue's created stylesheets:
|
|
51
|
+
* one `token-scale-vacuous` error per scale that fewer created CSS files
|
|
52
|
+
* (outside its `exclude`) carry a declaration of.
|
|
53
|
+
*
|
|
54
|
+
* @param createdFiles - Every patch-created file's path and content
|
|
55
|
+
* @param scales - `patchLint.tokenScales`
|
|
56
|
+
*/
|
|
57
|
+
export declare function lintTokenScaleFloors(createdFiles: Iterable<readonly [string, string]>, scales: readonly PatchLintTokenScale[]): PatchLintIssue[];
|
|
@@ -7,26 +7,44 @@
|
|
|
7
7
|
* or referencing foreign custom properties, but say nothing about
|
|
8
8
|
* `gap: 7px`, `font-size: 13px` or `z-index: 9999`. Those are the values a
|
|
9
9
|
* design scale exists to remove, and without a rule they drift back one
|
|
10
|
-
* literal at a time. A scale names its properties, the token prefix
|
|
11
|
-
* defines its steps, and the literals that are not steps at
|
|
12
|
-
* `auto`, a percentage).
|
|
10
|
+
* literal at a time. A scale names its properties, the token prefix (or
|
|
11
|
+
* prefixes) that defines its steps, and the literals that are not steps at
|
|
12
|
+
* all (`0`, `auto`, a percentage).
|
|
13
13
|
*
|
|
14
14
|
* Judged per value atom, after peeling `var()` innermost-first so a fallback
|
|
15
15
|
* is held to the same scale as the token it backs: `var(--x-gap, 7px)` is a
|
|
16
16
|
* literal in disguise. `calc()`, `min()`, `max()` and `clamp()` are
|
|
17
17
|
* transparent, so a value built from tokens passes and one that adds a raw
|
|
18
|
-
* length does not.
|
|
19
|
-
*
|
|
18
|
+
* length does not. Inside them, `allowInCalc` names literals legal only
|
|
19
|
+
* there (a unitless multiplier), and `offsetCap` lets a declaration
|
|
20
|
+
* subtract a few px from a token (a border inset) without allowing the px
|
|
21
|
+
* value anywhere else.
|
|
22
|
+
*
|
|
23
|
+
* Declarations inside descriptor at-rules (`@font-face`, `@counter-style`,
|
|
24
|
+
* `@property`, `@page`, …) are not property declarations and are not
|
|
25
|
+
* judged: `font-weight` in `@font-face` names a face, not a type step.
|
|
20
26
|
*
|
|
21
27
|
* Runs over CSS files the patch creates, not files it modifies: an upstream
|
|
22
|
-
* stylesheet is not the fork's to put on a scale. A
|
|
23
|
-
*
|
|
28
|
+
* stylesheet is not the fork's to put on a scale. A file a scale `exclude`s
|
|
29
|
+
* is not judged against that scale. A line carrying
|
|
30
|
+
* `fireforge-ignore: token-scale` is skipped. A scale with `minFiles` also
|
|
31
|
+
* fails the queue (`token-scale-vacuous`) when fewer created files carry
|
|
32
|
+
* its properties, so an emptied scan set cannot pass by judging nothing.
|
|
24
33
|
*/
|
|
34
|
+
import { matchesAnyGlob } from '../utils/path-glob.js';
|
|
25
35
|
import { escapeRegex } from '../utils/regex.js';
|
|
26
36
|
const IGNORE_MARKER = 'fireforge-ignore: token-scale';
|
|
27
37
|
/** CSS-wide keywords: never a step on any scale, always legal. */
|
|
28
38
|
const CSS_WIDE_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
|
|
29
39
|
const TOKEN_PLACEHOLDER = '\u0000token\u0000';
|
|
40
|
+
const MATH_FUNCTION = /^(?:calc|min|max|clamp)$/i;
|
|
41
|
+
const OPERATORS = new Set(['+', '-', '*', '/']);
|
|
42
|
+
/**
|
|
43
|
+
* At-rules whose blocks hold descriptors, not property declarations. A
|
|
44
|
+
* block nested in one (an `@page` margin box, a `@font-feature-values`
|
|
45
|
+
* feature block) is a descriptor block too.
|
|
46
|
+
*/
|
|
47
|
+
const DESCRIPTOR_AT_RULE = /^@(?:font-face|counter-style|property|font-feature-values|font-palette-values|page|view-transition)\b/i;
|
|
30
48
|
/** Exact name, or a `*`-suffixed prefix. */
|
|
31
49
|
function matchesName(name, patterns) {
|
|
32
50
|
return patterns.some((pattern) => pattern.endsWith('*') ? name.startsWith(pattern.slice(0, -1)) : name === pattern);
|
|
@@ -34,16 +52,70 @@ function matchesName(name, patterns) {
|
|
|
34
52
|
function matchesLiteral(atom, patterns) {
|
|
35
53
|
return patterns.some((pattern) => new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`).test(atom));
|
|
36
54
|
}
|
|
55
|
+
/** The scale's token prefixes, whether configured as one or a list. */
|
|
56
|
+
function scalePrefixes(scale) {
|
|
57
|
+
return typeof scale.prefix === 'string' ? [scale.prefix] : scale.prefix;
|
|
58
|
+
}
|
|
59
|
+
/** The scale as a message names it: `--x-space-*`, or `--x-space-* / --x-gap-*`. */
|
|
60
|
+
function scaleLabel(scale) {
|
|
61
|
+
return scalePrefixes(scale)
|
|
62
|
+
.map((prefix) => `${prefix}*`)
|
|
63
|
+
.join(' / ');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Splits a value into atoms, keeping operators as atoms and noting which
|
|
67
|
+
* atoms sit inside a math function. A math function's name is dropped; any
|
|
68
|
+
* other function's name stays an atom, judged like a literal.
|
|
69
|
+
*/
|
|
70
|
+
function valueAtoms(value) {
|
|
71
|
+
const atoms = [];
|
|
72
|
+
const mathStack = [];
|
|
73
|
+
let current = '';
|
|
74
|
+
const flush = () => {
|
|
75
|
+
if (current !== '')
|
|
76
|
+
atoms.push({ text: current, inMath: mathStack.includes(true) });
|
|
77
|
+
current = '';
|
|
78
|
+
};
|
|
79
|
+
for (const char of value) {
|
|
80
|
+
if (char === '(') {
|
|
81
|
+
const isMath = MATH_FUNCTION.test(current);
|
|
82
|
+
if (isMath)
|
|
83
|
+
current = '';
|
|
84
|
+
else
|
|
85
|
+
flush();
|
|
86
|
+
mathStack.push(isMath);
|
|
87
|
+
}
|
|
88
|
+
else if (char === ')') {
|
|
89
|
+
flush();
|
|
90
|
+
mathStack.pop();
|
|
91
|
+
}
|
|
92
|
+
else if (char === '*' || char === '/') {
|
|
93
|
+
flush();
|
|
94
|
+
atoms.push({ text: char, inMath: mathStack.includes(true) });
|
|
95
|
+
}
|
|
96
|
+
else if (char === ',' || /\s/.test(char)) {
|
|
97
|
+
flush();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
current += char;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
flush();
|
|
104
|
+
return atoms;
|
|
105
|
+
}
|
|
106
|
+
const PX_LITERAL = /^(\d*\.?\d+)px$/;
|
|
37
107
|
/**
|
|
38
108
|
* Returns the first atom of `value` that is off `scale`, or undefined when
|
|
39
109
|
* every atom resolves to the scale.
|
|
40
110
|
*/
|
|
41
111
|
export function findOffScaleAtom(value, scale) {
|
|
42
112
|
let rest = value.replace(/!\s*important\s*$/i, '');
|
|
113
|
+
const prefixes = scalePrefixes(scale);
|
|
43
114
|
const varPattern = /var\(\s*(--[\w-]+)\s*(?:,([^()]*))?\)/;
|
|
44
115
|
for (let match = varPattern.exec(rest); match !== null; match = varPattern.exec(rest)) {
|
|
45
116
|
const name = match[1] ?? '';
|
|
46
|
-
|
|
117
|
+
const onScale = prefixes.some((prefix) => name.startsWith(prefix));
|
|
118
|
+
if (!onScale && !matchesName(name, scale.allow ?? [])) {
|
|
47
119
|
return `var(${name})`;
|
|
48
120
|
}
|
|
49
121
|
const fallback = match[2];
|
|
@@ -54,54 +126,159 @@ export function findOffScaleAtom(value, scale) {
|
|
|
54
126
|
}
|
|
55
127
|
rest = `${rest.slice(0, match.index)} ${TOKEN_PLACEHOLDER} ${rest.slice(match.index + match[0].length)}`;
|
|
56
128
|
}
|
|
57
|
-
const atoms = rest
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
129
|
+
const atoms = valueAtoms(rest);
|
|
130
|
+
// A px offset subtracted from a token (`token - 1px`, `token - 1px - 1px`)
|
|
131
|
+
// counts toward `offsetCap`, summed over the declaration.
|
|
132
|
+
let offsetTotal = 0;
|
|
133
|
+
let lastOffsetIndex = -1;
|
|
134
|
+
for (const [index, atom] of atoms.entries()) {
|
|
135
|
+
if (OPERATORS.has(atom.text) || atom.text === TOKEN_PLACEHOLDER)
|
|
136
|
+
continue;
|
|
137
|
+
if (CSS_WIDE_KEYWORDS.has(atom.text.toLowerCase()))
|
|
138
|
+
continue;
|
|
139
|
+
if (matchesLiteral(atom.text, scale.allowLiterals ?? []))
|
|
140
|
+
continue;
|
|
141
|
+
if (atom.inMath && matchesLiteral(atom.text, scale.allowInCalc ?? []))
|
|
142
|
+
continue;
|
|
143
|
+
const px = PX_LITERAL.exec(atom.text);
|
|
144
|
+
if (atom.inMath && px !== null && scale.offsetCap !== undefined) {
|
|
145
|
+
const subtrahendOf = atoms[index - 2]?.text;
|
|
146
|
+
const subtracted = atoms[index - 1]?.text === '-' &&
|
|
147
|
+
(subtrahendOf === TOKEN_PLACEHOLDER || lastOffsetIndex === index - 2);
|
|
148
|
+
if (subtracted) {
|
|
149
|
+
offsetTotal += Number(px[1]);
|
|
150
|
+
lastOffsetIndex = index;
|
|
151
|
+
if (offsetTotal <= scale.offsetCap)
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return atom.text;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
65
158
|
}
|
|
66
159
|
/** Blanks block comments, keeping newlines so offsets map to lines. */
|
|
67
160
|
function maskComments(css) {
|
|
68
161
|
return css.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' '));
|
|
69
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Blanks the contents of descriptor at-rule blocks (keeping braces and
|
|
165
|
+
* newlines), so the declaration scan never sees their descriptors.
|
|
166
|
+
*/
|
|
167
|
+
function maskDescriptorBlocks(css) {
|
|
168
|
+
const out = css.split('');
|
|
169
|
+
const stack = [];
|
|
170
|
+
let preludeStart = 0;
|
|
171
|
+
for (let i = 0; i < css.length; i++) {
|
|
172
|
+
const char = css.charAt(i);
|
|
173
|
+
const inDescriptor = stack[stack.length - 1] === true;
|
|
174
|
+
if (char === '{') {
|
|
175
|
+
const prelude = css.slice(preludeStart, i).trim();
|
|
176
|
+
stack.push(inDescriptor || DESCRIPTOR_AT_RULE.test(prelude));
|
|
177
|
+
preludeStart = i + 1;
|
|
178
|
+
}
|
|
179
|
+
else if (char === '}') {
|
|
180
|
+
stack.pop();
|
|
181
|
+
preludeStart = i + 1;
|
|
182
|
+
}
|
|
183
|
+
else if (char === ';') {
|
|
184
|
+
preludeStart = i + 1;
|
|
185
|
+
}
|
|
186
|
+
if (inDescriptor && char !== '\n' && char !== '{' && char !== '}')
|
|
187
|
+
out[i] = ' ';
|
|
188
|
+
}
|
|
189
|
+
return out.join('');
|
|
190
|
+
}
|
|
191
|
+
/** Every property declaration of a stylesheet, descriptors and comments left out. */
|
|
192
|
+
function scanDeclarations(css) {
|
|
193
|
+
const masked = maskDescriptorBlocks(maskComments(css));
|
|
194
|
+
const declarations = [];
|
|
195
|
+
const pattern = /(^|[{;\s])([a-zA-Z][a-zA-Z-]*)\s*:\s*([^;{}]*)/g;
|
|
196
|
+
for (let match = pattern.exec(masked); match !== null; match = pattern.exec(masked)) {
|
|
197
|
+
const start = match.index + (match[1] ?? '').length;
|
|
198
|
+
declarations.push({
|
|
199
|
+
property: (match[2] ?? '').toLowerCase(),
|
|
200
|
+
value: (match[3] ?? '').trim(),
|
|
201
|
+
line: masked.slice(0, start).split('\n').length,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return declarations;
|
|
205
|
+
}
|
|
206
|
+
/** The scales that govern `file`: those that do not `exclude` it. */
|
|
207
|
+
function scalesFor(file, scales) {
|
|
208
|
+
return scales.filter((scale) => !matchesAnyGlob(file, scale.exclude ?? []));
|
|
209
|
+
}
|
|
70
210
|
/**
|
|
71
211
|
* Lints one patch-created CSS file against the configured scales.
|
|
72
212
|
*
|
|
73
|
-
* @param file - Engine-relative path, for the issue
|
|
213
|
+
* @param file - Engine-relative path, for the issue and the `exclude` globs
|
|
74
214
|
* @param css - The file's full content
|
|
75
215
|
* @param scales - `patchLint.tokenScales`
|
|
76
216
|
* @returns One `token-scale` error per off-scale declaration
|
|
77
217
|
*/
|
|
78
218
|
export function lintTokenScales(file, css, scales) {
|
|
79
|
-
|
|
219
|
+
const governing = scalesFor(file, scales);
|
|
220
|
+
if (governing.length === 0)
|
|
80
221
|
return [];
|
|
81
222
|
const rawLines = css.split('\n');
|
|
82
|
-
const masked = maskComments(css);
|
|
83
223
|
const issues = [];
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const property = (match[2] ?? '').toLowerCase();
|
|
87
|
-
const value = (match[3] ?? '').trim();
|
|
88
|
-
const scale = scales.find((candidate) => matchesName(property, candidate.properties));
|
|
224
|
+
for (const { property, value, line } of scanDeclarations(css)) {
|
|
225
|
+
const scale = governing.find((candidate) => matchesName(property, candidate.properties));
|
|
89
226
|
if (scale === undefined || value === '')
|
|
90
227
|
continue;
|
|
91
|
-
const start = match.index + (match[1] ?? '').length;
|
|
92
|
-
const line = masked.slice(0, start).split('\n').length;
|
|
93
228
|
if ((rawLines[line - 1] ?? '').includes(IGNORE_MARKER))
|
|
94
229
|
continue;
|
|
95
230
|
const atom = findOffScaleAtom(value, scale);
|
|
96
231
|
if (atom === undefined)
|
|
97
232
|
continue;
|
|
233
|
+
const label = scaleLabel(scale);
|
|
98
234
|
issues.push({
|
|
99
235
|
file,
|
|
100
236
|
check: 'token-scale',
|
|
101
237
|
fingerprint: `token-scale:${file}:${property}:${value}`,
|
|
102
|
-
message: `Line ${String(line)}: ${property}: ${value} is off the ${
|
|
103
|
-
`("${atom}"). Use a ${
|
|
104
|
-
`patchLint.tokenScales (allowLiterals / allow) or mark the line "${IGNORE_MARKER}".`,
|
|
238
|
+
message: `Line ${String(line)}: ${property}: ${value} is off the ${label} scale ` +
|
|
239
|
+
`("${atom}"). Use a ${label} token, or list a reviewed exception in ` +
|
|
240
|
+
`patchLint.tokenScales (allowLiterals / allowInCalc / allow) or mark the line "${IGNORE_MARKER}".`,
|
|
241
|
+
severity: 'error',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return issues;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The `minFiles` floor of every scale over the queue's created stylesheets:
|
|
248
|
+
* one `token-scale-vacuous` error per scale that fewer created CSS files
|
|
249
|
+
* (outside its `exclude`) carry a declaration of.
|
|
250
|
+
*
|
|
251
|
+
* @param createdFiles - Every patch-created file's path and content
|
|
252
|
+
* @param scales - `patchLint.tokenScales`
|
|
253
|
+
*/
|
|
254
|
+
export function lintTokenScaleFloors(createdFiles, scales) {
|
|
255
|
+
const floored = scales.filter((scale) => scale.minFiles !== undefined);
|
|
256
|
+
if (floored.length === 0)
|
|
257
|
+
return [];
|
|
258
|
+
const counts = new Map(floored.map((scale) => [scale, 0]));
|
|
259
|
+
for (const [file, content] of createdFiles) {
|
|
260
|
+
if (!file.endsWith('.css'))
|
|
261
|
+
continue;
|
|
262
|
+
const properties = new Set(scanDeclarations(content).map((entry) => entry.property));
|
|
263
|
+
for (const scale of scalesFor(file, floored)) {
|
|
264
|
+
if ([...properties].some((property) => matchesName(property, scale.properties))) {
|
|
265
|
+
counts.set(scale, (counts.get(scale) ?? 0) + 1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const issues = [];
|
|
270
|
+
for (const [scale, count] of counts) {
|
|
271
|
+
const floor = scale.minFiles ?? 0;
|
|
272
|
+
if (count >= floor)
|
|
273
|
+
continue;
|
|
274
|
+
const label = scaleLabel(scale);
|
|
275
|
+
issues.push({
|
|
276
|
+
file: 'fireforge.json',
|
|
277
|
+
check: 'token-scale-vacuous',
|
|
278
|
+
fingerprint: `token-scale-vacuous:${label}`,
|
|
279
|
+
message: `The ${label} scale governs ${String(count)} patch-created stylesheet(s), under its ` +
|
|
280
|
+
`minFiles ${String(floor)}. A scale that judges nothing passes vacuously: check its ` +
|
|
281
|
+
'properties and exclude globs, or lower minFiles after a reviewed move.',
|
|
105
282
|
severity: 'error',
|
|
106
283
|
});
|
|
107
284
|
}
|
|
@@ -14,7 +14,7 @@ import { hasRelativeImport } from './patch-lint-imports.js';
|
|
|
14
14
|
import { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
15
15
|
import { lintMozBuildSortedLists } from './patch-lint-mozbuild.js';
|
|
16
16
|
import { lintObserverTopics } from './patch-lint-observer.js';
|
|
17
|
-
import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from './patch-lint-ownership.js';
|
|
17
|
+
import { resolvableModulesForTests, resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from './patch-lint-ownership.js';
|
|
18
18
|
import { invokePatchLintPrettier } from './patch-lint-prettier.js';
|
|
19
19
|
import { isTestFile, lintPatchSize, measurePatchSize, } from './patch-lint-size.js';
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -495,7 +495,7 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
495
495
|
// Export-time twin of per-patch lint's test-file pass, so
|
|
496
496
|
// export/re-export and `lint --per-patch` agree on the checked surface.
|
|
497
497
|
if (config.patchLint.checkJsTestFiles === true) {
|
|
498
|
-
const testGrouped = await runCheckJsTestFilesGrouped(repoDir, resolvePatchOwnedTestScripts(newFiles, patchQueueCtx), config.patchLint, dirname(repoDir));
|
|
498
|
+
const testGrouped = await runCheckJsTestFilesGrouped(repoDir, resolvePatchOwnedTestScripts(newFiles, patchQueueCtx), config.patchLint, dirname(repoDir), undefined, patchQueueCtx ? resolvableModulesForTests(patchQueueCtx) : undefined);
|
|
499
499
|
issues.push(...testGrouped.global);
|
|
500
500
|
for (const [rel, list] of testGrouped.byFile) {
|
|
501
501
|
if (options?.checkJsReportScope && !options.checkJsReportScope.has(rel))
|
|
@@ -19,6 +19,7 @@ import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.j
|
|
|
19
19
|
import { warn } from '../utils/logger.js';
|
|
20
20
|
import { isArray, isObject } from '../utils/validation.js';
|
|
21
21
|
import { assertPatchDirectoryLockHeld } from './patch-lock.js';
|
|
22
|
+
import { canonicalizeManifest, canonicalizePatchRow } from './patch-manifest-key-order.js';
|
|
22
23
|
import { validatePatchesManifest } from './patch-manifest-validate.js';
|
|
23
24
|
import { manifestWithRetiredSlots } from './patch-retired.js';
|
|
24
25
|
/** Filename for the patches manifest */
|
|
@@ -86,7 +87,7 @@ export async function loadPatchesManifestForWrite(patchesDir) {
|
|
|
86
87
|
*/
|
|
87
88
|
export async function savePatchesManifest(patchesDir, manifest) {
|
|
88
89
|
const manifestPath = join(patchesDir, PATCHES_MANIFEST);
|
|
89
|
-
await writeJson(manifestPath, manifest);
|
|
90
|
+
await writeJson(manifestPath, canonicalizeManifest(manifest));
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
93
|
* Mutates selected manifest rows while preserving the raw JSON shape of every
|
|
@@ -139,6 +140,12 @@ export async function mutatePatchRowsInManifest(patchesDir, filenames, mutator)
|
|
|
139
140
|
}
|
|
140
141
|
if (changedIndexes.length === 0)
|
|
141
142
|
return [];
|
|
143
|
+
// Only the rows this call changed are put in canonical order, so a key
|
|
144
|
+
// it sets lands where every other writer puts it; untouched rows keep
|
|
145
|
+
// their raw shape.
|
|
146
|
+
for (const index of changedIndexes) {
|
|
147
|
+
rawPatches[index] = canonicalizePatchRow(rawPatches[index]);
|
|
148
|
+
}
|
|
142
149
|
const nextRawManifest = {
|
|
143
150
|
...rawManifest,
|
|
144
151
|
patches: rawPatches,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one key order every `patches.json` writer uses for a patch row and
|
|
3
|
+
* for the manifest itself. Writers used to disagree: `export` spread
|
|
4
|
+
* `sourceProduct` before `sourceVersion` while a full-manifest save put it
|
|
5
|
+
* after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
|
|
6
|
+
* the manifest.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Returns a copy of a patch row with its keys in canonical order.
|
|
10
|
+
*
|
|
11
|
+
* @param row - A `PatchMetadata` row, typed or raw
|
|
12
|
+
* @returns The same fields in canonical key order
|
|
13
|
+
*/
|
|
14
|
+
export declare function canonicalizePatchRow<T extends object>(row: T): T;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a copy of a manifest with its top-level keys and every row in
|
|
17
|
+
* canonical order.
|
|
18
|
+
*
|
|
19
|
+
* @param manifest - A `PatchesManifest`, typed or raw
|
|
20
|
+
* @returns The manifest in canonical key order
|
|
21
|
+
*/
|
|
22
|
+
export declare function canonicalizeManifest<T extends {
|
|
23
|
+
patches: readonly object[];
|
|
24
|
+
}>(manifest: T): T;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The one key order every `patches.json` writer uses for a patch row and
|
|
4
|
+
* for the manifest itself. Writers used to disagree: `export` spread
|
|
5
|
+
* `sourceProduct` before `sourceVersion` while a full-manifest save put it
|
|
6
|
+
* after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
|
|
7
|
+
* the manifest.
|
|
8
|
+
*/
|
|
9
|
+
/** Patch-row keys in the order of the `PatchMetadata` interface. */
|
|
10
|
+
const PATCH_ROW_KEY_ORDER = [
|
|
11
|
+
'filename',
|
|
12
|
+
'order',
|
|
13
|
+
'category',
|
|
14
|
+
'name',
|
|
15
|
+
'description',
|
|
16
|
+
'createdAt',
|
|
17
|
+
'sourceEsrVersion',
|
|
18
|
+
'sourceProduct',
|
|
19
|
+
'sourceVersion',
|
|
20
|
+
'filesAffected',
|
|
21
|
+
'lintIgnore',
|
|
22
|
+
'tier',
|
|
23
|
+
'stagedDependencies',
|
|
24
|
+
];
|
|
25
|
+
/** Manifest keys in the order of the `PatchesManifest` interface. */
|
|
26
|
+
const MANIFEST_KEY_ORDER = ['version', 'patches', 'retired', 'reserved'];
|
|
27
|
+
function reorder(value, order) {
|
|
28
|
+
const source = value;
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const key of order) {
|
|
31
|
+
if (Object.hasOwn(source, key))
|
|
32
|
+
out[key] = source[key];
|
|
33
|
+
}
|
|
34
|
+
// Keys this build does not know keep their relative order after the
|
|
35
|
+
// known ones, so a newer writer's fields survive an older rewrite.
|
|
36
|
+
for (const key of Object.keys(source)) {
|
|
37
|
+
if (!Object.hasOwn(out, key))
|
|
38
|
+
out[key] = source[key];
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns a copy of a patch row with its keys in canonical order.
|
|
44
|
+
*
|
|
45
|
+
* @param row - A `PatchMetadata` row, typed or raw
|
|
46
|
+
* @returns The same fields in canonical key order
|
|
47
|
+
*/
|
|
48
|
+
export function canonicalizePatchRow(row) {
|
|
49
|
+
return reorder(row, PATCH_ROW_KEY_ORDER);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a copy of a manifest with its top-level keys and every row in
|
|
53
|
+
* canonical order.
|
|
54
|
+
*
|
|
55
|
+
* @param manifest - A `PatchesManifest`, typed or raw
|
|
56
|
+
* @returns The manifest in canonical key order
|
|
57
|
+
*/
|
|
58
|
+
export function canonicalizeManifest(manifest) {
|
|
59
|
+
const ordered = reorder(manifest, MANIFEST_KEY_ORDER);
|
|
60
|
+
return { ...ordered, patches: manifest.patches.map((row) => canonicalizePatchRow(row)) };
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manifest `patch stage` writes into the git index: HEAD's
|
|
3
|
+
* `patches.json` with only the named patches' rows (and their retired and
|
|
4
|
+
* reserved slots) taken from the working copy.
|
|
5
|
+
*
|
|
6
|
+
* Sessions sharing one checkout each touch their own rows of the shared
|
|
7
|
+
* manifest. Staging the whole file commits every session's rows under one
|
|
8
|
+
* message; staging hunks with `git apply --cached --unidiff-zero` lands a
|
|
9
|
+
* kept insertion at its new-side line number once a dropped hunk above it
|
|
10
|
+
* shifted the file, and committed invalid JSON. Merging rows from the
|
|
11
|
+
* parsed manifests has neither failure: the result is built from structure,
|
|
12
|
+
* then validated before anything is staged.
|
|
13
|
+
*/
|
|
14
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
15
|
+
/** What staging does to one named patch's row. */
|
|
16
|
+
export type StagedRowChange = 'added' | 'updated' | 'removed' | 'unchanged';
|
|
17
|
+
/** The staged manifest and the per-patch row changes it carries. */
|
|
18
|
+
export interface StagedManifest {
|
|
19
|
+
manifest: PatchesManifest;
|
|
20
|
+
changes: Array<{
|
|
21
|
+
filename: string;
|
|
22
|
+
change: StagedRowChange;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Merges the named rows of `working` into `head`.
|
|
27
|
+
*
|
|
28
|
+
* Rows: every unnamed row comes from HEAD; a named row comes from the
|
|
29
|
+
* working copy, or leaves when the working copy no longer has it.
|
|
30
|
+
* Retired slots: a slot whose `filename` is named follows the working
|
|
31
|
+
* copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
|
|
32
|
+
* order a staged row now fills is dropped, as every manifest write does.
|
|
33
|
+
*
|
|
34
|
+
* @param head - HEAD's manifest, or null when HEAD has none
|
|
35
|
+
* @param working - The working-copy manifest
|
|
36
|
+
* @param filenames - Patch filenames whose rows to take from the working copy
|
|
37
|
+
* @returns The validated manifest and the row changes
|
|
38
|
+
* @throws Error when two rows would hold one order, or the merge does not
|
|
39
|
+
* validate
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeStagedManifest(head: PatchesManifest | null, working: PatchesManifest, filenames: ReadonlySet<string>): StagedManifest;
|
|
42
|
+
/**
|
|
43
|
+
* Serializes a staged manifest exactly as the manifest writers do, so the
|
|
44
|
+
* staged blob and a later full save of the same content are byte-identical.
|
|
45
|
+
*/
|
|
46
|
+
export declare function serializeStagedManifest(manifest: PatchesManifest): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The manifest `patch stage` writes into the git index: HEAD's
|
|
4
|
+
* `patches.json` with only the named patches' rows (and their retired and
|
|
5
|
+
* reserved slots) taken from the working copy.
|
|
6
|
+
*
|
|
7
|
+
* Sessions sharing one checkout each touch their own rows of the shared
|
|
8
|
+
* manifest. Staging the whole file commits every session's rows under one
|
|
9
|
+
* message; staging hunks with `git apply --cached --unidiff-zero` lands a
|
|
10
|
+
* kept insertion at its new-side line number once a dropped hunk above it
|
|
11
|
+
* shifted the file, and committed invalid JSON. Merging rows from the
|
|
12
|
+
* parsed manifests has neither failure: the result is built from structure,
|
|
13
|
+
* then validated before anything is staged.
|
|
14
|
+
*/
|
|
15
|
+
import { toError } from '../utils/errors.js';
|
|
16
|
+
import { canonicalizeManifest } from './patch-manifest-key-order.js';
|
|
17
|
+
import { validatePatchesManifest } from './patch-manifest-validate.js';
|
|
18
|
+
import { carryReservedSlots, carryRetiredSlots } from './patch-retired.js';
|
|
19
|
+
/**
|
|
20
|
+
* Merges the named rows of `working` into `head`.
|
|
21
|
+
*
|
|
22
|
+
* Rows: every unnamed row comes from HEAD; a named row comes from the
|
|
23
|
+
* working copy, or leaves when the working copy no longer has it.
|
|
24
|
+
* Retired slots: a slot whose `filename` is named follows the working
|
|
25
|
+
* copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
|
|
26
|
+
* order a staged row now fills is dropped, as every manifest write does.
|
|
27
|
+
*
|
|
28
|
+
* @param head - HEAD's manifest, or null when HEAD has none
|
|
29
|
+
* @param working - The working-copy manifest
|
|
30
|
+
* @param filenames - Patch filenames whose rows to take from the working copy
|
|
31
|
+
* @returns The validated manifest and the row changes
|
|
32
|
+
* @throws Error when two rows would hold one order, or the merge does not
|
|
33
|
+
* validate
|
|
34
|
+
*/
|
|
35
|
+
export function mergeStagedManifest(head, working, filenames) {
|
|
36
|
+
const headRows = head?.patches ?? [];
|
|
37
|
+
const changes = [...filenames].sort().map((filename) => {
|
|
38
|
+
const before = headRows.find((row) => row.filename === filename);
|
|
39
|
+
const after = working.patches.find((row) => row.filename === filename);
|
|
40
|
+
const change = before === undefined
|
|
41
|
+
? 'added'
|
|
42
|
+
: after === undefined
|
|
43
|
+
? 'removed'
|
|
44
|
+
: JSON.stringify(canonicalRow(before)) === JSON.stringify(canonicalRow(after))
|
|
45
|
+
? 'unchanged'
|
|
46
|
+
: 'updated';
|
|
47
|
+
return { filename, change };
|
|
48
|
+
});
|
|
49
|
+
const rows = [
|
|
50
|
+
...headRows.filter((row) => !filenames.has(row.filename)),
|
|
51
|
+
...working.patches.filter((row) => filenames.has(row.filename)),
|
|
52
|
+
].sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
53
|
+
assertNoCollision(rows);
|
|
54
|
+
const retiredSource = [
|
|
55
|
+
...(head?.retired ?? []).filter((slot) => !filenames.has(slot.filename)),
|
|
56
|
+
...(working.retired ?? []).filter((slot) => filenames.has(slot.filename)),
|
|
57
|
+
];
|
|
58
|
+
const retired = carryRetiredSlots({ retired: retiredSource }, rows);
|
|
59
|
+
const reserved = carryReservedSlots(head, rows);
|
|
60
|
+
const merged = {
|
|
61
|
+
version: 1,
|
|
62
|
+
patches: rows,
|
|
63
|
+
...(retired !== undefined ? { retired } : {}),
|
|
64
|
+
...(reserved !== undefined ? { reserved } : {}),
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
validatePatchesManifest(JSON.parse(JSON.stringify(merged)));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`the staged manifest would not validate: ${toError(error).message}`, {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { manifest: canonicalizeManifest(merged), changes };
|
|
75
|
+
}
|
|
76
|
+
function canonicalRow(row) {
|
|
77
|
+
return canonicalizeManifest({ patches: [row] }).patches[0];
|
|
78
|
+
}
|
|
79
|
+
function assertNoCollision(rows) {
|
|
80
|
+
const seen = new Map();
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const holder = seen.get(row.order);
|
|
83
|
+
if (holder !== undefined) {
|
|
84
|
+
throw new Error(`order ${String(row.order)} would be held by both ${holder} and ${row.filename}; ` +
|
|
85
|
+
'stage the patch that moved off that order as well');
|
|
86
|
+
}
|
|
87
|
+
seen.set(row.order, row.filename);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Serializes a staged manifest exactly as the manifest writers do, so the
|
|
92
|
+
* staged blob and a later full save of the same content are byte-identical.
|
|
93
|
+
*/
|
|
94
|
+
export function serializeStagedManifest(manifest) {
|
|
95
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
96
|
+
}
|