@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.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/display-case.prompt.md +64 -0
- package/docs/ai-agents.md +126 -0
- package/docs/cli.md +99 -0
- package/docs/configuration.md +410 -0
- package/docs/documentation-panel.md +50 -0
- package/docs/examples/README.md +14 -0
- package/docs/examples/multi-variant.case.tsx +30 -0
- package/docs/examples/plain.case.tsx +22 -0
- package/docs/examples/tweak-control.placard.md +80 -0
- package/docs/examples/tweaks.case.tsx +39 -0
- package/docs/hierarchy.md +59 -0
- package/docs/quick-start.md +78 -0
- package/docs/style-engines.md +180 -0
- package/docs/testing.md +245 -0
- package/docs/theming.md +97 -0
- package/docs/tweaks.md +75 -0
- package/docs/writing-cases.md +144 -0
- package/docs/writing-placard-docs.md +194 -0
- package/package.json +113 -0
- package/skills/display-case-author-case/README.md +20 -0
- package/skills/display-case-author-case/SKILL.md +40 -0
- package/skills/display-case-author-placard-doc/README.md +24 -0
- package/skills/display-case-author-placard-doc/SKILL.md +65 -0
- package/skills/display-case-review/README.md +19 -0
- package/skills/display-case-review/SKILL.md +30 -0
- package/skills/display-case-snapshot/README.md +20 -0
- package/skills/display-case-snapshot/SKILL.md +29 -0
- package/src/checks/a11y-scanner.test.ts +240 -0
- package/src/checks/a11y-scanner.ts +410 -0
- package/src/checks/check-text.test.ts +53 -0
- package/src/checks/check-text.ts +78 -0
- package/src/checks/check.test.ts +194 -0
- package/src/checks/check.ts +473 -0
- package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
- package/src/checks/providers/pixelmatch-diff.ts +30 -0
- package/src/checks/providers/playwright-driver.ts +104 -0
- package/src/checks/ssr-check.test.ts +73 -0
- package/src/checks/ssr-check.ts +96 -0
- package/src/checks/structure-check.cross-package.test.ts +165 -0
- package/src/checks/structure-check.test.ts +651 -0
- package/src/checks/structure-check.ts +988 -0
- package/src/checks/tokens-check.test.ts +159 -0
- package/src/checks/tokens-check.ts +162 -0
- package/src/cli.ts +218 -0
- package/src/commands/agents.test.ts +24 -0
- package/src/commands/agents.ts +28 -0
- package/src/commands/init-run.test.ts +123 -0
- package/src/commands/init.test.ts +63 -0
- package/src/commands/init.ts +412 -0
- package/src/commands/publish.test.ts +210 -0
- package/src/commands/publish.ts +292 -0
- package/src/core/affected.test.ts +99 -0
- package/src/core/affected.ts +144 -0
- package/src/core/catalog.test.ts +152 -0
- package/src/core/catalog.ts +92 -0
- package/src/core/discovery.test.ts +184 -0
- package/src/core/discovery.ts +250 -0
- package/src/core/manifest.ts +41 -0
- package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
- package/src/core/mdx-lite/index.ts +393 -0
- package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
- package/src/core/mdx-plugin.test.ts +60 -0
- package/src/core/mdx-plugin.ts +30 -0
- package/src/flow-step.test-d.ts +39 -0
- package/src/index.test.ts +100 -0
- package/src/index.ts +564 -0
- package/src/render/collect-styles.emotion.test.tsx +114 -0
- package/src/render/collect-styles.test.tsx +72 -0
- package/src/render/collect-styles.ts +33 -0
- package/src/render/documents.test.ts +184 -0
- package/src/render/documents.ts +88 -0
- package/src/render/render-node.test.tsx +160 -0
- package/src/render/render-node.tsx +133 -0
- package/src/render/ssr-primer.test.tsx +25 -0
- package/src/render/ssr-primer.tsx +54 -0
- package/src/render/ssr-render.test.tsx +142 -0
- package/src/render/ssr-render.tsx +63 -0
- package/src/render/ssr-shell.test.tsx +57 -0
- package/src/render/ssr-shell.tsx +54 -0
- package/src/server/prod-server.ts +237 -0
- package/src/server/server.test.ts +117 -0
- package/src/server/server.ts +1039 -0
- package/src/style-engine.test-d.ts +37 -0
- package/src/testing/test-helpers.ts +27 -0
- package/src/types/pixelmatch.d.ts +12 -0
- package/src/ui/browser-entry.tsx +51 -0
- package/src/ui/chrome.css +485 -0
- package/src/ui/design-system/README.md +88 -0
- package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
- package/src/ui/design-system/components/controls/Button.css +89 -0
- package/src/ui/design-system/components/controls/Button.placard.md +14 -0
- package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
- package/src/ui/design-system/components/controls/Button.tsx +41 -0
- package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
- package/src/ui/design-system/components/controls/IconButton.css +67 -0
- package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
- package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
- package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
- package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
- package/src/ui/design-system/components/controls/Input.css +52 -0
- package/src/ui/design-system/components/controls/Input.placard.md +12 -0
- package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
- package/src/ui/design-system/components/controls/Input.tsx +45 -0
- package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
- package/src/ui/design-system/components/controls/Select.css +44 -0
- package/src/ui/design-system/components/controls/Select.placard.md +15 -0
- package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
- package/src/ui/design-system/components/controls/Select.tsx +58 -0
- package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
- package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
- package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
- package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
- package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
- package/src/ui/design-system/components/index.ts +66 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
- package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
- package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
- package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
- package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
- package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
- package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
- package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
- package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
- package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
- package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
- package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
- package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
- package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
- package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
- package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
- package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
- package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
- package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
- package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
- package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
- package/src/ui/design-system/components/showcase/Chip.css +51 -0
- package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
- package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
- package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
- package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
- package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
- package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
- package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
- package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
- package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
- package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
- package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
- package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
- package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
- package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
- package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
- package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
- package/src/ui/design-system/components/showcase/NavItem.css +111 -0
- package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
- package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
- package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
- package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
- package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
- package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
- package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
- package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
- package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
- package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
- package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
- package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
- package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
- package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
- package/src/ui/design-system/components/showcase/Stage.css +91 -0
- package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
- package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
- package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
- package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
- package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
- package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
- package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
- package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
- package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
- package/src/ui/design-system/primer-specimens/components.tsx +308 -0
- package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
- package/src/ui/design-system/primer-specimens/index.ts +25 -0
- package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
- package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
- package/src/ui/design-system/primer-specimens/type.tsx +75 -0
- package/src/ui/design-system/primer.mdx +236 -0
- package/src/ui/design-system/styles.css +14 -0
- package/src/ui/design-system/tokens/colors.css +172 -0
- package/src/ui/design-system/tokens/fonts.css +18 -0
- package/src/ui/design-system/tokens/spacing.css +48 -0
- package/src/ui/design-system/tokens/typography.css +49 -0
- package/src/ui/markdown.test.tsx +54 -0
- package/src/ui/markdown.tsx +19 -0
- package/src/ui/primer-mount.tsx +76 -0
- package/src/ui/primer.css +175 -0
- package/src/ui/primer.tsx +277 -0
- package/src/ui/render-mount.tsx +284 -0
- package/src/ui/shell-core.test.ts +340 -0
- package/src/ui/shell-core.ts +295 -0
- package/src/ui/shell.tsx +60 -0
- package/src/ui/test-ids.ts +53 -0
- 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
|
+
}
|