@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,210 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdir, mkdtemp, readdir, rm } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import type { Manifest } from '../core/manifest'
|
|
5
|
+
import { startProdServer } from '../server/prod-server'
|
|
6
|
+
import { makeTempDir } from '../testing/test-helpers'
|
|
7
|
+
import { type BuildDescriptor, publish } from './publish'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A build dir anchored INSIDE the repo. The published server/static SSR bundle
|
|
11
|
+
* keeps `react`/`react-dom` external (resolved at runtime), so when we import or
|
|
12
|
+
* serve it in-process the build must sit where module resolution walks up to the
|
|
13
|
+
* repo's `node_modules` — a `/tmp` dir has none. (A real deploy installs
|
|
14
|
+
* `node_modules` alongside the build; this reproduces that.) Gitignored via
|
|
15
|
+
* `.tmp/`.
|
|
16
|
+
*/
|
|
17
|
+
const REPO = resolve(import.meta.dir, '..', '..')
|
|
18
|
+
async function makeRepoTempDir(): Promise<string> {
|
|
19
|
+
const base = join(REPO, '.tmp')
|
|
20
|
+
await mkdir(base, { recursive: true })
|
|
21
|
+
return mkdtemp(join(base, 'publish-'))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Coverage for the publish / deploy path — previously untested. Two levels,
|
|
26
|
+
* both Docker-free:
|
|
27
|
+
* 1. the published *artifacts* (bundle, frozen manifest, generated
|
|
28
|
+
* server/Dockerfile, no dev machinery), and
|
|
29
|
+
* 2. the *served* build booted in-process via `startProdServer`, plus the
|
|
30
|
+
* `--static` export.
|
|
31
|
+
* The containerized deploy path (a real `docker build`) is Docker-gated in
|
|
32
|
+
* `test/publish-container.test.ts`.
|
|
33
|
+
*
|
|
34
|
+
* Published against a minimal fixture for speed and determinism.
|
|
35
|
+
*/
|
|
36
|
+
const FIXTURE = resolve(
|
|
37
|
+
import.meta.dir,
|
|
38
|
+
'..',
|
|
39
|
+
'..',
|
|
40
|
+
'e2e/fixtures/consumer-plain',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
describe('publish: artifacts', () => {
|
|
44
|
+
let out: string
|
|
45
|
+
let descriptor: BuildDescriptor
|
|
46
|
+
|
|
47
|
+
beforeAll(async () => {
|
|
48
|
+
out = await makeTempDir()
|
|
49
|
+
;({ descriptor } = await publish(FIXTURE, { out }))
|
|
50
|
+
})
|
|
51
|
+
afterAll(() => rm(out, { recursive: true, force: true }))
|
|
52
|
+
|
|
53
|
+
test('emits content-hashed browser + render bundles', async () => {
|
|
54
|
+
const assets = await readdir(join(out, 'assets'))
|
|
55
|
+
expect(assets.some((f) => /^browser-entry-.+\.js$/.test(f))).toBe(true)
|
|
56
|
+
expect(assets.some((f) => /^render-entry-.+\.js$/.test(f))).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('descriptor: production, a11y disabled, base-prefixed assets', () => {
|
|
60
|
+
expect(descriptor.title).toBe('Plain Consumer')
|
|
61
|
+
expect(descriptor.a11y).toBe(false)
|
|
62
|
+
expect(descriptor.base).toBe('')
|
|
63
|
+
expect(descriptor.assets.browser).toMatch(
|
|
64
|
+
/^\/assets\/browser-entry-.+\.js$/,
|
|
65
|
+
)
|
|
66
|
+
expect(descriptor.assets.render).toMatch(/^\/assets\/render-entry-.+\.js$/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('frozen manifest + build descriptor written to disk', async () => {
|
|
70
|
+
const manifest = (await Bun.file(
|
|
71
|
+
join(out, 'manifest.json'),
|
|
72
|
+
).json()) as Manifest
|
|
73
|
+
expect(Array.isArray(manifest.components)).toBe(true)
|
|
74
|
+
expect(manifest.components.length).toBeGreaterThan(0)
|
|
75
|
+
const dc = (await Bun.file(
|
|
76
|
+
join(out, 'dc-build.json'),
|
|
77
|
+
).json()) as BuildDescriptor
|
|
78
|
+
expect(dc.a11y).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('SSR renderers are built for the production server', async () => {
|
|
82
|
+
expect(await Bun.file(join(out, 'server', 'ssr-entry.js')).exists()).toBe(
|
|
83
|
+
true,
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('generated server.ts uses the prod-server, carries no dev machinery', async () => {
|
|
88
|
+
const s = await Bun.file(join(out, 'server.ts')).text()
|
|
89
|
+
expect(s).toContain("from 'display-case/prod-server'")
|
|
90
|
+
expect(s).toContain('AUTO-GENERATED')
|
|
91
|
+
expect(s).not.toContain('__livereload')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('generated package.json is a deployable service descriptor', async () => {
|
|
95
|
+
const pkg = (await Bun.file(join(out, 'package.json')).json()) as {
|
|
96
|
+
scripts: Record<string, string>
|
|
97
|
+
dependencies: Record<string, string>
|
|
98
|
+
}
|
|
99
|
+
expect(pkg.scripts.start).toBe('bun server.ts')
|
|
100
|
+
expect(pkg.dependencies['display-case']).toBeDefined()
|
|
101
|
+
expect(pkg.dependencies.react).toBeDefined()
|
|
102
|
+
expect(pkg.dependencies['react-dom']).toBeDefined()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('generated Dockerfile builds a Bun service with a health check', async () => {
|
|
106
|
+
const df = await Bun.file(join(out, 'Dockerfile')).text()
|
|
107
|
+
expect(df).toContain('FROM oven/bun')
|
|
108
|
+
expect(df).toContain('bun install --production')
|
|
109
|
+
expect(df).toContain('/health')
|
|
110
|
+
expect(df).toContain('CMD ["bun", "server.ts"]')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('--base prefixes the hashed asset URLs for subpath hosting', async () => {
|
|
114
|
+
const o2 = await makeTempDir()
|
|
115
|
+
try {
|
|
116
|
+
const { descriptor: d } = await publish(FIXTURE, {
|
|
117
|
+
out: o2,
|
|
118
|
+
base: '/showcase',
|
|
119
|
+
})
|
|
120
|
+
expect(d.base).toBe('/showcase')
|
|
121
|
+
expect(d.assets.browser).toMatch(/^\/showcase\/assets\//)
|
|
122
|
+
} finally {
|
|
123
|
+
await rm(o2, { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('publish: the served build is a functional showcase', () => {
|
|
129
|
+
let out: string
|
|
130
|
+
let descriptor: BuildDescriptor
|
|
131
|
+
let manifest: Manifest
|
|
132
|
+
let server: Awaited<ReturnType<typeof startProdServer>>
|
|
133
|
+
let baseUrl: string
|
|
134
|
+
|
|
135
|
+
beforeAll(async () => {
|
|
136
|
+
out = await makeRepoTempDir()
|
|
137
|
+
;({ descriptor } = await publish(FIXTURE, { out }))
|
|
138
|
+
manifest = (await Bun.file(join(out, 'manifest.json')).json()) as Manifest
|
|
139
|
+
server = await startProdServer(out, { port: 0 })
|
|
140
|
+
baseUrl = `http://localhost:${server.port}`
|
|
141
|
+
})
|
|
142
|
+
afterAll(async () => {
|
|
143
|
+
await server?.stop(true)
|
|
144
|
+
await rm(out, { recursive: true, force: true })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('GET /health returns ok', async () => {
|
|
148
|
+
const r = await fetch(`${baseUrl}/health`)
|
|
149
|
+
expect(r.status).toBe(200)
|
|
150
|
+
expect(await r.text()).toBe('ok')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('the shell is server-rendered, links hashed assets, and has no dev live-reload', async () => {
|
|
154
|
+
const r = await fetch(`${baseUrl}/`)
|
|
155
|
+
expect(r.status).toBe(200)
|
|
156
|
+
const html = await r.text()
|
|
157
|
+
expect(html).toContain('Plain Consumer') // title server-rendered
|
|
158
|
+
expect(html).toContain('data-ssr="1"') // content present before scripts
|
|
159
|
+
expect(html).toContain(descriptor.assets.browser) // hashed bundle linked
|
|
160
|
+
expect(html).not.toContain('__livereload') // dev SSE stripped
|
|
161
|
+
expect(html).not.toContain('EventSource')
|
|
162
|
+
expect(r.headers.get('cache-control')).toContain('no-cache')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('the isolated /render endpoint serves a chrome-free, pre-scripting document', async () => {
|
|
166
|
+
const c = manifest.components[0]
|
|
167
|
+
const cs = c.cases[0]
|
|
168
|
+
const r = await fetch(`${baseUrl}/render/${c.id}/${cs.id}`)
|
|
169
|
+
expect(r.status).toBe(200)
|
|
170
|
+
const html = await r.text()
|
|
171
|
+
expect(html).toContain('data-ssr="1"')
|
|
172
|
+
expect(html).toContain(descriptor.assets.render)
|
|
173
|
+
// Chrome-free: the shell's title chrome is absent from the isolated doc.
|
|
174
|
+
expect(html).not.toContain('Plain Consumer')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('hashed assets are served with immutable caching', async () => {
|
|
178
|
+
const r = await fetch(`${baseUrl}${descriptor.assets.browser}`)
|
|
179
|
+
expect(r.status).toBe(200)
|
|
180
|
+
expect(r.headers.get('cache-control')).toContain('immutable')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('publish: the static export needs no running server', () => {
|
|
185
|
+
let out: string
|
|
186
|
+
let descriptor: BuildDescriptor
|
|
187
|
+
let manifest: Manifest
|
|
188
|
+
|
|
189
|
+
beforeAll(async () => {
|
|
190
|
+
out = await makeRepoTempDir()
|
|
191
|
+
;({ descriptor } = await publish(FIXTURE, { out, static: true }))
|
|
192
|
+
manifest = (await Bun.file(join(out, 'manifest.json')).json()) as Manifest
|
|
193
|
+
})
|
|
194
|
+
afterAll(() => rm(out, { recursive: true, force: true }))
|
|
195
|
+
|
|
196
|
+
test('writes a complete index.html with the shell pre-rendered', async () => {
|
|
197
|
+
const html = await Bun.file(join(out, 'index.html')).text()
|
|
198
|
+
expect(html).toContain('<!doctype html>')
|
|
199
|
+
expect(html).toContain('Plain Consumer')
|
|
200
|
+
expect(html).toContain(descriptor.assets.browser)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('writes a complete per-case render document', async () => {
|
|
204
|
+
const c = manifest.components[0]
|
|
205
|
+
const cs = c.cases[0]
|
|
206
|
+
const file = join(out, 'render', c.id, cs.id, 'index.html')
|
|
207
|
+
expect(await Bun.file(file).exists()).toBe(true)
|
|
208
|
+
expect(await Bun.file(file).text()).toContain('data-ssr="1"')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
2
|
+
import { basename, join, resolve } from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
cacheDir,
|
|
5
|
+
codegenPrimerEntry,
|
|
6
|
+
codegenRenderEntry,
|
|
7
|
+
codegenSsrEntry,
|
|
8
|
+
codegenSsrPrimerEntry,
|
|
9
|
+
discoverCaseFiles,
|
|
10
|
+
resolveConfig,
|
|
11
|
+
} from '../core/discovery'
|
|
12
|
+
import { mdxPlugin } from '../core/mdx-plugin'
|
|
13
|
+
import type { DisplayCaseConfig } from '../index'
|
|
14
|
+
import { getManifest } from '../server/server'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a self-contained, hostable showcase: production-bundled assets
|
|
18
|
+
* (minified, content-hashed, no dev injects) plus a frozen manifest, styles, and
|
|
19
|
+
* the SSR renderers, written to an output directory that a thin production server
|
|
20
|
+
* (`prod-server.ts`) serves — or, with `static: true`, a crawl writes complete
|
|
21
|
+
* HTML files that need no running server. None of the dev machinery (watcher,
|
|
22
|
+
* live-reload, on-demand a11y, dev endpoints) is carried into the build.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const HERE = resolve(import.meta.dir, '..')
|
|
26
|
+
const BROWSER_ENTRY = join(HERE, 'ui', 'browser-entry.tsx')
|
|
27
|
+
const CHROME_CSS = join(HERE, 'ui', 'chrome.css')
|
|
28
|
+
const DS_DIR = join(HERE, 'ui', 'design-system', 'tokens')
|
|
29
|
+
const DS_TOKEN_FILES = ['colors.css', 'typography.css', 'spacing.css']
|
|
30
|
+
const COMPONENTS_DIR = join(HERE, 'ui', 'design-system', 'components')
|
|
31
|
+
const PRIMER_CSS = join(HERE, 'ui', 'primer.css')
|
|
32
|
+
|
|
33
|
+
export interface PublishOptions {
|
|
34
|
+
/** Output directory (default `<pkgDir>/dist-showcase`). */
|
|
35
|
+
out?: string
|
|
36
|
+
/** Base path for hosting under a subpath (e.g. `/showcase`). */
|
|
37
|
+
base?: string
|
|
38
|
+
/** Also write a fully-static (server-less) export. */
|
|
39
|
+
static?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Public env (`BUN_PUBLIC_*`) inlined into the browser bundle, plus a forced
|
|
43
|
+
* production NODE_ENV so React builds in production mode. */
|
|
44
|
+
async function buildDefines(pkgDir: string): Promise<Record<string, string>> {
|
|
45
|
+
const defines: Record<string, string> = {
|
|
46
|
+
'process.env.NODE_ENV': '"production"',
|
|
47
|
+
}
|
|
48
|
+
for (const name of ['.env', '.env.local']) {
|
|
49
|
+
const file = Bun.file(join(pkgDir, name))
|
|
50
|
+
if (!(await file.exists())) continue
|
|
51
|
+
for (const raw of (await file.text()).split('\n')) {
|
|
52
|
+
const line = raw.trim()
|
|
53
|
+
if (!line || line.startsWith('#')) continue
|
|
54
|
+
const eq = line.indexOf('=')
|
|
55
|
+
if (eq === -1) continue
|
|
56
|
+
const key = line
|
|
57
|
+
.slice(0, eq)
|
|
58
|
+
.replace(/^export\s+/, '')
|
|
59
|
+
.trim()
|
|
60
|
+
if (!key.startsWith('BUN_PUBLIC_')) continue
|
|
61
|
+
let value = line.slice(eq + 1).trim()
|
|
62
|
+
if (
|
|
63
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
64
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
65
|
+
) {
|
|
66
|
+
value = value.slice(1, -1)
|
|
67
|
+
}
|
|
68
|
+
defines[`process.env.${key}`] = JSON.stringify(value)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
72
|
+
if (key.startsWith('BUN_PUBLIC_') && value !== undefined) {
|
|
73
|
+
defines[`process.env.${key}`] = JSON.stringify(value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return defines
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readDesignTokens(): Promise<string> {
|
|
80
|
+
const parts = await Promise.all(
|
|
81
|
+
DS_TOKEN_FILES.map((f) => Bun.file(join(DS_DIR, f)).text()),
|
|
82
|
+
)
|
|
83
|
+
return parts.join('\n')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The Vitrine's own chrome stylesheet — chrome.css + every design-system
|
|
87
|
+
// component's co-located CSS + the primer chrome's CSS, read and concatenated in
|
|
88
|
+
// path-sorted order. Mirrors server.ts; the published documents inline it so the
|
|
89
|
+
// chrome paints before scripts (the components no longer inject at runtime).
|
|
90
|
+
async function readVitrineCss(): Promise<string> {
|
|
91
|
+
const componentFiles: string[] = []
|
|
92
|
+
for await (const f of new Bun.Glob('**/*.css').scan({
|
|
93
|
+
cwd: COMPONENTS_DIR,
|
|
94
|
+
absolute: true,
|
|
95
|
+
})) {
|
|
96
|
+
componentFiles.push(f)
|
|
97
|
+
}
|
|
98
|
+
componentFiles.sort()
|
|
99
|
+
const parts = await Promise.all(
|
|
100
|
+
[CHROME_CSS, ...componentFiles, PRIMER_CSS].map((f) => Bun.file(f).text()),
|
|
101
|
+
)
|
|
102
|
+
return parts.join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readGlobalCss(
|
|
106
|
+
pkgDir: string,
|
|
107
|
+
config: DisplayCaseConfig,
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
const parts: string[] = []
|
|
110
|
+
for (const rel of config.globalStyles ?? []) {
|
|
111
|
+
const abs = resolve(pkgDir, rel)
|
|
112
|
+
if (await Bun.file(abs).exists()) parts.push(await Bun.file(abs).text())
|
|
113
|
+
}
|
|
114
|
+
return parts.join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Map a built entry-point's output path back to its logical name. */
|
|
118
|
+
function entryName(path: string): 'browser' | 'render' | 'primer' | null {
|
|
119
|
+
const b = basename(path)
|
|
120
|
+
if (b.startsWith('browser-entry')) return 'browser'
|
|
121
|
+
if (b.startsWith('render-entry')) return 'render'
|
|
122
|
+
if (b.startsWith('primer-entry')) return 'primer'
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface BuildDescriptor {
|
|
127
|
+
title: string
|
|
128
|
+
base: string
|
|
129
|
+
a11y: false
|
|
130
|
+
hasPrimer: boolean
|
|
131
|
+
/** Content-hashed, base-prefixed entry URLs. */
|
|
132
|
+
assets: { browser: string; render: string; primer: string }
|
|
133
|
+
tokensCss: string
|
|
134
|
+
globalCss: string
|
|
135
|
+
vitrineCss: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function publish(
|
|
139
|
+
pkgDir: string,
|
|
140
|
+
opts: PublishOptions = {},
|
|
141
|
+
): Promise<{ out: string; descriptor: BuildDescriptor }> {
|
|
142
|
+
const out = resolve(opts.out ?? join(pkgDir, 'dist-showcase'))
|
|
143
|
+
const base = (opts.base ?? '').replace(/\/+$/, '')
|
|
144
|
+
const { config, configPath } = await resolveConfig(pkgDir)
|
|
145
|
+
|
|
146
|
+
await rm(out, { recursive: true, force: true })
|
|
147
|
+
await mkdir(join(out, 'assets'), { recursive: true })
|
|
148
|
+
await mkdir(join(out, 'server'), { recursive: true })
|
|
149
|
+
|
|
150
|
+
const files = await discoverCaseFiles(pkgDir, config)
|
|
151
|
+
const primerConfigured = !!config.primer
|
|
152
|
+
const primerSrc = primerConfigured
|
|
153
|
+
? resolve(pkgDir, config.primer as string)
|
|
154
|
+
: null
|
|
155
|
+
const hasPrimer = !!primerSrc && (await Bun.file(primerSrc).exists())
|
|
156
|
+
|
|
157
|
+
// Codegen the same entries the dev server uses.
|
|
158
|
+
const renderEntry = await codegenRenderEntry(pkgDir, files, configPath)
|
|
159
|
+
const primerEntry = hasPrimer
|
|
160
|
+
? await codegenPrimerEntry(pkgDir, config.primer as string)
|
|
161
|
+
: null
|
|
162
|
+
const ssrEntry = await codegenSsrEntry(pkgDir, files, configPath)
|
|
163
|
+
const ssrPrimerEntry = hasPrimer
|
|
164
|
+
? await codegenSsrPrimerEntry(pkgDir, config.primer as string, configPath)
|
|
165
|
+
: null
|
|
166
|
+
|
|
167
|
+
const defines = await buildDefines(pkgDir)
|
|
168
|
+
|
|
169
|
+
// Browser bundle: minified, content-hashed, production React.
|
|
170
|
+
const browserEntries = [BROWSER_ENTRY, renderEntry]
|
|
171
|
+
if (primerEntry) browserEntries.push(primerEntry)
|
|
172
|
+
const browser = await Bun.build({
|
|
173
|
+
entrypoints: browserEntries,
|
|
174
|
+
outdir: join(out, 'assets'),
|
|
175
|
+
target: 'browser',
|
|
176
|
+
minify: true,
|
|
177
|
+
sourcemap: 'none',
|
|
178
|
+
plugins: [mdxPlugin()],
|
|
179
|
+
define: defines,
|
|
180
|
+
naming: {
|
|
181
|
+
entry: '[name]-[hash].[ext]',
|
|
182
|
+
chunk: '[name]-[hash].[ext]',
|
|
183
|
+
asset: '[name]-[hash].[ext]',
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
if (!browser.success) {
|
|
187
|
+
for (const log of browser.logs) console.error(log)
|
|
188
|
+
throw new Error('Display Case publish: browser bundle failed')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const assets = { browser: '', render: '', primer: '' }
|
|
192
|
+
for (const o of browser.outputs) {
|
|
193
|
+
if (o.kind !== 'entry-point') continue
|
|
194
|
+
const name = entryName(o.path)
|
|
195
|
+
if (name) assets[name] = `${base}/assets/${basename(o.path)}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// SSR renderers for the production server: built once (no watching), imported
|
|
199
|
+
// by `prod-server`. React stays external (resolved at runtime).
|
|
200
|
+
const ssrEntries = [ssrEntry]
|
|
201
|
+
if (ssrPrimerEntry) ssrEntries.push(ssrPrimerEntry)
|
|
202
|
+
const ssr = await Bun.build({
|
|
203
|
+
entrypoints: ssrEntries,
|
|
204
|
+
outdir: join(out, 'server'),
|
|
205
|
+
target: 'bun',
|
|
206
|
+
plugins: [mdxPlugin()],
|
|
207
|
+
define: defines,
|
|
208
|
+
external: [
|
|
209
|
+
'react',
|
|
210
|
+
'react-dom',
|
|
211
|
+
'react-dom/server',
|
|
212
|
+
'react/jsx-runtime',
|
|
213
|
+
'react/jsx-dev-runtime',
|
|
214
|
+
],
|
|
215
|
+
naming: { entry: '[name].[ext]', chunk: '[name]-[hash].[ext]' },
|
|
216
|
+
})
|
|
217
|
+
if (!ssr.success) {
|
|
218
|
+
for (const log of ssr.logs) console.error(log)
|
|
219
|
+
throw new Error('Display Case publish: SSR bundle failed')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const manifest = await getManifest(pkgDir)
|
|
223
|
+
const descriptor: BuildDescriptor = {
|
|
224
|
+
title: config.title,
|
|
225
|
+
base,
|
|
226
|
+
a11y: false,
|
|
227
|
+
hasPrimer,
|
|
228
|
+
assets,
|
|
229
|
+
tokensCss: await readDesignTokens(),
|
|
230
|
+
globalCss: await readGlobalCss(pkgDir, config),
|
|
231
|
+
vitrineCss: await readVitrineCss(),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await Bun.write(
|
|
235
|
+
join(out, 'manifest.json'),
|
|
236
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
237
|
+
)
|
|
238
|
+
await Bun.write(
|
|
239
|
+
join(out, 'dc-build.json'),
|
|
240
|
+
`${JSON.stringify(descriptor, null, 2)}\n`,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// A standalone start script + run recipe, so the build deploys like any service.
|
|
244
|
+
await Bun.write(
|
|
245
|
+
join(out, 'server.ts'),
|
|
246
|
+
`// AUTO-GENERATED by display-case publish — do not edit.\n` +
|
|
247
|
+
`import { startProdServer } from 'display-case/prod-server'\n` +
|
|
248
|
+
`await startProdServer(import.meta.dir, { port: Number(process.env.PORT) || 3000 })\n`,
|
|
249
|
+
)
|
|
250
|
+
await Bun.write(
|
|
251
|
+
join(out, 'package.json'),
|
|
252
|
+
`${JSON.stringify(
|
|
253
|
+
{
|
|
254
|
+
name: 'display-case-showcase',
|
|
255
|
+
private: true,
|
|
256
|
+
type: 'module',
|
|
257
|
+
scripts: { start: 'bun server.ts' },
|
|
258
|
+
dependencies: {
|
|
259
|
+
'display-case': 'latest',
|
|
260
|
+
react: '^19',
|
|
261
|
+
'react-dom': '^19',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2,
|
|
266
|
+
)}\n`,
|
|
267
|
+
)
|
|
268
|
+
await Bun.write(
|
|
269
|
+
join(out, 'Dockerfile'),
|
|
270
|
+
[
|
|
271
|
+
'FROM oven/bun:1 AS base',
|
|
272
|
+
'WORKDIR /app',
|
|
273
|
+
'COPY . .',
|
|
274
|
+
'RUN bun install --production',
|
|
275
|
+
'ENV PORT=3000',
|
|
276
|
+
'EXPOSE 3000',
|
|
277
|
+
`HEALTHCHECK CMD bun -e "await fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"`,
|
|
278
|
+
'CMD ["bun", "server.ts"]',
|
|
279
|
+
'',
|
|
280
|
+
].join('\n'),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
// Drop the gitignored codegen leftovers out of the published tree's cache.
|
|
284
|
+
void cacheDir
|
|
285
|
+
|
|
286
|
+
if (opts.static) {
|
|
287
|
+
const { writeStaticExport } = await import('../server/prod-server')
|
|
288
|
+
await writeStaticExport(out)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { out, descriptor }
|
|
292
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { makeTempDir, writeFiles } from '../testing/test-helpers'
|
|
5
|
+
import { affectedComponents, importClosure } from './affected'
|
|
6
|
+
|
|
7
|
+
describe('importClosure', () => {
|
|
8
|
+
let dir: string
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
dir = await makeTempDir()
|
|
11
|
+
await writeFiles(dir, {
|
|
12
|
+
// Button: case → component → styles.css; styles.css → tokens.css.
|
|
13
|
+
'Button.case.tsx':
|
|
14
|
+
"import { Button } from './Button'\nexport default Button",
|
|
15
|
+
'Button.tsx': "import './styles.css'\nimport { tk } from './util'",
|
|
16
|
+
'styles.css': "@import './tokens.css';\n.b { color: red }",
|
|
17
|
+
'tokens.css': ':root { --x: 1 }',
|
|
18
|
+
'util.ts': "import React from 'react'\nexport const tk = 1",
|
|
19
|
+
// Unrelated island the Button does not import.
|
|
20
|
+
'Other.tsx': 'export const Other = 1',
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await rm(dir, { recursive: true, force: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('follows relative JS and CSS imports transitively', async () => {
|
|
28
|
+
const closure = await importClosure([join(dir, 'Button.case.tsx')])
|
|
29
|
+
expect(closure.has(resolve(dir, 'Button.case.tsx'))).toBe(true)
|
|
30
|
+
expect(closure.has(resolve(dir, 'Button.tsx'))).toBe(true)
|
|
31
|
+
expect(closure.has(resolve(dir, 'styles.css'))).toBe(true)
|
|
32
|
+
expect(closure.has(resolve(dir, 'tokens.css'))).toBe(true)
|
|
33
|
+
expect(closure.has(resolve(dir, 'util.ts'))).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('does not pull in unrelated files or bare specifiers', async () => {
|
|
37
|
+
const closure = await importClosure([join(dir, 'Button.case.tsx')])
|
|
38
|
+
expect(closure.has(resolve(dir, 'Other.tsx'))).toBe(false)
|
|
39
|
+
// `react` is a bare specifier — not traced, so nothing resolves for it.
|
|
40
|
+
for (const f of closure) expect(f.includes('node_modules')).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('a missing entry contributes only itself', async () => {
|
|
44
|
+
const ghost = join(dir, 'Nope.case.tsx')
|
|
45
|
+
const closure = await importClosure([ghost])
|
|
46
|
+
expect([...closure]).toEqual([resolve(ghost)])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('affectedComponents', () => {
|
|
51
|
+
let dir: string
|
|
52
|
+
let components: { id: string; caseFile: string }[]
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
dir = await makeTempDir()
|
|
55
|
+
await writeFiles(dir, {
|
|
56
|
+
'Button.case.tsx': "import './Button'",
|
|
57
|
+
'Button.tsx': "import './button.css'",
|
|
58
|
+
'button.css': '.b {}',
|
|
59
|
+
'Card.case.tsx': "import './Card'",
|
|
60
|
+
'Card.tsx': "import { tk } from './shared'",
|
|
61
|
+
'shared.ts': 'export const tk = 1',
|
|
62
|
+
})
|
|
63
|
+
components = [
|
|
64
|
+
{ id: 'button', caseFile: resolve(dir, 'Button.case.tsx') },
|
|
65
|
+
{ id: 'card', caseFile: resolve(dir, 'Card.case.tsx') },
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
await rm(dir, { recursive: true, force: true })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('a changed leaf affects only the component that imports it', async () => {
|
|
73
|
+
const affected = await affectedComponents(components, [
|
|
74
|
+
join(dir, 'button.css'),
|
|
75
|
+
])
|
|
76
|
+
expect([...affected]).toEqual(['button'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('a shared dependency affects every importer', async () => {
|
|
80
|
+
const affected = await affectedComponents(components, [
|
|
81
|
+
join(dir, 'shared.ts'),
|
|
82
|
+
])
|
|
83
|
+
expect([...affected]).toEqual(['card'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('a changed case file affects its own component', async () => {
|
|
87
|
+
const affected = await affectedComponents(components, [
|
|
88
|
+
join(dir, 'Card.case.tsx'),
|
|
89
|
+
])
|
|
90
|
+
expect([...affected]).toEqual(['card'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('an unrelated change affects nothing', async () => {
|
|
94
|
+
const affected = await affectedComponents(components, [
|
|
95
|
+
join(dir, 'README.md'),
|
|
96
|
+
])
|
|
97
|
+
expect(affected.size).toBe(0)
|
|
98
|
+
})
|
|
99
|
+
})
|