@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,144 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change-scoping support: compute, for a set of changed files, which components
|
|
5
|
+
* a render-time check (a11y / visual) actually needs to re-run.
|
|
6
|
+
*
|
|
7
|
+
* A component is "affected" when any changed file lies in the *import closure*
|
|
8
|
+
* of its case file — the case itself, the component it imports, and everything
|
|
9
|
+
* those pull in transitively (including `@import`ed CSS). The closure follows
|
|
10
|
+
* only **relative** specifiers: a Display Case showcase is dependency-light and
|
|
11
|
+
* a pull request virtually never edits a node_modules package, so bare imports
|
|
12
|
+
* (`react`, `display-case`, …) are intentionally not traced. This keeps the
|
|
13
|
+
* analysis a pure, fast file walk with no bundler or module-graph dependency.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is pure (path + file reads only) so it is unit-testable
|
|
16
|
+
* without a server, a browser, or git.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Extensions we try when a relative specifier omits one, in resolution order.
|
|
20
|
+
// A specifier that already carries an extension (e.g. `./styles.css`) resolves
|
|
21
|
+
// via the bare candidate first.
|
|
22
|
+
const RESOLVE_EXTS = [
|
|
23
|
+
'.tsx',
|
|
24
|
+
'.ts',
|
|
25
|
+
'.jsx',
|
|
26
|
+
'.js',
|
|
27
|
+
'.mjs',
|
|
28
|
+
'.cjs',
|
|
29
|
+
'.css',
|
|
30
|
+
'.json',
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
// Relative import/export specifiers in a JS/TS source. Covers `import … from`,
|
|
34
|
+
// `export … from`, side-effect `import '…'`, and dynamic `import('…')` /
|
|
35
|
+
// `require('…')`. Source-level regexes (not a parser) — good enough for a
|
|
36
|
+
// dependency walk and free of any parsing dependency.
|
|
37
|
+
const JS_FROM = /(?:import|export)\b[^;'"]*?\bfrom\s*['"]([^'"]+)['"]/g
|
|
38
|
+
const JS_BARE = /\bimport\s*['"]([^'"]+)['"]/g
|
|
39
|
+
const JS_DYNAMIC = /\b(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
40
|
+
// `@import '…'` / `@import "…"` / `@import url(…)` in CSS.
|
|
41
|
+
const CSS_IMPORT = /@import\s+(?:url\(\s*)?['"]([^'"]+)['"]/g
|
|
42
|
+
|
|
43
|
+
function specifiers(source: string, isCss: boolean): string[] {
|
|
44
|
+
const out: string[] = []
|
|
45
|
+
const collect = (re: RegExp) => {
|
|
46
|
+
re.lastIndex = 0
|
|
47
|
+
for (let m = re.exec(source); m; m = re.exec(source)) out.push(m[1])
|
|
48
|
+
}
|
|
49
|
+
if (isCss) {
|
|
50
|
+
collect(CSS_IMPORT)
|
|
51
|
+
} else {
|
|
52
|
+
collect(JS_FROM)
|
|
53
|
+
collect(JS_BARE)
|
|
54
|
+
collect(JS_DYNAMIC)
|
|
55
|
+
}
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function* candidates(base: string): Generator<string> {
|
|
60
|
+
yield base
|
|
61
|
+
for (const ext of RESOLVE_EXTS) yield base + ext
|
|
62
|
+
for (const ext of RESOLVE_EXTS) yield join(base, `index${ext}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolve a relative specifier to an absolute file path, or null. Bare
|
|
66
|
+
* specifiers (not starting with `.` or `/`) are not traced and return null. */
|
|
67
|
+
async function resolveSpecifier(
|
|
68
|
+
fromFile: string,
|
|
69
|
+
spec: string,
|
|
70
|
+
): Promise<string | null> {
|
|
71
|
+
if (!spec.startsWith('.') && !spec.startsWith('/')) return null
|
|
72
|
+
const base = isAbsolute(spec) ? spec : resolve(dirname(fromFile), spec)
|
|
73
|
+
for (const cand of candidates(base)) {
|
|
74
|
+
if (await Bun.file(cand).exists()) return cand
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The transitive relative-import closure of one or more entry files, as a set
|
|
81
|
+
* of absolute paths that includes the entries themselves. Files that can't be
|
|
82
|
+
* read are skipped (an entry pointing at a deleted file contributes only
|
|
83
|
+
* itself).
|
|
84
|
+
*/
|
|
85
|
+
export async function importClosure(entries: string[]): Promise<Set<string>> {
|
|
86
|
+
const seen = new Set<string>()
|
|
87
|
+
const queue = entries.map((e) => resolve(e))
|
|
88
|
+
while (queue.length) {
|
|
89
|
+
const file = queue.pop()
|
|
90
|
+
if (!file || seen.has(file)) continue
|
|
91
|
+
seen.add(file)
|
|
92
|
+
let source: string
|
|
93
|
+
try {
|
|
94
|
+
source = await Bun.file(file).text()
|
|
95
|
+
} catch {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
for (const spec of specifiers(source, file.endsWith('.css'))) {
|
|
99
|
+
const resolved = await resolveSpecifier(file, spec)
|
|
100
|
+
if (resolved && !seen.has(resolved)) queue.push(resolved)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return seen
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Each component's import closure, keyed by component id. */
|
|
107
|
+
export async function componentClosures(
|
|
108
|
+
components: { id: string; caseFile: string }[],
|
|
109
|
+
): Promise<Map<string, Set<string>>> {
|
|
110
|
+
const map = new Map<string, Set<string>>()
|
|
111
|
+
for (const c of components) map.set(c.id, await importClosure([c.caseFile]))
|
|
112
|
+
return map
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The subset of `components` affected by `changedFiles` — i.e. those whose case
|
|
117
|
+
* file's import closure contains at least one changed file. `changedFiles` and
|
|
118
|
+
* each `caseFile` are absolute paths.
|
|
119
|
+
*
|
|
120
|
+
* Note: this attributes a changed file to a component only when that file is in
|
|
121
|
+
* the component's *import closure*. A changed file that no closure claims (e.g.
|
|
122
|
+
* globally-inlined CSS, the render pipeline) is invisible here — the caller is
|
|
123
|
+
* responsible for treating such an unattributed render-input as affecting every
|
|
124
|
+
* component. See `componentClosures` for building the closures once to make that
|
|
125
|
+
* coverage check.
|
|
126
|
+
*/
|
|
127
|
+
export async function affectedComponents(
|
|
128
|
+
components: { id: string; caseFile: string }[],
|
|
129
|
+
changedFiles: Iterable<string>,
|
|
130
|
+
): Promise<Set<string>> {
|
|
131
|
+
const changed = new Set<string>()
|
|
132
|
+
for (const f of changedFiles) changed.add(resolve(f))
|
|
133
|
+
const closures = await componentClosures(components)
|
|
134
|
+
const affected = new Set<string>()
|
|
135
|
+
for (const [id, files] of closures) {
|
|
136
|
+
for (const f of files) {
|
|
137
|
+
if (changed.has(f)) {
|
|
138
|
+
affected.add(id)
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return affected
|
|
144
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { CaseModule } from '../index'
|
|
3
|
+
import { defineCases, defineFlow, flowStep, tweak } from '../index'
|
|
4
|
+
import { buildCatalog, findCase, slugify } from './catalog'
|
|
5
|
+
|
|
6
|
+
describe('slugify', () => {
|
|
7
|
+
test('lowercases and kebabs whitespace', () => {
|
|
8
|
+
expect(slugify('Sign In Form')).toBe('sign-in-form')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('collapses runs of non-alphanumerics into a single dash', () => {
|
|
12
|
+
expect(slugify('Hello World!!!')).toBe('hello-world')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('strips leading and trailing separators', () => {
|
|
16
|
+
expect(slugify(' --Button-- ')).toBe('button')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('keeps an existing hyphen inside a name', () => {
|
|
20
|
+
expect(slugify('Sign-in')).toBe('sign-in')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('keeps digits', () => {
|
|
24
|
+
expect(slugify('H1 Heading 2')).toBe('h1-heading-2')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('treats non-ascii letters as separators', () => {
|
|
28
|
+
expect(slugify('Café Crème')).toBe('caf-cr-me')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('slugs all-separator or empty input to an empty string', () => {
|
|
32
|
+
expect(slugify(' ')).toBe('')
|
|
33
|
+
expect(slugify('—')).toBe('')
|
|
34
|
+
expect(slugify('')).toBe('')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('buildCatalog', () => {
|
|
39
|
+
test('slugifies component and case ids while preserving display names', () => {
|
|
40
|
+
const cat = buildCatalog([
|
|
41
|
+
defineCases('Icon Button', { 'With Label': () => null }),
|
|
42
|
+
])
|
|
43
|
+
expect(cat).toHaveLength(1)
|
|
44
|
+
expect(cat[0].id).toBe('icon-button')
|
|
45
|
+
expect(cat[0].name).toBe('Icon Button')
|
|
46
|
+
expect(cat[0].cases[0].id).toBe('with-label')
|
|
47
|
+
expect(cat[0].cases[0].name).toBe('With Label')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('a simple (function) case carries null tweaks and no transitions', () => {
|
|
51
|
+
const cat = buildCatalog([defineCases('Button', { Default: () => null })])
|
|
52
|
+
expect(cat[0].cases[0].tweaks).toBeNull()
|
|
53
|
+
expect(cat[0].cases[0].transitions).toEqual([])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a tweaked case exposes its declared tweak schema', () => {
|
|
57
|
+
const tweaks = { label: tweak.text('Save') }
|
|
58
|
+
const cat = buildCatalog([
|
|
59
|
+
defineCases('Button', { Custom: { tweaks, render: () => null } }),
|
|
60
|
+
])
|
|
61
|
+
expect(cat[0].cases[0].tweaks).toEqual(tweaks)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('orders components by hierarchy level, then by name', () => {
|
|
65
|
+
const cat = buildCatalog([
|
|
66
|
+
defineCases('Zeta', { D: () => null }, { level: 'organism' }),
|
|
67
|
+
defineCases('Alpha', { D: () => null }, { level: 'atom' }),
|
|
68
|
+
defineCases('Beta', { D: () => null }, { level: 'atom' }),
|
|
69
|
+
])
|
|
70
|
+
expect(cat.map((c) => c.name)).toEqual(['Alpha', 'Beta', 'Zeta'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('places unclassified (level-less) components last', () => {
|
|
74
|
+
const cat = buildCatalog([
|
|
75
|
+
defineCases('NoLevel', { D: () => null }),
|
|
76
|
+
defineCases('Page', { D: () => null }, { level: 'page' }),
|
|
77
|
+
defineCases('Atom', { D: () => null }, { level: 'atom' }),
|
|
78
|
+
])
|
|
79
|
+
expect(cat.map((c) => c.name)).toEqual(['Atom', 'Page', 'NoLevel'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('preserves case insertion order within a component', () => {
|
|
83
|
+
const cat = buildCatalog([
|
|
84
|
+
defineCases('Button', {
|
|
85
|
+
Third: () => null,
|
|
86
|
+
First: () => null,
|
|
87
|
+
Second: () => null,
|
|
88
|
+
}),
|
|
89
|
+
])
|
|
90
|
+
expect(cat[0].cases.map((c) => c.name)).toEqual([
|
|
91
|
+
'Third',
|
|
92
|
+
'First',
|
|
93
|
+
'Second',
|
|
94
|
+
])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('records a flow step’s outgoing transitions as slugified target ids', () => {
|
|
98
|
+
const cat = buildCatalog([
|
|
99
|
+
defineFlow('Sign In', {
|
|
100
|
+
steps: {
|
|
101
|
+
'Request Link': flowStep({
|
|
102
|
+
transitions: ['Check Email'],
|
|
103
|
+
render: () => null,
|
|
104
|
+
}),
|
|
105
|
+
'Check Email': flowStep({ render: () => null }),
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
])
|
|
109
|
+
expect(cat[0].isFlow).toBe(true)
|
|
110
|
+
expect(cat[0].level).toBe('flow')
|
|
111
|
+
expect(cat[0].cases[0].transitions).toEqual(['check-email'])
|
|
112
|
+
expect(cat[0].cases[1].transitions).toEqual([])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('sorts flows after every classified level (flow is the last level)', () => {
|
|
116
|
+
const cat = buildCatalog([
|
|
117
|
+
defineFlow('Onboard', { steps: { A: flowStep({ render: () => null }) } }),
|
|
118
|
+
defineCases('Page', { D: () => null }, { level: 'page' }),
|
|
119
|
+
])
|
|
120
|
+
expect(cat.map((c) => c.name)).toEqual(['Page', 'Onboard'])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('findCase', () => {
|
|
125
|
+
const modules: CaseModule[] = [
|
|
126
|
+
defineCases('Icon Button', { 'With Label': () => null }),
|
|
127
|
+
defineFlow('Sign In', {
|
|
128
|
+
steps: { 'Request Link': flowStep({ render: () => null }) },
|
|
129
|
+
}),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
test('resolves a component+case slug to original names and the case', () => {
|
|
133
|
+
const hit = findCase(modules, 'icon-button', 'with-label')
|
|
134
|
+
expect(hit).not.toBeNull()
|
|
135
|
+
expect(hit?.module.component).toBe('Icon Button')
|
|
136
|
+
expect(hit?.caseName).toBe('With Label')
|
|
137
|
+
expect(typeof hit?.case).toBe('function')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('resolves a flow step by its slug', () => {
|
|
141
|
+
const hit = findCase(modules, 'sign-in', 'request-link')
|
|
142
|
+
expect(hit?.caseName).toBe('Request Link')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('returns null for an unknown component', () => {
|
|
146
|
+
expect(findCase(modules, 'nope', 'with-label')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('returns null for an unknown case within a known component', () => {
|
|
150
|
+
expect(findCase(modules, 'icon-button', 'nope')).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Case,
|
|
3
|
+
CaseModule,
|
|
4
|
+
FlowStep,
|
|
5
|
+
HierarchyLevel,
|
|
6
|
+
TweakSchema,
|
|
7
|
+
} from '../index'
|
|
8
|
+
import { HIERARCHY_LEVELS } from '../index'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pure catalog model shared by the server (to build the manifest) and the
|
|
12
|
+
* browser (to resolve a slug back to a case). No node/DOM-specific imports.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface CatalogCase {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
/** Declared tweak schema, or null when the case takes no tweaks. */
|
|
19
|
+
tweaks: TweakSchema | null
|
|
20
|
+
/** Slugified ids of steps this step can transition to (flows only; else []). */
|
|
21
|
+
transitions: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CatalogComponent {
|
|
25
|
+
id: string
|
|
26
|
+
name: string
|
|
27
|
+
level: HierarchyLevel | null
|
|
28
|
+
isFlow: boolean
|
|
29
|
+
cases: CatalogCase[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Kebab-case a display name into a stable, URL-safe slug. */
|
|
33
|
+
export function slugify(name: string): string {
|
|
34
|
+
return name
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-+|-+$/g, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function caseTweaks(c: Case | FlowStep): TweakSchema | null {
|
|
42
|
+
return typeof c === 'function' ? null : (c.tweaks ?? null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Outgoing transitions of a flow step, as slugified target ids. */
|
|
46
|
+
function caseTransitions(c: Case | FlowStep): string[] {
|
|
47
|
+
if (typeof c === 'function' || !('transitions' in c) || !c.transitions) {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
return c.transitions.map(slugify)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build the ordered catalog from discovered case modules. */
|
|
54
|
+
export function buildCatalog(modules: CaseModule[]): CatalogComponent[] {
|
|
55
|
+
const components = modules.map((mod) => ({
|
|
56
|
+
id: slugify(mod.component),
|
|
57
|
+
name: mod.component,
|
|
58
|
+
level: mod.level ?? null,
|
|
59
|
+
isFlow: mod.isFlow,
|
|
60
|
+
cases: Object.entries(mod.cases).map(([name, c]) => ({
|
|
61
|
+
id: slugify(name),
|
|
62
|
+
name,
|
|
63
|
+
tweaks: caseTweaks(c),
|
|
64
|
+
transitions: caseTransitions(c),
|
|
65
|
+
})),
|
|
66
|
+
}))
|
|
67
|
+
// Stable sort: by hierarchy level (atoms first, unclassified last), then name.
|
|
68
|
+
return components.sort((a, b) => {
|
|
69
|
+
const la = a.level
|
|
70
|
+
? HIERARCHY_LEVELS.indexOf(a.level)
|
|
71
|
+
: HIERARCHY_LEVELS.length
|
|
72
|
+
const lb = b.level
|
|
73
|
+
? HIERARCHY_LEVELS.indexOf(b.level)
|
|
74
|
+
: HIERARCHY_LEVELS.length
|
|
75
|
+
return la - lb || a.name.localeCompare(b.name)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolve a component+case slug pair back to its module and renderable case. */
|
|
80
|
+
export function findCase(
|
|
81
|
+
modules: CaseModule[],
|
|
82
|
+
componentId: string,
|
|
83
|
+
caseId: string,
|
|
84
|
+
): { module: CaseModule; caseName: string; case: Case | FlowStep } | null {
|
|
85
|
+
const mod = modules.find((m) => slugify(m.component) === componentId)
|
|
86
|
+
if (!mod) return null
|
|
87
|
+
const entry = Object.entries(mod.cases).find(
|
|
88
|
+
([name]) => slugify(name) === caseId,
|
|
89
|
+
)
|
|
90
|
+
if (!entry) return null
|
|
91
|
+
return { module: mod, caseName: entry[0], case: entry[1] }
|
|
92
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
3
|
+
import { isAbsolute, join } from 'node:path'
|
|
4
|
+
import type { DisplayCaseConfig } from '../index'
|
|
5
|
+
import { makeTempDir, writeFiles } from '../testing/test-helpers'
|
|
6
|
+
import {
|
|
7
|
+
baselineDir,
|
|
8
|
+
cacheDir,
|
|
9
|
+
codegenPrimerEntry,
|
|
10
|
+
codegenRenderEntry,
|
|
11
|
+
discoverCaseFiles,
|
|
12
|
+
loadModules,
|
|
13
|
+
resolveConfig,
|
|
14
|
+
} from './discovery'
|
|
15
|
+
|
|
16
|
+
const dirs: string[] = []
|
|
17
|
+
const setup = async (files: Record<string, string>) => {
|
|
18
|
+
const dir = await makeTempDir()
|
|
19
|
+
dirs.push(dir)
|
|
20
|
+
await writeFiles(dir, files)
|
|
21
|
+
return dir
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
while (dirs.length)
|
|
26
|
+
await rm(dirs.pop() as string, { recursive: true, force: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const cfg = (over: Partial<DisplayCaseConfig> = {}): DisplayCaseConfig => ({
|
|
30
|
+
title: 'T',
|
|
31
|
+
roots: ['**/*.case.tsx'],
|
|
32
|
+
...over,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('resolveConfig', () => {
|
|
36
|
+
test('imports the default-exported config and returns its path', async () => {
|
|
37
|
+
const dir = await setup({
|
|
38
|
+
'display-case.config.ts': `export default { title: 'T', roots: ['x'] }\n`,
|
|
39
|
+
})
|
|
40
|
+
const { config, configPath } = await resolveConfig(dir)
|
|
41
|
+
expect(config.title).toBe('T')
|
|
42
|
+
expect(configPath.endsWith('display-case.config.ts')).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('throws when no config file is present', async () => {
|
|
46
|
+
const dir = await setup({})
|
|
47
|
+
expect(resolveConfig(dir)).rejects.toThrow(/No Display Case config/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('throws when the config file has no default export', async () => {
|
|
51
|
+
const dir = await setup({
|
|
52
|
+
'display-case.config.ts': `export const notDefault = { title: 'T', roots: [] }\n`,
|
|
53
|
+
})
|
|
54
|
+
expect(resolveConfig(dir)).rejects.toThrow(/default-export/)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('discoverCaseFiles', () => {
|
|
59
|
+
test('resolves globs to sorted absolute paths', async () => {
|
|
60
|
+
const dir = await setup({
|
|
61
|
+
'a/Beta.case.tsx': 'export default {}\n',
|
|
62
|
+
'a/Alpha.case.tsx': 'export default {}\n',
|
|
63
|
+
'b/Gamma.case.tsx': 'export default {}\n',
|
|
64
|
+
})
|
|
65
|
+
const files = await discoverCaseFiles(dir, cfg())
|
|
66
|
+
expect(files.every(isAbsolute)).toBe(true)
|
|
67
|
+
expect(files.map((f) => f.slice(dir.length + 1))).toEqual([
|
|
68
|
+
'a/Alpha.case.tsx',
|
|
69
|
+
'a/Beta.case.tsx',
|
|
70
|
+
'b/Gamma.case.tsx',
|
|
71
|
+
])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('skips node_modules and dedups across overlapping globs', async () => {
|
|
75
|
+
const dir = await setup({
|
|
76
|
+
'Button.case.tsx': 'export default {}\n',
|
|
77
|
+
'node_modules/pkg/Ignored.case.tsx': 'export default {}\n',
|
|
78
|
+
})
|
|
79
|
+
const files = await discoverCaseFiles(
|
|
80
|
+
dir,
|
|
81
|
+
cfg({ roots: ['**/*.case.tsx', '**/*.case.tsx'] }),
|
|
82
|
+
)
|
|
83
|
+
expect(files.map((f) => f.slice(dir.length + 1))).toEqual([
|
|
84
|
+
'Button.case.tsx',
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('loadModules', () => {
|
|
90
|
+
test('imports a valid case module', async () => {
|
|
91
|
+
const dir = await setup({
|
|
92
|
+
'Button.case.tsx': `export default { component: 'Button', cases: {}, isFlow: false }\n`,
|
|
93
|
+
})
|
|
94
|
+
const file = join(dir, 'Button.case.tsx')
|
|
95
|
+
const { modules, errors } = await loadModules([file])
|
|
96
|
+
expect(errors).toEqual([])
|
|
97
|
+
expect(modules).toHaveLength(1)
|
|
98
|
+
expect(modules[0].module.component).toBe('Button')
|
|
99
|
+
expect(modules[0].file).toBe(file)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('records a file with no default export as an error', async () => {
|
|
103
|
+
const dir = await setup({ 'Bad.case.tsx': `export const nope = 1\n` })
|
|
104
|
+
const file = join(dir, 'Bad.case.tsx')
|
|
105
|
+
const { modules, errors } = await loadModules([file])
|
|
106
|
+
expect(modules).toEqual([])
|
|
107
|
+
expect(errors).toHaveLength(1)
|
|
108
|
+
expect(errors[0].file).toBe(file)
|
|
109
|
+
expect(errors[0].error).toMatch(/no valid default export/)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('records a default export whose component is not a string', async () => {
|
|
113
|
+
const dir = await setup({
|
|
114
|
+
'Bad.case.tsx': `export default { cases: {}, isFlow: false }\n`,
|
|
115
|
+
})
|
|
116
|
+
const { modules, errors } = await loadModules([join(dir, 'Bad.case.tsx')])
|
|
117
|
+
expect(modules).toEqual([])
|
|
118
|
+
expect(errors).toHaveLength(1)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('captures a throwing import and still loads the rest', async () => {
|
|
122
|
+
const dir = await setup({
|
|
123
|
+
'Throws.case.tsx': `throw new Error('boom')\n`,
|
|
124
|
+
'Good.case.tsx': `export default { component: 'Good', cases: {}, isFlow: false }\n`,
|
|
125
|
+
})
|
|
126
|
+
const { modules, errors } = await loadModules([
|
|
127
|
+
join(dir, 'Throws.case.tsx'),
|
|
128
|
+
join(dir, 'Good.case.tsx'),
|
|
129
|
+
])
|
|
130
|
+
expect(modules.map((m) => m.module.component)).toEqual(['Good'])
|
|
131
|
+
expect(errors).toHaveLength(1)
|
|
132
|
+
expect(errors[0].error).toMatch(/boom/)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('cacheDir / baselineDir', () => {
|
|
137
|
+
test('cacheDir is the .display-case dir under the package', () => {
|
|
138
|
+
expect(cacheDir('/pkg')).toBe('/pkg/.display-case')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('baselineDir defaults to the cache baselines dir', () => {
|
|
142
|
+
expect(baselineDir('/pkg', cfg())).toBe('/pkg/.display-case/baselines')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('baselineDir joins a relative override to the package', () => {
|
|
146
|
+
expect(baselineDir('/pkg', cfg({ baselineDir: 'snaps' }))).toBe(
|
|
147
|
+
'/pkg/snaps',
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('baselineDir honors an absolute override verbatim', () => {
|
|
152
|
+
expect(baselineDir('/pkg', cfg({ baselineDir: '/abs/snaps' }))).toBe(
|
|
153
|
+
'/abs/snaps',
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('codegen entries', () => {
|
|
159
|
+
test('codegenRenderEntry imports every case, tags its source path, and mounts', async () => {
|
|
160
|
+
const dir = await setup({})
|
|
161
|
+
const files = [join(dir, 'a/Button.case.tsx'), join(dir, 'b/Card.case.tsx')]
|
|
162
|
+
const configPath = join(dir, 'display-case.config.ts')
|
|
163
|
+
const entry = await codegenRenderEntry(dir, files, configPath)
|
|
164
|
+
expect(entry).toBe(join(cacheDir(dir), 'render-entry.tsx'))
|
|
165
|
+
const src = await Bun.file(entry).text()
|
|
166
|
+
expect(src).toContain('AUTO-GENERATED')
|
|
167
|
+
expect(src).toContain('import m0 from')
|
|
168
|
+
expect(src).toContain('import m1 from')
|
|
169
|
+
expect(src).toContain('mountRender([')
|
|
170
|
+
expect(src).toContain(`sourcePath: ${JSON.stringify('a/Button.case.tsx')}`)
|
|
171
|
+
expect(src).toContain(`sourcePath: ${JSON.stringify('b/Card.case.tsx')}`)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('codegenPrimerEntry imports the MDX document and mounts the primer', async () => {
|
|
175
|
+
const dir = await setup({})
|
|
176
|
+
const entry = await codegenPrimerEntry(dir, 'docs/primer.mdx')
|
|
177
|
+
expect(entry).toBe(join(cacheDir(dir), 'primer-entry.tsx'))
|
|
178
|
+
const src = await Bun.file(entry).text()
|
|
179
|
+
expect(src).toContain('AUTO-GENERATED')
|
|
180
|
+
expect(src).toContain('import MDXContent from')
|
|
181
|
+
expect(src).toContain('primer.mdx')
|
|
182
|
+
expect(src).toContain('mountPrimer(MDXContent)')
|
|
183
|
+
})
|
|
184
|
+
})
|