Skip to content

Commit 4b3f877

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(design-studio): publish grouped compact catalog
1 parent f8ada5f commit 4b3f877

6 files changed

Lines changed: 330 additions & 281 deletions

File tree

‎scripts/design-studio/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
`studio:refresh` checks generated design contracts, scans the checkout, and publishes a local manifest. Run `design:generate` after changing its source inputs. The catalog discovers public exports and finite variant values automatically; new components remain `needs-fixture` until an adapter mounts them.
44

5+
The published catalog groups exports and scanner signals into visual treatments. Each EMCN export stores its product uses once and keeps variant choices beneath it. The manifest carries diagnostic counts and short samples; the complete diagnostic lists remain in the same run's `scan/scan.json`. This keeps the Studio publication small without dropping scanner evidence.
6+
57
## Adapter coverage
68

79
`tools/design-studio/_components/component-fixtures.tsx` supplies fixed props, data, and providers. The matching switch case must render each supported export by its actual JSX name. Icons use the generic namespace adapter.

‎scripts/design-studio/refresh.mjs‎

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module'
1515
import { homedir } from 'node:os'
1616
import path from 'node:path'
1717
import { fileURLToPath } from 'node:url'
18+
import { groupComponents, groupExtras } from '../../tools/design-studio/_lib/catalog.ts'
1819
import { productScope } from '../design-conformance/control-scope.ts'
1920

2021
const toolRoot = path.dirname(fileURLToPath(import.meta.url))
@@ -262,6 +263,7 @@ function componentInventory() {
262263
fixture,
263264
states: fixtureContracts.states?.[item.name] ?? [],
264265
status: fixture ? 'ready' : 'needs-fixture',
266+
...(item.kind === 'component' ? { variants: [] } : {}),
265267
}
266268
entries.push(base)
267269
if (item.kind !== 'component') continue
@@ -289,11 +291,11 @@ function componentInventory() {
289291
: null
290292
if (variantFixture?.defaultState && !applicable(variantFixture.defaultState))
291293
variantFixture.defaultState = undefined
292-
entries.push({
293-
...base,
294+
base.variants.push({
294295
id: `component:${item.name}:${axis.name}=${value}`,
295-
name: `${item.name} · ${axis.name}: ${value}`,
296-
variant: { axis: axis.name, value, defaultValue: axis.defaultValue },
296+
axis: axis.name,
297+
value,
298+
defaultValue: axis.defaultValue,
297299
fixture: variantFixture,
298300
states: base.states.filter(applicable),
299301
status: supportsVariant ? 'ready' : 'needs-fixture',
@@ -306,9 +308,7 @@ function componentInventory() {
306308
/** The scanner already resolves product wrappers to EMCN terminal renderers. */
307309
function attachTracedUses(entries, controls) {
308310
const bySymbol = new Map(
309-
entries
310-
.filter((entry) => !entry.variant && entry.kind === 'component')
311-
.map((entry) => [entry.name, entry])
311+
entries.filter((entry) => entry.kind === 'component').map((entry) => [entry.name, entry])
312312
)
313313
for (const record of controls.records ?? []) {
314314
if (record.origin !== 'emcn-component' || !record.projection) continue
@@ -332,8 +332,6 @@ function attachTracedUses(entries, controls) {
332332
entry.usages.push(site)
333333
}
334334
}
335-
for (const entry of entries)
336-
if (entry.variant) entry.usages = bySymbol.get(entry.id.split(':')[1])?.usages ?? entry.usages
337335
}
338336

339337
const sourceContextCache = new Map()
@@ -610,9 +608,7 @@ async function main() {
610608
attachTracedUses(components.entries, controls)
611609
const extras = extraInventory(inventory.findings)
612610
for (const signal of extras.centralSignals) {
613-
const owners = components.entries.filter(
614-
(entry) => !entry.variant && entry.source.file === signal.source.file
615-
)
611+
const owners = components.entries.filter((entry) => entry.source.file === signal.source.file)
616612
if (owners.length)
617613
for (const owner of owners)
618614
(owner.signals ??= []).push({
@@ -635,18 +631,20 @@ async function main() {
635631
fixtureSources.map((file) => `${file}:${sha(readFileSync(path.join(repo, file)))}`).join('\n')
636632
)
637633
const manifest = {
638-
version: 2,
634+
version: 3,
639635
runId,
640636
identity,
641637
sourceRevision: initialSourceRevision,
642638
fixtureHash,
643-
components: components.entries,
639+
components: groupComponents(components.entries),
644640
nonvisualExports: components.nonvisual,
645-
extras: extras.entries,
641+
extras: groupExtras(extras.entries),
646642
coverageFailures: [...scannerFailures, ...components.failures],
647643
analysis: {
648-
stylingUnchecked: inventory.unchecked,
649-
controlUnchecked: controls.unchecked,
644+
stylingUncheckedCount: inventory.unchecked.length,
645+
controlUncheckedCount: controls.unchecked.length,
646+
stylingUncheckedSample: inventory.unchecked.slice(0, 20),
647+
controlUncheckedSample: controls.unchecked.slice(0, 20),
650648
limitations: inventory.limitations,
651649
},
652650
}
@@ -655,13 +653,21 @@ async function main() {
655653
file: '<product-working-tree>',
656654
reason: 'Source changed during refresh; refresh again.',
657655
})
658-
const missing = [...manifest.components, ...manifest.extras].filter(
659-
(entry) => entry.status !== 'ready'
660-
)
656+
const componentEntries = manifest.components.flatMap((treatment) => treatment.entries)
657+
const extraEntries = manifest.extras.flatMap((treatment) => treatment.entries)
658+
const missing = [
659+
...componentEntries.filter((entry) => entry.status !== 'ready'),
660+
...componentEntries
661+
.flatMap((entry) => entry.variants ?? [])
662+
.filter((entry) => entry.status !== 'ready'),
663+
...extraEntries.filter((entry) => entry.status !== 'ready'),
664+
]
661665
manifest.status = missing.length || manifest.coverageFailures.length ? 'incomplete' : 'complete'
662666
manifest.counts = {
663-
components: manifest.components.length,
664-
extras: manifest.extras.length,
667+
components:
668+
componentEntries.length +
669+
componentEntries.reduce((total, entry) => total + (entry.variants?.length ?? 0), 0),
670+
extras: extraEntries.length,
665671
missing: missing.length,
666672
}
667673
writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)

‎scripts/design-studio/refresh.test.mjs‎

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ function setFindings(scan, next) {
2525
writeFileSync(file, JSON.stringify(report))
2626
}
2727

28+
function components(manifest) {
29+
return manifest.components.flatMap((treatment) => treatment.entries)
30+
}
31+
32+
function variants(manifest) {
33+
return components(manifest).flatMap((entry) => entry.variants ?? [])
34+
}
35+
36+
function extras(manifest) {
37+
return manifest.extras.flatMap((treatment) => treatment.entries)
38+
}
39+
2840
function command(root, executable, args) {
2941
const result = spawnSync(executable, args, { cwd: root, encoding: 'utf8' })
3042
assert.equal(result.status, 0, result.stderr)
@@ -136,10 +148,10 @@ test('refresh requires a fixture mapping for each variant axis', () => {
136148
const { refresh } = guardedRefresh()
137149
const manifest = refresh()
138150
assert.ok(
139-
manifest.components.find((entry) => entry.id === 'component:Example:variant=filled').fixture
151+
variants(manifest).find((entry) => entry.id === 'component:Example:variant=filled').fixture
140152
)
141153
assert.equal(
142-
manifest.components.find((entry) => entry.id === 'component:Example:size=lg').fixture,
154+
variants(manifest).find((entry) => entry.id === 'component:Example:size=lg').fixture,
143155
null
144156
)
145157
}, 60000)
@@ -161,10 +173,10 @@ test('refresh keeps disabled variants without impossible interaction states', ()
161173
})
162174
)
163175
const manifest = refresh()
164-
const enabled = manifest.components.find(
176+
const enabled = variants(manifest).find(
165177
(entry) => entry.id === 'component:Example:disabled=false'
166178
)
167-
const disabled = manifest.components.find(
179+
const disabled = variants(manifest).find(
168180
(entry) => entry.id === 'component:Example:disabled=true'
169181
)
170182
assert.deepEqual(enabled.states, ['open', 'focus', 'disabled'])
@@ -192,8 +204,8 @@ test('refresh keeps explicitly closed variants out of open states', () => {
192204
})
193205
)
194206
const manifest = refresh()
195-
const closed = manifest.components.find((entry) => entry.id === 'component:Example:open=false')
196-
const opened = manifest.components.find((entry) => entry.id === 'component:Example:open=true')
207+
const closed = variants(manifest).find((entry) => entry.id === 'component:Example:open=false')
208+
const opened = variants(manifest).find((entry) => entry.id === 'component:Example:open=true')
197209
assert.deepEqual(closed.states, ['focus'])
198210
assert.equal(closed.fixture.defaultState, undefined)
199211
assert.deepEqual(opened.states, ['open', 'focus'])
@@ -203,10 +215,10 @@ test('refresh keeps explicitly closed variants out of open states', () => {
203215
test('refresh publishes unresolved analysis and scanner limits separately from inspection failures', () => {
204216
const { refresh } = guardedRefresh()
205217
const manifest = refresh()
206-
assert.deepEqual(manifest.analysis.stylingUnchecked, [
218+
assert.deepEqual(manifest.analysis.stylingUncheckedSample, [
207219
{ file: 'example.tsx', line: 1, reason: 'Computed styling is unchecked' },
208220
])
209-
assert.deepEqual(manifest.analysis.controlUnchecked, [
221+
assert.deepEqual(manifest.analysis.controlUncheckedSample, [
210222
{ file: 'example.tsx', line: 2, reason: 'Unknown spread' },
211223
])
212224
assert.deepEqual(manifest.analysis.limitations, ['Runtime cascade is unsupported.'])
@@ -359,22 +371,31 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
359371
}
360372

361373
const first = refresh()
374+
assert.equal(first.version, 3)
375+
assert.equal(first.counts.components, components(first).length + variants(first).length)
376+
assert.equal(first.counts.extras, extras(first).length)
377+
assert.equal(
378+
new Set([...components(first), ...variants(first), ...extras(first)].map((entry) => entry.id))
379+
.size,
380+
first.counts.components + first.counts.extras
381+
)
382+
assert.ok(variants(first).every((entry) => !('usages' in entry)))
362383
assert.equal(
363-
first.components.some((e) => e.name === 'SPACING' || e.name === 'Compound'),
384+
components(first).some((e) => e.name === 'SPACING' || e.name === 'Compound'),
364385
false
365386
)
366387
assert.equal(
367388
first.nonvisualExports.some((e) => e.name === 'SPACING'),
368389
true
369390
)
370-
assert.equal(first.components.find((e) => e.name === 'Compound.Part').fixture.id, 'example')
391+
assert.equal(components(first).find((e) => e.name === 'Compound.Part').fixture.id, 'example')
371392
assert.equal(
372-
first.components.find((e) => e.id === 'component:Compound.Part:size=lg').fixture,
393+
variants(first).find((e) => e.id === 'component:Compound.Part:size=lg').fixture,
373394
null
374395
)
375396

376397
assert.equal(
377-
first.components
398+
components(first)
378399
.find((e) => e.name === 'Compound.Part')
379400
.usages.filter((u) => u.relationship === 'direct').length,
380401
1
@@ -393,7 +414,10 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
393414
assert.notEqual(changedClassMerger.sourceRevision, first.sourceRevision)
394415
write(repo, 'packages/emcn/src/lib/cn.ts', '')
395416
assert.deepEqual(
396-
first.components.map((entry) => entry.id),
417+
components(first).flatMap((entry) => [
418+
entry.id,
419+
...(entry.variants ?? []).map((variant) => variant.id),
420+
]),
397421
[
398422
'component:Compound.Part',
399423
'component:Compound.Part:size=lg',
@@ -405,20 +429,20 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
405429
]
406430
)
407431
assert.deepEqual(
408-
first.components
432+
components(first)
409433
.find((entry) => entry.name === 'Example')
410434
.usages.map((site) => site.relationship),
411435
['direct', 'via Wrapper']
412436
)
413-
assert.equal(first.extras.length, 2)
437+
assert.equal(extras(first).length, 2)
414438
assert.deepEqual(
415-
first.extras.map((entry) => entry.id),
439+
extras(first).map((entry) => entry.id),
416440
['finding:finding-one', 'finding:advisory-one']
417441
)
418-
assert.equal(first.extras[0].fixture.type, 'sample')
419-
assert.equal(first.extras[0].fixture.sample.kind, 'surface')
420-
assert.equal(first.extras[0].fixture.sample.tag, 'Example')
421-
assert.equal(first.extras[0].previewKind, 'source-style-sample')
442+
assert.equal(extras(first)[0].fixture.type, 'sample')
443+
assert.equal(extras(first)[0].fixture.sample.kind, 'surface')
444+
assert.equal(extras(first)[0].fixture.sample.tag, 'Example')
445+
assert.equal(extras(first)[0].previewKind, 'source-style-sample')
422446

423447
write(
424448
repo,
@@ -431,7 +455,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
431455
"export * from './components'\nexport * from './icons'\nexport { RootOnly } from './components/root-only'\n"
432456
)
433457
assert.equal(
434-
refresh().components.find((entry) => entry.id === 'component:RootOnly')?.status,
458+
components(refresh()).find((entry) => entry.id === 'component:RootOnly')?.status,
435459
'needs-fixture'
436460
)
437461
write(
@@ -448,8 +472,14 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
448472
"export { Example, SPACING, Compound } from './example/example'\nexport * from './cycle-a'\n"
449473
)
450474
assert.deepEqual(
451-
refresh().components.map((entry) => entry.id),
452-
first.components.map((entry) => entry.id)
475+
components(refresh()).flatMap((entry) => [
476+
entry.id,
477+
...(entry.variants ?? []).map((variant) => variant.id),
478+
]),
479+
components(first).flatMap((entry) => [
480+
entry.id,
481+
...(entry.variants ?? []).map((variant) => variant.id),
482+
])
453483
)
454484

455485
write(
@@ -459,7 +489,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
459489
)
460490
const changed = refresh()
461491
assert.notEqual(changed.sourceRevision, first.sourceRevision)
462-
assert.equal(changed.components.find((entry) => entry.name === 'Example').usages.length, 3)
492+
assert.equal(components(changed).find((entry) => entry.name === 'Example').usages.length, 3)
463493

464494
write(
465495
repo,
@@ -473,7 +503,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
473503
)
474504
const exported = refresh()
475505
assert.equal(
476-
exported.components.find((entry) => entry.id === 'component:NewControl')?.status,
506+
components(exported).find((entry) => entry.id === 'component:NewControl')?.status,
477507
'needs-fixture'
478508
)
479509

@@ -498,11 +528,11 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
498528
},
499529
])
500530
const central = refresh()
501-
assert.equal(central.extras.length, 4)
502-
assert.equal(central.extras[2].id, 'finding:central-one')
503-
assert.equal(central.extras[3].id, 'finding:unmatched-central')
531+
assert.equal(extras(central).length, 4)
532+
assert.equal(extras(central)[2].id, 'finding:central-one')
533+
assert.equal(extras(central)[3].id, 'finding:unmatched-central')
504534
assert.deepEqual(
505-
central.components
535+
components(central)
506536
.find((entry) => entry.id === 'component:Example')
507537
?.signals?.map((signal) => signal.id),
508538
['finding:central-one']
@@ -533,7 +563,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
533563
]
534564
setFindings(scan, [...findings(scan), ...newSignals])
535565
const discovered = refresh()
536-
const newEntries = discovered.extras.filter((entry) =>
566+
const newEntries = extras(discovered).filter((entry) =>
537567
entry.source.file.endsWith('/new-action.tsx')
538568
)
539569
assert.equal(newEntries.length, 2)
@@ -554,7 +584,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
554584
value: 'WorkflowIcon',
555585
},
556586
])
557-
const movedIcon = refresh().extras.find((entry) => entry.id === 'finding:moved-icon')
587+
const movedIcon = extras(refresh()).find((entry) => entry.id === 'finding:moved-icon')
558588
assert.deepEqual(movedIcon.fixture, { type: 'extra', id: 'workflowIcon' })
559589

560590
const richCss =
@@ -612,7 +642,7 @@ test('refresh catalogs every detected treatment and new EMCN export', () => {
612642
value: 'bg-black/10',
613643
},
614644
])
615-
const semantic = new Map(refresh().extras.map((entry) => [entry.id, entry]))
645+
const semantic = new Map(extras(refresh()).map((entry) => [entry.id, entry]))
616646
assert.equal(semantic.get('finding:code-moved').fixture.id, 'rich-code')
617647
assert.equal(semantic.get('finding:selection-moved').fixture.id, 'rich-selection')
618648
assert.equal(semantic.get('finding:other-css').fixture.type, 'sample')

0 commit comments

Comments
 (0)