@awarebydefault/display-case 1.0.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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/display-case.prompt.md +64 -0
  4. package/docs/ai-agents.md +126 -0
  5. package/docs/cli.md +99 -0
  6. package/docs/configuration.md +410 -0
  7. package/docs/documentation-panel.md +50 -0
  8. package/docs/examples/README.md +14 -0
  9. package/docs/examples/multi-variant.case.tsx +30 -0
  10. package/docs/examples/plain.case.tsx +22 -0
  11. package/docs/examples/tweak-control.placard.md +80 -0
  12. package/docs/examples/tweaks.case.tsx +39 -0
  13. package/docs/hierarchy.md +59 -0
  14. package/docs/quick-start.md +78 -0
  15. package/docs/style-engines.md +180 -0
  16. package/docs/testing.md +245 -0
  17. package/docs/theming.md +97 -0
  18. package/docs/tweaks.md +75 -0
  19. package/docs/writing-cases.md +144 -0
  20. package/docs/writing-placard-docs.md +194 -0
  21. package/package.json +113 -0
  22. package/skills/display-case-author-case/README.md +20 -0
  23. package/skills/display-case-author-case/SKILL.md +40 -0
  24. package/skills/display-case-author-placard-doc/README.md +24 -0
  25. package/skills/display-case-author-placard-doc/SKILL.md +65 -0
  26. package/skills/display-case-review/README.md +19 -0
  27. package/skills/display-case-review/SKILL.md +30 -0
  28. package/skills/display-case-snapshot/README.md +20 -0
  29. package/skills/display-case-snapshot/SKILL.md +29 -0
  30. package/src/checks/a11y-scanner.test.ts +240 -0
  31. package/src/checks/a11y-scanner.ts +410 -0
  32. package/src/checks/check-text.test.ts +53 -0
  33. package/src/checks/check-text.ts +78 -0
  34. package/src/checks/check.test.ts +194 -0
  35. package/src/checks/check.ts +473 -0
  36. package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
  37. package/src/checks/providers/pixelmatch-diff.ts +30 -0
  38. package/src/checks/providers/playwright-driver.ts +104 -0
  39. package/src/checks/ssr-check.test.ts +73 -0
  40. package/src/checks/ssr-check.ts +96 -0
  41. package/src/checks/structure-check.cross-package.test.ts +165 -0
  42. package/src/checks/structure-check.test.ts +651 -0
  43. package/src/checks/structure-check.ts +988 -0
  44. package/src/checks/tokens-check.test.ts +159 -0
  45. package/src/checks/tokens-check.ts +162 -0
  46. package/src/cli.ts +218 -0
  47. package/src/commands/agents.test.ts +24 -0
  48. package/src/commands/agents.ts +28 -0
  49. package/src/commands/init-run.test.ts +123 -0
  50. package/src/commands/init.test.ts +63 -0
  51. package/src/commands/init.ts +412 -0
  52. package/src/commands/publish.test.ts +210 -0
  53. package/src/commands/publish.ts +292 -0
  54. package/src/core/affected.test.ts +99 -0
  55. package/src/core/affected.ts +144 -0
  56. package/src/core/catalog.test.ts +152 -0
  57. package/src/core/catalog.ts +92 -0
  58. package/src/core/discovery.test.ts +184 -0
  59. package/src/core/discovery.ts +250 -0
  60. package/src/core/manifest.ts +41 -0
  61. package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
  62. package/src/core/mdx-lite/index.ts +393 -0
  63. package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
  64. package/src/core/mdx-plugin.test.ts +60 -0
  65. package/src/core/mdx-plugin.ts +30 -0
  66. package/src/flow-step.test-d.ts +39 -0
  67. package/src/index.test.ts +100 -0
  68. package/src/index.ts +564 -0
  69. package/src/render/collect-styles.emotion.test.tsx +114 -0
  70. package/src/render/collect-styles.test.tsx +72 -0
  71. package/src/render/collect-styles.ts +33 -0
  72. package/src/render/documents.test.ts +184 -0
  73. package/src/render/documents.ts +88 -0
  74. package/src/render/render-node.test.tsx +160 -0
  75. package/src/render/render-node.tsx +133 -0
  76. package/src/render/ssr-primer.test.tsx +25 -0
  77. package/src/render/ssr-primer.tsx +54 -0
  78. package/src/render/ssr-render.test.tsx +142 -0
  79. package/src/render/ssr-render.tsx +63 -0
  80. package/src/render/ssr-shell.test.tsx +57 -0
  81. package/src/render/ssr-shell.tsx +54 -0
  82. package/src/server/prod-server.ts +237 -0
  83. package/src/server/server.test.ts +117 -0
  84. package/src/server/server.ts +1039 -0
  85. package/src/style-engine.test-d.ts +37 -0
  86. package/src/testing/test-helpers.ts +27 -0
  87. package/src/types/pixelmatch.d.ts +12 -0
  88. package/src/ui/browser-entry.tsx +51 -0
  89. package/src/ui/chrome.css +485 -0
  90. package/src/ui/design-system/README.md +88 -0
  91. package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
  92. package/src/ui/design-system/components/controls/Button.css +89 -0
  93. package/src/ui/design-system/components/controls/Button.placard.md +14 -0
  94. package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
  95. package/src/ui/design-system/components/controls/Button.tsx +41 -0
  96. package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
  97. package/src/ui/design-system/components/controls/IconButton.css +67 -0
  98. package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
  99. package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
  100. package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
  101. package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
  102. package/src/ui/design-system/components/controls/Input.css +52 -0
  103. package/src/ui/design-system/components/controls/Input.placard.md +12 -0
  104. package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
  105. package/src/ui/design-system/components/controls/Input.tsx +45 -0
  106. package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
  107. package/src/ui/design-system/components/controls/Select.css +44 -0
  108. package/src/ui/design-system/components/controls/Select.placard.md +15 -0
  109. package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
  110. package/src/ui/design-system/components/controls/Select.tsx +58 -0
  111. package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
  112. package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
  113. package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
  114. package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
  115. package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
  116. package/src/ui/design-system/components/index.ts +66 -0
  117. package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
  118. package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
  119. package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
  120. package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
  121. package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
  122. package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
  123. package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
  124. package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
  125. package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
  126. package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
  127. package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
  128. package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
  129. package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
  130. package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
  131. package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
  132. package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
  133. package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
  134. package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
  135. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
  136. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
  137. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
  138. package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
  139. package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
  140. package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
  141. package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
  142. package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
  143. package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
  144. package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
  145. package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
  146. package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
  147. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
  148. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
  149. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
  150. package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
  151. package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
  152. package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
  153. package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
  154. package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
  155. package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
  156. package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
  157. package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
  158. package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
  159. package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
  160. package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
  161. package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
  162. package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
  163. package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
  164. package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
  165. package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
  166. package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
  167. package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
  168. package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
  169. package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
  170. package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
  171. package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
  172. package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
  173. package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
  174. package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
  175. package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
  176. package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
  177. package/src/ui/design-system/components/showcase/Chip.css +51 -0
  178. package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
  179. package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
  180. package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
  181. package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
  182. package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
  183. package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
  184. package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
  185. package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
  186. package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
  187. package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
  188. package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
  189. package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
  190. package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
  191. package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
  192. package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
  193. package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
  194. package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
  195. package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
  196. package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
  197. package/src/ui/design-system/components/showcase/NavItem.css +111 -0
  198. package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
  199. package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
  200. package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
  201. package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
  202. package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
  203. package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
  204. package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
  205. package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
  206. package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
  207. package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
  208. package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
  209. package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
  210. package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
  211. package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
  212. package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
  213. package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
  214. package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
  215. package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
  216. package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
  217. package/src/ui/design-system/components/showcase/Stage.css +91 -0
  218. package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
  219. package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
  220. package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
  221. package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
  222. package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
  223. package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
  224. package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
  225. package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
  226. package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
  227. package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
  228. package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
  229. package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
  230. package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
  231. package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
  232. package/src/ui/design-system/primer-specimens/components.tsx +308 -0
  233. package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
  234. package/src/ui/design-system/primer-specimens/index.ts +25 -0
  235. package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
  236. package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
  237. package/src/ui/design-system/primer-specimens/type.tsx +75 -0
  238. package/src/ui/design-system/primer.mdx +236 -0
  239. package/src/ui/design-system/styles.css +14 -0
  240. package/src/ui/design-system/tokens/colors.css +172 -0
  241. package/src/ui/design-system/tokens/fonts.css +18 -0
  242. package/src/ui/design-system/tokens/spacing.css +48 -0
  243. package/src/ui/design-system/tokens/typography.css +49 -0
  244. package/src/ui/markdown.test.tsx +54 -0
  245. package/src/ui/markdown.tsx +19 -0
  246. package/src/ui/primer-mount.tsx +76 -0
  247. package/src/ui/primer.css +175 -0
  248. package/src/ui/primer.tsx +277 -0
  249. package/src/ui/render-mount.tsx +284 -0
  250. package/src/ui/shell-core.test.ts +340 -0
  251. package/src/ui/shell-core.ts +295 -0
  252. package/src/ui/shell.tsx +60 -0
  253. package/src/ui/test-ids.ts +53 -0
  254. package/src/ui/use-shell.ts +1230 -0
@@ -0,0 +1,250 @@
1
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
2
+ import { Glob } from 'bun'
3
+ import type { CaseModule, DisplayCaseConfig } from '../index'
4
+
5
+ export interface LoadedModule {
6
+ /** Absolute path to the case file. */
7
+ file: string
8
+ module: CaseModule
9
+ }
10
+
11
+ export interface LoadError {
12
+ file: string
13
+ error: string
14
+ }
15
+
16
+ const CONFIG_NAMES = ['display-case.config.ts', 'display-case.config.tsx']
17
+
18
+ /** Resolve and import a consumer's Display Case config from a package dir. */
19
+ export async function resolveConfig(
20
+ pkgDir: string,
21
+ ): Promise<{ config: DisplayCaseConfig; configPath: string }> {
22
+ for (const name of CONFIG_NAMES) {
23
+ const candidate = join(pkgDir, name)
24
+ if (await Bun.file(candidate).exists()) {
25
+ const mod = (await import(candidate)) as { default?: DisplayCaseConfig }
26
+ if (!mod.default) {
27
+ throw new Error(`${name} must default-export defineConfig(...)`)
28
+ }
29
+ return { config: mod.default, configPath: candidate }
30
+ }
31
+ }
32
+ throw new Error(
33
+ `No Display Case config found in ${pkgDir} (expected one of: ${CONFIG_NAMES.join(', ')})`,
34
+ )
35
+ }
36
+
37
+ /** Resolve the configured roots globs to absolute case-file paths. */
38
+ export async function discoverCaseFiles(
39
+ pkgDir: string,
40
+ config: DisplayCaseConfig,
41
+ ): Promise<string[]> {
42
+ const found = new Set<string>()
43
+ for (const pattern of config.roots) {
44
+ const glob = new Glob(pattern)
45
+ for await (const match of glob.scan({ cwd: pkgDir, absolute: true })) {
46
+ if (!match.includes('/node_modules/')) found.add(match)
47
+ }
48
+ }
49
+ return [...found].sort()
50
+ }
51
+
52
+ /**
53
+ * Import each case file's default export. A file that fails to load (throws on
54
+ * import, or has no valid default) is collected as an error and skipped so the
55
+ * rest still load.
56
+ *
57
+ * Note: Bun caches ES modules by resolved path and ignores `?v=`-style query
58
+ * busting, so re-importing an edited file in the same process returns the stale
59
+ * module. The long-lived dev server therefore rebuilds its manifest in a fresh
60
+ * subprocess (see `loadManifestFresh` in server.ts); the one-shot callers here
61
+ * (`--print-manifest`, `check`) each run in their own process, so a bare import
62
+ * is always current for them.
63
+ */
64
+ export async function loadModules(
65
+ files: string[],
66
+ ): Promise<{ modules: LoadedModule[]; errors: LoadError[] }> {
67
+ const modules: LoadedModule[] = []
68
+ const errors: LoadError[] = []
69
+ for (const file of files) {
70
+ try {
71
+ const mod = (await import(file)) as { default?: CaseModule }
72
+ if (!mod.default || typeof mod.default.component !== 'string') {
73
+ errors.push({
74
+ file,
75
+ error: 'no valid default export (use defineCases/defineFlow)',
76
+ })
77
+ continue
78
+ }
79
+ modules.push({ file, module: mod.default })
80
+ } catch (err) {
81
+ errors.push({
82
+ file,
83
+ error: err instanceof Error ? err.message : String(err),
84
+ })
85
+ }
86
+ }
87
+ return { modules, errors }
88
+ }
89
+
90
+ /** Absolute path of the gitignored cache dir for a consumer package. */
91
+ export function cacheDir(pkgDir: string): string {
92
+ return join(pkgDir, '.display-case')
93
+ }
94
+
95
+ /** Resolve the baseline directory (config override or the default cache). */
96
+ export function baselineDir(pkgDir: string, config: DisplayCaseConfig): string {
97
+ if (config.baselineDir) {
98
+ return isAbsolute(config.baselineDir)
99
+ ? config.baselineDir
100
+ : join(pkgDir, config.baselineDir)
101
+ }
102
+ return join(cacheDir(pkgDir), 'baselines')
103
+ }
104
+
105
+ function importPath(from: string, to: string): string {
106
+ let rel = relative(dirname(from), to)
107
+ if (!rel.startsWith('.')) rel = `./${rel}`
108
+ return rel
109
+ }
110
+
111
+ /**
112
+ * Codegen the render entry: a static module that imports every discovered case
113
+ * file plus the consumer config, then hands them to the render mount. Bun's
114
+ * bundler needs static entrypoints, so we generate this file rather than using
115
+ * a runtime glob.
116
+ */
117
+ export async function codegenRenderEntry(
118
+ pkgDir: string,
119
+ files: string[],
120
+ configPath: string,
121
+ ): Promise<string> {
122
+ const dir = cacheDir(pkgDir)
123
+ const entry = join(dir, 'render-entry.tsx')
124
+ const here = resolve(import.meta.dir, '..')
125
+ const mountImport = importPath(entry, join(here, 'ui', 'render-mount.tsx'))
126
+ const configImport = importPath(entry, configPath)
127
+
128
+ const imports = files
129
+ .map((f, i) => `import m${i} from '${importPath(entry, f)}'`)
130
+ .join('\n')
131
+ // Tag each module with its source path (relative to the package) so a
132
+ // level-aware decorator can pick per-area app chrome from it.
133
+ const list = files
134
+ .map(
135
+ (f, i) =>
136
+ `Object.assign(m${i}, { sourcePath: ${JSON.stringify(relative(pkgDir, f))} })`,
137
+ )
138
+ .join(', ')
139
+
140
+ const source = `// AUTO-GENERATED by display-case — do not edit.
141
+ import { mountRender } from '${mountImport}'
142
+ import config from '${configImport}'
143
+ ${imports}
144
+
145
+ mountRender([${list}], config)
146
+ `
147
+ await Bun.write(entry, source)
148
+ return entry
149
+ }
150
+
151
+ /**
152
+ * Codegen the SSR entry: a static module that imports every discovered case
153
+ * file plus the consumer config and exports `renderCaseToHtml` — the server's
154
+ * pre-render function. The shape mirrors {@link codegenRenderEntry} (same import
155
+ * list, same `sourcePath` tagging so a decorator behaves identically), but it
156
+ * runs under Bun (not the browser) and returns markup instead of mounting.
157
+ */
158
+ export async function codegenSsrEntry(
159
+ pkgDir: string,
160
+ files: string[],
161
+ configPath: string,
162
+ ): Promise<string> {
163
+ const dir = cacheDir(pkgDir)
164
+ const entry = join(dir, 'ssr-entry.tsx')
165
+ const here = resolve(import.meta.dir, '..')
166
+ const rendererImport = importPath(
167
+ entry,
168
+ join(here, 'render', 'ssr-render.tsx'),
169
+ )
170
+ const configImport = importPath(entry, configPath)
171
+
172
+ const imports = files
173
+ .map((f, i) => `import m${i} from '${importPath(entry, f)}'`)
174
+ .join('\n')
175
+ const list = files
176
+ .map(
177
+ (f, i) =>
178
+ `Object.assign(m${i}, { sourcePath: ${JSON.stringify(relative(pkgDir, f))} })`,
179
+ )
180
+ .join(', ')
181
+
182
+ const source = `// AUTO-GENERATED by display-case — do not edit.
183
+ import { makeCaseRenderer } from '${rendererImport}'
184
+ import config from '${configImport}'
185
+ ${imports}
186
+
187
+ export const renderCaseToHtml = makeCaseRenderer([${list}], config)
188
+ `
189
+ await Bun.write(entry, source)
190
+ return entry
191
+ }
192
+
193
+ /**
194
+ * Codegen the primer entry: a static module that imports the consumer's `.mdx`
195
+ * document (compiled by the MDX bundler plugin) and hands it to the primer
196
+ * mount. `primerPath` is the config's `primer` value, relative to the package.
197
+ */
198
+ export async function codegenPrimerEntry(
199
+ pkgDir: string,
200
+ primerPath: string,
201
+ ): Promise<string> {
202
+ const dir = cacheDir(pkgDir)
203
+ const entry = join(dir, 'primer-entry.tsx')
204
+ const here = resolve(import.meta.dir, '..')
205
+ const mountImport = importPath(entry, join(here, 'ui', 'primer-mount.tsx'))
206
+ const mdxImport = importPath(entry, resolve(pkgDir, primerPath))
207
+
208
+ const source = `// AUTO-GENERATED by display-case — do not edit.
209
+ import { mountPrimer } from '${mountImport}'
210
+ import MDXContent from '${mdxImport}'
211
+
212
+ mountPrimer(MDXContent)
213
+ `
214
+ await Bun.write(entry, source)
215
+ return entry
216
+ }
217
+
218
+ /**
219
+ * Codegen the SSR-primer entry: imports the compiled MDX and exports
220
+ * `renderPrimerToHtml` — the server's primer pre-render function. The browser
221
+ * counterpart of this is {@link codegenPrimerEntry}; this one runs under Bun
222
+ * and returns markup instead of mounting.
223
+ */
224
+ export async function codegenSsrPrimerEntry(
225
+ pkgDir: string,
226
+ primerPath: string,
227
+ configPath: string,
228
+ ): Promise<string> {
229
+ const dir = cacheDir(pkgDir)
230
+ const entry = join(dir, 'ssr-primer-entry.tsx')
231
+ const here = resolve(import.meta.dir, '..')
232
+ const rendererImport = importPath(
233
+ entry,
234
+ join(here, 'render', 'ssr-primer.tsx'),
235
+ )
236
+ const mdxImport = importPath(entry, resolve(pkgDir, primerPath))
237
+ const configImport = importPath(entry, configPath)
238
+
239
+ // Pass `config` so the primer render honors `styleEngines` exactly as a case
240
+ // render does (its specimens are real consumer components).
241
+ const source = `// AUTO-GENERATED by display-case — do not edit.
242
+ import { makePrimerRenderer } from '${rendererImport}'
243
+ import config from '${configImport}'
244
+ import MDXContent from '${mdxImport}'
245
+
246
+ export const renderPrimerToHtml = makePrimerRenderer(MDXContent, config)
247
+ `
248
+ await Bun.write(entry, source)
249
+ return entry
250
+ }
@@ -0,0 +1,41 @@
1
+ import type { HierarchyLevel, TweakSchema } from '../index'
2
+
3
+ /** Type-only manifest contract shared by the server (builder) and the shell. */
4
+
5
+ export interface ManifestCase {
6
+ id: string
7
+ name: string
8
+ /** In-app browse address, e.g. /c/button/default. */
9
+ browseUrl: string
10
+ /** Isolated render address, e.g. /render/button/default. */
11
+ renderUrl: string
12
+ /** Declared tweak schema, or null when the case takes no tweaks. */
13
+ tweaks: TweakSchema | null
14
+ /** Slugified ids of steps this step can transition to (flows only; else []). */
15
+ transitions: string[]
16
+ }
17
+
18
+ export interface ManifestComponent {
19
+ id: string
20
+ name: string
21
+ level: HierarchyLevel | null
22
+ isFlow: boolean
23
+ /** Repo-relative path to the case file. */
24
+ caseFile: string
25
+ /** Repo-relative path to the authored usage doc, or null. */
26
+ placardDoc: string | null
27
+ /** For a flow these are its ordered, transitionable steps. */
28
+ cases: ManifestCase[]
29
+ }
30
+
31
+ export interface Manifest {
32
+ title: string
33
+ components: ManifestComponent[]
34
+ /** True when a Primer (`.mdx` reading page) is configured and present. The
35
+ * chrome shows the Primer / Cases mode switch only then. */
36
+ primer: boolean
37
+ /** The view the chrome lands on at `/`: `'primer'` only when a Primer is
38
+ * configured and the config didn't override the landing to `'cases'`; else
39
+ * `'library'`. A deep-linked case always opens the library. */
40
+ landing: 'primer' | 'library'
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ /** A trivial imported component used by the synthetic SSR round-trip test, to
4
+ * prove that mdx-lite imports resolve like any other TypeScript import. */
5
+ export function Box({ children }: { children?: ReactNode }) {
6
+ return <span data-box="">{children}</span>
7
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * mdx-lite — a deliberately tiny, dependency-free compiler for a *constrained*
3
+ * dialect of MDX, targeting the one thing Display Case's Primer actually needs:
4
+ * Markdown prose interleaved with **block-level** JSX specimens, plus real ES
5
+ * `import` statements that resolve like any other TypeScript module.
6
+ *
7
+ * It is NOT a general MDX implementation. It does not parse the combined
8
+ * Markdown+JSX grammar that `@mdx-js/mdx` does. Instead it *segments* a document
9
+ * into three block kinds and emits a `.tsx` module, then hands the hard parts
10
+ * back to the toolchain that already exists:
11
+ *
12
+ * - **imports** → passed through verbatim; the bundler (Bun) resolves them.
13
+ * - **JSX blocks** → passed through verbatim; the TSX compiler handles JSX and
14
+ * expression props (e.g. `style={{…}}`) for free — the exact features a
15
+ * runtime Markdown renderer cannot do.
16
+ * - **markdown runs** → emitted as `<Markdown>{"…"}</Markdown>` using a single
17
+ * runtime Markdown component (markdown-to-jsx), the same renderer the doc
18
+ * placards use.
19
+ *
20
+ * The compiled default export is `MDXContent({ components })`, matching the MDX
21
+ * calling convention Display Case's primer mount already uses: capitalized tags
22
+ * the document does not import (notably `<Display>`) resolve from `components`,
23
+ * and Markdown headings route to `components.h1` / `components.h2`.
24
+ *
25
+ * Self-contained on purpose (no imports from the rest of the repo) so it can be
26
+ * lifted into its own package later if it proves useful in isolation.
27
+ *
28
+ * ## The supported dialect (everything else is out of scope and should be
29
+ * rejected by callers such as the structure check)
30
+ *
31
+ * - `import`/`export` statements at column 0 (single- or multi-line).
32
+ * - CommonMark + GFM prose, as supported by markdown-to-jsx. Raw HTML is NOT
33
+ * rendered (`disableParsingRawHTML`).
34
+ * - **Block-level** JSX only: an element that begins a line at column 0 with
35
+ * `<Capitalized…` or a `<>` fragment, consumed to its matching close.
36
+ * - Fenced code blocks are prose, never JSX — even when they contain `<Tag>`.
37
+ *
38
+ * Unsupported (by construction): inline JSX inside a prose paragraph, Markdown
39
+ * syntax inside JSX children (passed through as literal JSX), and `{expression}`
40
+ * interpolation in prose.
41
+ */
42
+
43
+ export type MdxBlock =
44
+ | { kind: 'imports'; code: string }
45
+ | { kind: 'markdown'; text: string }
46
+ | { kind: 'jsx'; code: string; tags: string[] }
47
+
48
+ export interface MdxToTsxOptions {
49
+ /** Import specifier for the runtime Markdown component. Default markdown-to-jsx. */
50
+ markdownSpecifier?: string
51
+ }
52
+
53
+ // ----------------------------------------------------------------------------
54
+ // Low-level character scanners. Each takes the full source and a start index,
55
+ // and returns the index immediately AFTER the construct it consumed.
56
+ // ----------------------------------------------------------------------------
57
+
58
+ function isWs(ch: string): boolean {
59
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r'
60
+ }
61
+
62
+ function skipWs(s: string, from: number): number {
63
+ let p = from
64
+ while (p < s.length && isWs(s[p] as string)) p++
65
+ return p
66
+ }
67
+
68
+ /** Scan a string literal. `s[from]` is the opening quote (', ", or `). Handles
69
+ * escapes and — for templates — nested `${ … }` interpolation. */
70
+ export function scanString(s: string, from: number): number {
71
+ const quote = s[from]
72
+ let p = from + 1
73
+ while (p < s.length) {
74
+ const c = s[p]
75
+ if (c === '\\') {
76
+ p += 2
77
+ continue
78
+ }
79
+ if (quote === '`' && c === '$' && s[p + 1] === '{') {
80
+ p = scanBraces(s, p + 1)
81
+ continue
82
+ }
83
+ if (c === quote) return p + 1
84
+ p++
85
+ }
86
+ throw new Error('mdx-lite: unterminated string literal')
87
+ }
88
+
89
+ /** Scan a brace group. `s[from]` is `{`. Returns the index after the matching
90
+ * `}`. Respects nested braces, string/template literals, and `//` + block
91
+ * comments — so `style={{ a: '}' }}`, `{/* } *​/}` and friends are handled. */
92
+ export function scanBraces(s: string, from: number): number {
93
+ let p = from + 1 // past the opening {
94
+ while (p < s.length) {
95
+ const c = s[p] as string
96
+ if (c === '{') {
97
+ p = scanBraces(s, p)
98
+ continue
99
+ }
100
+ if (c === '}') return p + 1
101
+ if (c === '"' || c === "'" || c === '`') {
102
+ p = scanString(s, p)
103
+ continue
104
+ }
105
+ if (c === '/' && s[p + 1] === '/') {
106
+ const nl = s.indexOf('\n', p)
107
+ p = nl === -1 ? s.length : nl
108
+ continue
109
+ }
110
+ if (c === '/' && s[p + 1] === '*') {
111
+ const end = s.indexOf('*/', p + 2)
112
+ p = end === -1 ? s.length : end + 2
113
+ continue
114
+ }
115
+ p++
116
+ }
117
+ throw new Error('mdx-lite: unterminated braces')
118
+ }
119
+
120
+ function readName(s: string, from: number): { name: string; next: number } {
121
+ let p = from
122
+ while (p < s.length && /[\w$.-]/.test(s[p] as string)) p++
123
+ return { name: s.slice(from, p), next: p }
124
+ }
125
+
126
+ /** Scan one JSX element. `s[from]` is `<`. Returns the index after the element's
127
+ * close. Collects every element/component tag name into `tags`. */
128
+ export function scanElement(s: string, from: number, tags: string[]): number {
129
+ let p = skipWs(s, from + 1)
130
+ // Fragment <> … </>
131
+ if (s[p] === '>') return scanChildren(s, p + 1, tags)
132
+ const { name, next } = readName(s, p)
133
+ tags.push(name)
134
+ p = next
135
+ // Attributes
136
+ while (p < s.length) {
137
+ p = skipWs(s, p)
138
+ if (s[p] === '/' && s[p + 1] === '>') return p + 2
139
+ if (s[p] === '>') {
140
+ p++
141
+ break
142
+ }
143
+ if (s[p] === '{') {
144
+ // spread attribute {...x}
145
+ p = scanBraces(s, p)
146
+ continue
147
+ }
148
+ // attribute name
149
+ while (p < s.length && !/[\s=/>]/.test(s[p] as string)) p++
150
+ p = skipWs(s, p)
151
+ if (s[p] === '=') {
152
+ p = skipWs(s, p + 1)
153
+ if (s[p] === '{') p = scanBraces(s, p)
154
+ else if (s[p] === '"' || s[p] === "'") p = scanString(s, p)
155
+ else while (p < s.length && !/[\s>]/.test(s[p] as string)) p++
156
+ }
157
+ }
158
+ return scanChildren(s, p, tags)
159
+ }
160
+
161
+ /** Scan element children starting after the open tag's `>`. Returns the index
162
+ * after the matching close tag (`</…>` or `</>`). */
163
+ function scanChildren(s: string, from: number, tags: string[]): number {
164
+ let p = from
165
+ while (p < s.length) {
166
+ const c = s[p]
167
+ if (c === '<') {
168
+ if (s[p + 1] === '/') {
169
+ // closing tag — consume through '>'
170
+ p += 2
171
+ while (p < s.length && s[p] !== '>') p++
172
+ return p + 1
173
+ }
174
+ p = scanElement(s, p, tags)
175
+ continue
176
+ }
177
+ if (c === '{') {
178
+ p = scanBraces(s, p)
179
+ continue
180
+ }
181
+ p++ // text node
182
+ }
183
+ throw new Error('mdx-lite: unterminated JSX element')
184
+ }
185
+
186
+ // ----------------------------------------------------------------------------
187
+ // Segmentation
188
+ // ----------------------------------------------------------------------------
189
+
190
+ const FENCE = /^\s*(```|~~~)/
191
+ const IMPORT_EXPORT = /^(import|export)\b/
192
+ const JSX_BLOCK_START = /^<([A-Z]|>)/
193
+
194
+ /** Count `{` minus `}` outside strings/comments — used to tell whether a
195
+ * (possibly multi-line) import/export statement is complete. */
196
+ function braceBalance(text: string): number {
197
+ let depth = 0
198
+ let i = 0
199
+ while (i < text.length) {
200
+ const c = text[i] as string
201
+ if (c === '"' || c === "'" || c === '`') {
202
+ i = scanString(text, i)
203
+ continue
204
+ }
205
+ if (c === '/' && text[i + 1] === '/') {
206
+ const nl = text.indexOf('\n', i)
207
+ i = nl === -1 ? text.length : nl
208
+ continue
209
+ }
210
+ if (c === '/' && text[i + 1] === '*') {
211
+ const end = text.indexOf('*/', i + 2)
212
+ i = end === -1 ? text.length : end + 2
213
+ continue
214
+ }
215
+ if (c === '{') depth++
216
+ else if (c === '}') depth--
217
+ i++
218
+ }
219
+ return depth
220
+ }
221
+
222
+ /** Segment an mdx-lite document into imports / markdown / jsx blocks. */
223
+ export function segmentMdx(source: string): MdxBlock[] {
224
+ const lines = source.replace(/\r\n/g, '\n').split('\n')
225
+ const blocks: MdxBlock[] = []
226
+ let md: string[] = []
227
+ let inFence = false
228
+ let fence = ''
229
+
230
+ const flushMd = (): void => {
231
+ const text = md.join('\n').replace(/^\n+/, '').replace(/\n+$/, '')
232
+ if (text.trim() !== '') blocks.push({ kind: 'markdown', text })
233
+ md = []
234
+ }
235
+
236
+ let li = 0
237
+ while (li < lines.length) {
238
+ const line = lines[li] as string
239
+
240
+ if (inFence) {
241
+ md.push(line)
242
+ if (FENCE.test(line) && line.trim().startsWith(fence)) inFence = false
243
+ li++
244
+ continue
245
+ }
246
+
247
+ const fenceMatch = FENCE.exec(line)
248
+ if (fenceMatch) {
249
+ inFence = true
250
+ fence = fenceMatch[1] as string
251
+ md.push(line)
252
+ li++
253
+ continue
254
+ }
255
+
256
+ if (IMPORT_EXPORT.test(line)) {
257
+ flushMd()
258
+ const start = li
259
+ let stmt = line
260
+ li++
261
+ while (braceBalance(stmt) > 0 && li < lines.length) {
262
+ stmt += `\n${lines[li]}`
263
+ li++
264
+ }
265
+ // greedily absorb a run of further import/export statements
266
+ const codeLines = lines.slice(start, li)
267
+ while (li < lines.length && IMPORT_EXPORT.test(lines[li] as string)) {
268
+ let next = lines[li] as string
269
+ codeLines.push(next)
270
+ li++
271
+ while (braceBalance(next) > 0 && li < lines.length) {
272
+ next += `\n${lines[li]}`
273
+ codeLines.push(lines[li] as string)
274
+ li++
275
+ }
276
+ }
277
+ blocks.push({ kind: 'imports', code: codeLines.join('\n') })
278
+ continue
279
+ }
280
+
281
+ if (JSX_BLOCK_START.test(line)) {
282
+ flushMd()
283
+ const rest = lines.slice(li).join('\n')
284
+ const tags: string[] = []
285
+ const end = scanElement(rest, 0, tags)
286
+ const consumed = rest.slice(0, end)
287
+ const nLines = consumed.split('\n').length
288
+ const code = lines.slice(li, li + nLines).join('\n')
289
+ blocks.push({ kind: 'jsx', code, tags })
290
+ li += nLines
291
+ continue
292
+ }
293
+
294
+ md.push(line)
295
+ li++
296
+ }
297
+ flushMd()
298
+ return blocks
299
+ }
300
+
301
+ // ----------------------------------------------------------------------------
302
+ // Compilation to TSX
303
+ // ----------------------------------------------------------------------------
304
+
305
+ /** Extract the local binding names introduced by import/export statements. */
306
+ export function extractBoundNames(code: string): Set<string> {
307
+ const names = new Set<string>()
308
+ // namespace: import * as N from …
309
+ for (const m of code.matchAll(/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)/g))
310
+ names.add(m[1] as string)
311
+ // default: import Name from … / import Name, { … } from …
312
+ for (const m of code.matchAll(/import\s+([A-Za-z_$][\w$]*)\s*(?:,|from)/g))
313
+ names.add(m[1] as string)
314
+ // named: import … { a, b as c } from …
315
+ for (const m of code.matchAll(/import[^{]*\{([^}]*)\}/g)) {
316
+ for (const part of (m[1] as string).split(',')) {
317
+ const seg = part.trim()
318
+ if (!seg) continue
319
+ const as = seg.split(/\s+as\s+/)
320
+ names.add((as[1] ?? as[0] ?? '').trim())
321
+ }
322
+ }
323
+ // export const/let/var/function/class Name
324
+ for (const m of code.matchAll(
325
+ /export\s+(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g,
326
+ ))
327
+ names.add(m[1] as string)
328
+ names.delete('')
329
+ return names
330
+ }
331
+
332
+ const indent = (text: string, pad: string): string =>
333
+ text
334
+ .split('\n')
335
+ .map((l) => (l === '' ? l : pad + l))
336
+ .join('\n')
337
+
338
+ /** Compile an mdx-lite document to a `.tsx` module source string. */
339
+ export function mdxToTsx(source: string, opts: MdxToTsxOptions = {}): string {
340
+ const spec = opts.markdownSpecifier ?? 'markdown-to-jsx'
341
+ const blocks = segmentMdx(source)
342
+
343
+ const importsCode = blocks
344
+ .filter(
345
+ (b): b is Extract<MdxBlock, { kind: 'imports' }> => b.kind === 'imports',
346
+ )
347
+ .map((b) => b.code)
348
+ .join('\n')
349
+ const bound = extractBoundNames(importsCode)
350
+
351
+ // Component tags used by JSX blocks but not imported → resolved from props.
352
+ const used = new Set<string>()
353
+ for (const b of blocks) {
354
+ if (b.kind !== 'jsx') continue
355
+ for (const t of b.tags) used.add(t)
356
+ }
357
+ const external = [...used].filter(
358
+ (n) => /^[A-Z][\w]*$/.test(n) && !bound.has(n),
359
+ )
360
+
361
+ const body = blocks
362
+ .map((b) => {
363
+ if (b.kind === 'imports') return ''
364
+ if (b.kind === 'markdown')
365
+ return `<__Md options={__mdOpts}>{${JSON.stringify(b.text)}}</__Md>`
366
+ return b.code
367
+ })
368
+ .filter((x) => x !== '')
369
+
370
+ const destructure =
371
+ external.length > 0
372
+ ? ` const { ${external.join(', ')} } = __components\n`
373
+ : ''
374
+
375
+ return `// AUTO-GENERATED by display-case mdx-lite — do not edit.
376
+ import __Md from ${JSON.stringify(spec)}
377
+ ${importsCode}
378
+
379
+ export default function MDXContent(props) {
380
+ const __components = (props && props.components) || {}
381
+ ${destructure} const { h1: __h1, h2: __h2 } = __components
382
+ const __ov = {}
383
+ if (__h1) __ov.h1 = __h1
384
+ if (__h2) __ov.h2 = __h2
385
+ const __mdOpts = { disableParsingRawHTML: true, overrides: __ov }
386
+ return (
387
+ <>
388
+ ${body.map((x) => indent(x, ' ')).join('\n')}
389
+ </>
390
+ )
391
+ }
392
+ `
393
+ }