@yschimke/compose-design-map 1.44.0 → 1.46.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/design-map.mjs +149 -13
- package/package.json +1 -1
package/design-map.mjs
CHANGED
|
@@ -68,9 +68,37 @@ const LIGHT_MODE = "Light";
|
|
|
68
68
|
/** The tag discovery puts in the id of an `@OverrideVariant` reseed: `…_VARIANT_<name>`. */
|
|
69
69
|
const VARIANT_TAG = "_VARIANT_";
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The head of a capture's id with the `_VARIANT_<name>` reseed suffix removed, and whether there
|
|
73
|
+
* was one — `{ head, reseed }`.
|
|
74
|
+
*
|
|
75
|
+
* `_VARIANT_` is a legal substring of a Kotlin function name, so a plain `includes()` read the BASE
|
|
76
|
+
* capture of a composable called `Icon_VARIANT_Only` as a generated reseed. Every `continue` guarded
|
|
77
|
+
* by that test then dropped the composable outright, including its explicit `noReference` — a
|
|
78
|
+
* record whose whole purpose is to survive into the diagnostics.
|
|
79
|
+
*
|
|
80
|
+
* Discovery appends the tag to the id it has already built (`base.id + "_VARIANT_<name>"`), so the
|
|
81
|
+
* marker of an actual reseed is the LAST one and the function's own name is still in front of it.
|
|
82
|
+
* Splitting there and requiring the `.<functionName>` marker to survive in the head distinguishes
|
|
83
|
+
* the two: `…FooKt.Icon_VARIANT_Only_Light` splits to `…FooKt.Icon`, which no longer contains
|
|
84
|
+
* `.Icon_VARIANT_Only`, so it is a base capture; a real reseed of `Icon`,
|
|
85
|
+
* `…FooKt.Icon_Light_VARIANT_pressed`, splits to `…FooKt.Icon_Light`, which still contains `.Icon`.
|
|
86
|
+
*
|
|
87
|
+
* A capture that names no function cannot be told apart this way, so it keeps the old reading.
|
|
88
|
+
*/
|
|
89
|
+
function splitVariantTag(preview) {
|
|
90
|
+
const id = String(preview.id ?? "");
|
|
91
|
+
const at = id.lastIndexOf(VARIANT_TAG);
|
|
92
|
+
if (at < 0) return { head: id, reseed: false };
|
|
93
|
+
const head = id.slice(0, at);
|
|
94
|
+
const marker = preview.functionName ? `.${preview.functionName}` : null;
|
|
95
|
+
if (marker && !head.includes(marker)) return { head: id, reseed: false };
|
|
96
|
+
return { head, reseed: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
/** Whether a capture is an `@OverrideVariant` reseed rather than a base capture. */
|
|
72
100
|
function isVariantCapture(preview) {
|
|
73
|
-
return
|
|
101
|
+
return splitVariantTag(preview).reseed;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
/**
|
|
@@ -80,13 +108,14 @@ function isVariantCapture(preview) {
|
|
|
80
108
|
* is the `@Preview` name a multipreview gives the capture — `Light` / `Dark` for a themed pair, and
|
|
81
109
|
* EMPTY for an unnamed single capture. Splitting on the function name rather than pattern-matching
|
|
82
110
|
* the tail is what lets a dark-first catalog be recognised at all: its ids carry no mode segment to
|
|
83
|
-
* match against.
|
|
111
|
+
* match against. The reseed suffix comes off first, via [splitVariantTag] rather than a bare
|
|
112
|
+
* substring split, so a function whose own name contains `_VARIANT_` keeps its whole identity.
|
|
84
113
|
*
|
|
85
114
|
* A capture whose id does not contain its own function name is left as its own subject with an
|
|
86
115
|
* empty mode — it cannot be grouped with anything, so it selects itself.
|
|
87
116
|
*/
|
|
88
117
|
export function captureIdentity(preview) {
|
|
89
|
-
const head =
|
|
118
|
+
const { head } = splitVariantTag(preview);
|
|
90
119
|
const marker = preview.functionName ? `.${preview.functionName}` : null;
|
|
91
120
|
const at = marker ? head.lastIndexOf(marker) : -1;
|
|
92
121
|
if (at < 0) return { subject: head, mode: "" };
|
|
@@ -408,6 +437,33 @@ export function variantSeeds(preview) {
|
|
|
408
437
|
}
|
|
409
438
|
|
|
410
439
|
/** The name a variant render goes by, for a report and for the design-map `state` slot. */
|
|
440
|
+
/**
|
|
441
|
+
* How a folded variant names itself in the reference diagnostics: `<parentId> [<axis>=<value> …]`.
|
|
442
|
+
*
|
|
443
|
+
* Not the bare `componentId` — that is the PARENT's id for a VARIANT role, so reporting a variant's
|
|
444
|
+
* stated absence under it would read as a finding about a parent that may carry a perfectly good
|
|
445
|
+
* reference.
|
|
446
|
+
*
|
|
447
|
+
* Not [variantName] either, though that is what the variant is called elsewhere: it narrows to the
|
|
448
|
+
* `state` alone whenever there is one, so two variants of a parent sharing a state and differing
|
|
449
|
+
* only in `props` produce the SAME name. These labels are map keys, so a collision silently drops
|
|
450
|
+
* one of the two stated absences — losing exactly the record this diagnostic exists to keep. The
|
|
451
|
+
* full seed vector is what distinguishes them, so the label is built from that.
|
|
452
|
+
*/
|
|
453
|
+
export function variantAbsenceId(preview) {
|
|
454
|
+
const parent = preview.catalog?.componentId ?? "(unnamed)";
|
|
455
|
+
const axes = variantSeeds(preview)
|
|
456
|
+
.map((seed) => `${seed.key}=${seed.raw}`)
|
|
457
|
+
.join(" ");
|
|
458
|
+
// `state` and `props` are both optional, so a variant CAN declare `noReference` and name no axis
|
|
459
|
+
// at all. Falling back to the bare parent id then publishes "Button — <reason>" for a parent that
|
|
460
|
+
// may hold a perfectly good reference: it reads as a finding about the parent, and says nothing
|
|
461
|
+
// about which variant the reason belongs to. The function name is what distinguishes such a
|
|
462
|
+
// variant, so it stands in for the axes it did not give.
|
|
463
|
+
const label = axes || preview.functionName || captureIdentity(preview).subject;
|
|
464
|
+
return label ? `${parent} [${label}]` : parent;
|
|
465
|
+
}
|
|
466
|
+
|
|
411
467
|
function variantName(preview, seeds) {
|
|
412
468
|
const catalog = preview.catalog;
|
|
413
469
|
const cell = preview.overrides?.name;
|
|
@@ -492,8 +548,28 @@ export function variantRendersByComponent(previews, selection = selectCaptures(p
|
|
|
492
548
|
const seeds = variantSeeds(preview);
|
|
493
549
|
if (!seeds.length) continue;
|
|
494
550
|
|
|
551
|
+
// A `@CatalogVariant` may state its OWN kit correspondence, and either spelling changes what a
|
|
552
|
+
// resolver should do with this render. Without reading them here a variant's declaration is
|
|
553
|
+
// emitted under the parent's `reference` regardless — which is precisely the mispairing the two
|
|
554
|
+
// fields were added to prevent.
|
|
555
|
+
//
|
|
556
|
+
// `noReference` says the kit exports no cell this render could honestly pair with. Handing it
|
|
557
|
+
// to the resolver anyway scores it against the PARENT's picture, so it is dropped: the absence
|
|
558
|
+
// is already reported once, as a stated absence, and a render that says "there is nothing to
|
|
559
|
+
// compare me to" must not then be compared.
|
|
560
|
+
if (isCatalogVariant && catalog.noReference) continue;
|
|
561
|
+
|
|
495
562
|
const list = byComponent.get(catalog.componentId) ?? [];
|
|
496
|
-
list.push({
|
|
563
|
+
list.push({
|
|
564
|
+
previewId: preview.id,
|
|
565
|
+
name: variantName(preview, seeds),
|
|
566
|
+
seeds,
|
|
567
|
+
// `reference` names the variant's own kit cell. Carried onto the render so a resolver pairs
|
|
568
|
+
// that handle instead of deriving one from the parent's by seed. Additive to the sidecar's
|
|
569
|
+
// shape — a resolver that does not read it sees exactly what it saw before, which is why the
|
|
570
|
+
// schema string does not move.
|
|
571
|
+
...(isCatalogVariant && catalog.reference ? { reference: catalog.reference } : {}),
|
|
572
|
+
});
|
|
497
573
|
byComponent.set(catalog.componentId, list);
|
|
498
574
|
}
|
|
499
575
|
return byComponent;
|
|
@@ -529,29 +605,87 @@ export function projectDesignMap(previews, opts = {}) {
|
|
|
529
605
|
* however many captures it publishes.
|
|
530
606
|
*/
|
|
531
607
|
const unmappedIds = new Map();
|
|
608
|
+
/**
|
|
609
|
+
* Stated absences, keyed by a COLLISION-SAFE identity and carrying the display label separately.
|
|
610
|
+
*
|
|
611
|
+
* A component keys on its own id and a variant on its capture subject, each behind its own
|
|
612
|
+
* prefix. Both are free-form strings from different namespaces — a `@CatalogComponent(id = …)`
|
|
613
|
+
* may legally be spelled like a capture subject — so sharing one map without tagging the domain
|
|
614
|
+
* is the same collision one namespace over. Never keyed on the rendered label. The label is built by joining `key=value` pairs, and
|
|
615
|
+
* discovery splits an annotation prop at its FIRST `=` only, so a value may legally contain both
|
|
616
|
+
* a space and an `=`: `props = ["a=b c=d"]` is one prop, and renders identically to the two props
|
|
617
|
+
* `a=b` and `c=d`. Keying on that string would silently drop one of two distinct absences — the
|
|
618
|
+
* same data loss this diagnostic exists to prevent, one level subtler.
|
|
619
|
+
*/
|
|
532
620
|
const statedAbsentIds = new Map();
|
|
621
|
+
/** Capture subjects whose absence is stated — a variant is named by subject, not by component. */
|
|
622
|
+
const referencelessSubjects = new Set();
|
|
623
|
+
/** Component ids whose absence is stated, for the componentId-keyed ambiguity filter below. */
|
|
624
|
+
const statedAbsentComponentIds = new Set();
|
|
533
625
|
for (const preview of previews) {
|
|
534
626
|
const catalog = preview.catalog;
|
|
535
|
-
if (!catalog || catalog.
|
|
627
|
+
if (!catalog || catalog.reference) continue;
|
|
536
628
|
if (isVariantCapture(preview)) continue;
|
|
629
|
+
// A `@CatalogVariant` can now state its own kit correspondence, so its absence is reported
|
|
630
|
+
// like a component's. Scanning components alone meant folding a render under a parent silently
|
|
631
|
+
// dropped its stated absence from this accounting — a catalog could lose an audit signal by
|
|
632
|
+
// restructuring, which is exactly what `statedAbsent` exists to prevent. `--strict` counts a
|
|
633
|
+
// variant's stated absence the same as a component's: someone looked, and wrote down what they
|
|
634
|
+
// found, wherever the render sits.
|
|
635
|
+
//
|
|
636
|
+
// Reported under the variant's own label, not its parent's id (`componentId` is the PARENT for
|
|
637
|
+
// a VARIANT), or a folded variant's reason would be reported against a parent that may have a
|
|
638
|
+
// perfectly good reference of its own. The label is for reading; the KEY is the capture
|
|
639
|
+
// subject, which cannot collide — see the map's own note above.
|
|
640
|
+
if (catalog.role === "VARIANT") {
|
|
641
|
+
if (!catalog.noReference) continue; // silence under a parent is the parent's business
|
|
642
|
+
const subject = captureIdentity(preview).subject;
|
|
643
|
+
statedAbsentIds.set(`subject:${subject}`, {
|
|
644
|
+
label: variantAbsenceId(preview),
|
|
645
|
+
reason: catalog.noReference,
|
|
646
|
+
});
|
|
647
|
+
// The ambiguity filter below matches on componentId, which for a VARIANT is the PARENT's --
|
|
648
|
+
// so a variant's own stated absence could not suppress its own ambiguous-mode record, and a
|
|
649
|
+
// parent carrying a good reference left it unsuppressed. `--strict --allow-stated-absence`
|
|
650
|
+
// then failed on precisely the case that flag exists to accept. A variant render has its own
|
|
651
|
+
// capture subject, so record that instead of trying to name it by component.
|
|
652
|
+
referencelessSubjects.add(subject);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (catalog.role !== "COMPONENT") continue;
|
|
537
656
|
const id = catalog.componentId;
|
|
538
|
-
if (catalog.noReference)
|
|
539
|
-
|
|
657
|
+
if (catalog.noReference) {
|
|
658
|
+
statedAbsentIds.set(`component:${id}`, { label: id, reason: catalog.noReference });
|
|
659
|
+
statedAbsentComponentIds.add(id);
|
|
660
|
+
// Unconditional: [unmapped] below filters this set through [statedAbsentComponentIds], which
|
|
661
|
+
// is the only correct place for it — a component's stated absence may be read from a LATER
|
|
662
|
+
// capture than the one that first reports it unmapped, and a guard here can only see what has
|
|
663
|
+
// been read so far. The guard that used to stand here tested a bare `id` against a map now
|
|
664
|
+
// keyed `component:<id>` / `subject:<id>`, so for an ordinary id it never matched, and for a
|
|
665
|
+
// component legally named `component:X` it matched the wrong entry and dropped it from
|
|
666
|
+
// `unmapped` — letting `--strict` pass over a component with no reference and no reason.
|
|
667
|
+
} else unmappedIds.set(id, true);
|
|
540
668
|
}
|
|
541
669
|
/** Components carrying neither a reference nor a stated reason for its absence. */
|
|
542
|
-
const unmapped = [...unmappedIds.keys()].filter((id) => !
|
|
670
|
+
const unmapped = [...unmappedIds.keys()].filter((id) => !statedAbsentComponentIds.has(id));
|
|
543
671
|
/**
|
|
544
672
|
* Components whose reference is absent for a STATED reason. Reported apart from `unmapped`
|
|
545
673
|
* because they are the opposite situation: someone looked, and what they found is that the kit
|
|
546
674
|
* has nothing live to point at. Rolling the two together is what made a retired pattern read as
|
|
547
675
|
* neglect.
|
|
548
676
|
*/
|
|
549
|
-
const statedAbsent = [...statedAbsentIds].map((
|
|
550
|
-
componentId,
|
|
677
|
+
const statedAbsent = [...statedAbsentIds.values()].map(({ label, reason }) => ({
|
|
678
|
+
componentId: label,
|
|
551
679
|
reason,
|
|
552
680
|
}));
|
|
553
|
-
/**
|
|
554
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Every COMPONENT that reaches no reference, however its absence was spelled — the set the
|
|
683
|
+
* ambiguity filter tests `componentIds` against. A variant's key is a capture subject rather than
|
|
684
|
+
* a component id, so it is deliberately absent here and suppressed through
|
|
685
|
+
* [referencelessSubjects] instead; putting subjects in this set would only add entries no
|
|
686
|
+
* componentId can ever equal.
|
|
687
|
+
*/
|
|
688
|
+
const referencelessIds = new Set([...unmapped, ...statedAbsentComponentIds]);
|
|
555
689
|
|
|
556
690
|
for (const preview of previews) {
|
|
557
691
|
const catalog = preview.catalog;
|
|
@@ -614,7 +748,9 @@ export function projectDesignMap(previews, opts = {}) {
|
|
|
614
748
|
// absence already reported above, and under --strict it would be a second, unfixable failure
|
|
615
749
|
// for the same component.
|
|
616
750
|
ambiguousMode: selection.ambiguous.filter(
|
|
617
|
-
(a) =>
|
|
751
|
+
(a) =>
|
|
752
|
+
!referencelessSubjects.has(a.subject) &&
|
|
753
|
+
(!a.componentIds.length || a.componentIds.some((id) => !referencelessIds.has(id))),
|
|
618
754
|
),
|
|
619
755
|
variantRenders: declarations.reduce((n, d) => n + d.renders.length, 0),
|
|
620
756
|
withSet: components.filter((c) => c.refSet).length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yschimke/compose-design-map",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Project a compose-preview discovery manifest into design-parity's design-map.json, plus a sidecar of unresolved variant declarations. Dependency-free.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|