@transtyle/exporter-primeng 0.1.0-alpha.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.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * AL3: classify every slot of PrimeNG's theming surface (surface-inventory.json)
3
+ * against what this exporter actually emits — the numerator over AL3's
4
+ * denominator. Shared by the exporter (which turns it into report.json rows)
5
+ * and scripts/check-coverage-bar.mjs (which enforces completeness), so the
6
+ * two can never disagree.
7
+ *
8
+ * PrimeNG resolves `{token.path}` references at runtime through its own
9
+ * semantic tier, so "driven" has three honest shapes here, not one:
10
+ *
11
+ * driven — this exporter emits the slot itself (Button's grid, formField,
12
+ * list/navigation/overlay/content, the ramps).
13
+ * inherited — Aura's own default for the slot is a REFERENCE into a semantic
14
+ * path this exporter drives, so the component follows our theme
15
+ * without us naming it. This is real coverage, not a gap — it is
16
+ * how PrimeNG is designed to be themed.
17
+ * base — Aura's default is a literal we don't override (a padding, a
18
+ * font weight), or a reference into a semantic path we don't
19
+ * drive. The slot keeps Aura's value: honest `unsupported`,
20
+ * always with a note. Never silent — that is the AL3 bar.
21
+ */
22
+
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ export const INVENTORY = JSON.parse(
26
+ readFileSync(new URL('../surface-inventory.json', import.meta.url), 'utf8'),
27
+ );
28
+
29
+ /** Collect every leaf path of the emitted preset object (`semantic.x.y`, `components.button.…`). */
30
+ export function emittedPaths(preset) {
31
+ const out = new Set();
32
+ const walk = (obj, prefix) => {
33
+ for (const [k, v] of Object.entries(obj ?? {})) {
34
+ const path = prefix ? `${prefix}.${k}` : k;
35
+ if (v && typeof v === 'object' && !Array.isArray(v)) walk(v, path);
36
+ else out.add(path);
37
+ }
38
+ };
39
+ walk(preset.semantic, 'semantic');
40
+ walk(preset.components, 'components');
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * Does our emitted preset drive the semantic path an Aura reference points at?
46
+ * Aura writes references in ITS namespace (`{content.background}`,
47
+ * `{primary.color}`, `{form.field.padding.x}`), which is our emitted
48
+ * `semantic.*` object with dots collapsed — `form.field.x` is `formField.x`.
49
+ * Compare on a normalized, case- and separator-insensitive key so the check
50
+ * doesn't hinge on PrimeNG's cosmetic dotting.
51
+ */
52
+ const normalize = (s) => s.toLowerCase().replace(/[.\-_]/g, '');
53
+
54
+ export function drivenSemanticKeys(preset) {
55
+ const keys = new Set();
56
+ const walk = (obj, prefix) => {
57
+ for (const [k, v] of Object.entries(obj ?? {})) {
58
+ const path = prefix ? `${prefix}.${k}` : k;
59
+ keys.add(normalize(path));
60
+ if (v && typeof v === 'object' && !Array.isArray(v)) walk(v, path);
61
+ }
62
+ };
63
+ // Both the mode-invariant half and one colorScheme half (they share names).
64
+ walk(preset.semantic, '');
65
+ walk(preset.semantic?.colorScheme?.light, '');
66
+ return keys;
67
+ }
68
+
69
+ /**
70
+ * Classify one inventory slot. Returns { class, reason, note? } where `class`
71
+ * is a coverage class from validation-and-coverage.md.
72
+ */
73
+ export function classifySlot(slot, emitted, drivenKeys) {
74
+ const fullPath = slot.group === 'semantic' ? `semantic.${slot.path}` : `components.${slot.path}`;
75
+ if (emitted.has(fullPath)) {
76
+ return { class: 'native', reason: 'driven', note: 'emitted directly by this exporter' };
77
+ }
78
+ if (slot.kind === 'ref' && slot.ref) {
79
+ // Aura's default points into its semantic tier; if we drive that path the
80
+ // slot follows our theme at runtime.
81
+ const key = normalize(slot.ref);
82
+ // EXACT match only. A prefix rule ("we drive some formField.* keys, so
83
+ // {form.field.font.size} must be covered") was tried and rejected: it
84
+ // over-claimed 221 slots we demonstrably do not drive — `formField.fontSize`
85
+ // and `formField.focusRing.*` among them. Inflating coverage is the one
86
+ // outcome this bar exists to prevent. `drivenSemanticKeys` already walks
87
+ // every nested path we emit, so exact matching covers real nesting.
88
+ const driven =
89
+ drivenKeys.has(key) ||
90
+ // `{primary.500}`/`{surface.200}` address the ramps we emit wholesale.
91
+ /^(primary|surface)\d+$/.test(key);
92
+ if (driven) {
93
+ return {
94
+ class: 'derived',
95
+ reason: 'inherited',
96
+ note: `follows our theme through PrimeNG's own reference {${slot.ref}}`,
97
+ };
98
+ }
99
+ return {
100
+ class: 'unsupported',
101
+ reason: 'base',
102
+ note: `Aura default references {${slot.ref}}, a semantic path this exporter does not drive`,
103
+ };
104
+ }
105
+ return {
106
+ class: 'unsupported',
107
+ reason: 'base',
108
+ note: `Aura's own literal (${slot.value}) kept — this exporter does not override this slot`,
109
+ };
110
+ }
111
+
112
+ /** Classify the whole inventory. Returns { rows, counts }. */
113
+ export function classifySurface(inventory, preset) {
114
+ const emitted = emittedPaths(preset);
115
+ const drivenKeys = drivenSemanticKeys(preset);
116
+ const rows = inventory.slots.map((slot) => ({
117
+ slot,
118
+ ...classifySlot(slot, emitted, drivenKeys),
119
+ }));
120
+ const counts = { native: 0, derived: 0, approximated: 0, dropped: 0, unsupported: 0 };
121
+ const byReason = { driven: 0, inherited: 0, base: 0 };
122
+ for (const r of rows) {
123
+ counts[r.class]++;
124
+ byReason[r.reason]++;
125
+ }
126
+ return { rows, counts, byReason };
127
+ }
128
+
129
+ /** Compact per-family summary rows for report.json (2759 individual rows would drown it). */
130
+ export function coverageRows(inventory, preset) {
131
+ const { rows, byReason } = classifySurface(inventory, preset);
132
+ const families = new Map();
133
+ for (const r of rows) {
134
+ const f = r.slot.family;
135
+ if (!families.has(f)) families.set(f, { driven: 0, inherited: 0, base: 0, example: null });
136
+ const e = families.get(f);
137
+ e[r.reason]++;
138
+ if (r.reason === 'base' && !e.example) e.example = r.note;
139
+ }
140
+ const out = [];
141
+ for (const [family, e] of [...families.entries()].sort()) {
142
+ const total = e.driven + e.inherited + e.base;
143
+ const cls = e.driven ? 'native' : e.inherited > e.base ? 'derived' : 'unsupported';
144
+ out.push({
145
+ variable: `${family}.* (${total} slots)`,
146
+ slot:
147
+ e.driven || e.inherited
148
+ ? `${e.driven} driven · ${e.inherited} inherited via our semantic tier · ${e.base} on Aura's default`
149
+ : "Aura's own defaults",
150
+ class: cls,
151
+ note:
152
+ e.base > 0
153
+ ? `${e.base} slot(s) keep Aura's value — ${e.example}`
154
+ : 'every slot follows this theme',
155
+ });
156
+ }
157
+ out.push({
158
+ variable: 'PrimeNG surface totals',
159
+ slot: `${byReason.driven} driven · ${byReason.inherited} inherited · ${byReason.base} Aura default`,
160
+ class: 'derived',
161
+ note: `measured against surface-inventory.json (@primeuix/themes ${inventory.themesVersion}); AL3 bar: every slot classified, no silent gap`,
162
+ });
163
+ return out;
164
+ }