describe-me 0.3.0 → 0.5.0-next.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/README.md +3 -0
- package/package.json +8 -5
- package/src/data.ts +23 -9
- package/src/diagnostics-menu.ts +102 -0
- package/src/header.ts +3 -1
- package/src/load-style.ts +66 -0
- package/src/replay-cache.ts +4 -0
- package/src/resolve-asset-urls.ts +9 -0
- package/src/restore-styles.ts +44 -0
- package/src/serialized-node.ts +4 -0
- package/src/stage.ts +3 -2
- package/src/states-gallery.ts +3 -2
- package/src/style.css +98 -0
- package/src/without-empty-tests.ts +12 -0
- package/vite.config.ts +26 -2
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ writes, and replays the snapshots in a sandboxed iframe.
|
|
|
14
14
|
pnpm add -D describe-me @describe-me/vitest @describe-me/react
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
`vite` is a peer dependency (Vitest requires it anyway): pnpm and npm install
|
|
18
|
+
it automatically; with yarn, add `vite` to your `devDependencies`.
|
|
19
|
+
|
|
17
20
|
## Usage
|
|
18
21
|
|
|
19
22
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "describe-me",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-next.0",
|
|
4
4
|
"description": "Living component documentation generated from your Vitest tests: viewer and CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vitest",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"bugs": "https://github.com/grzehub/describe-me/issues",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": "^20.
|
|
24
|
+
"node": "^20.16.0 || >=22.4.0"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public",
|
|
@@ -42,11 +42,14 @@
|
|
|
42
42
|
"sideEffects": false,
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"rrweb-snapshot": "^2.1.6",
|
|
45
|
-
"
|
|
46
|
-
|
|
45
|
+
"@describe-me/core": "^0.5.0-next.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^22"
|
|
51
|
+
"@types/node": "^22",
|
|
52
|
+
"vite": "^8.3.0"
|
|
50
53
|
},
|
|
51
54
|
"scripts": {
|
|
52
55
|
"build": "rimraf dist-cli && tsc -p tsconfig.cli.json",
|
package/src/data.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
|
|
2
1
|
import type { Manifest } from '@describe-me/core/types'
|
|
3
2
|
import { allTests, currentTest, state } from './state.js'
|
|
4
3
|
import { rerender } from './rerender.js'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type SerializedNode
|
|
4
|
+
import { resolveAssetUrls } from './resolve-asset-urls.js'
|
|
5
|
+
import { restoreStyles } from './restore-styles.js'
|
|
6
|
+
import type { SerializedNode } from './serialized-node.js'
|
|
7
|
+
import { withoutEmptyTests } from './without-empty-tests.js'
|
|
8
8
|
|
|
9
9
|
const snapshotCache = new Map<string, Promise<SerializedNode>>()
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ export async function loadManifest(): Promise<void> {
|
|
|
18
18
|
throw new Error(`manifest: ${response.status}`)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const next = (await response.json()) as Manifest
|
|
21
|
+
const next = withoutEmptyTests((await response.json()) as Manifest)
|
|
22
22
|
if (state.manifest?.generatedAt === next.generatedAt) {
|
|
23
23
|
return
|
|
24
24
|
}
|
|
@@ -39,16 +39,30 @@ export async function loadManifest(): Promise<void> {
|
|
|
39
39
|
rerender()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Fetch one serialized DOM, memoized until the next manifest load. The promise
|
|
44
|
+
* resolves with asset URLs made absolute and stored stylesheets put back, so
|
|
45
|
+
* the stage and the gallery thumbnails always rebuild complete CSS and measure
|
|
46
|
+
* styled boxes.
|
|
47
|
+
*/
|
|
43
48
|
export function loadSnapshot(path: string): Promise<SerializedNode> {
|
|
44
49
|
let pending = snapshotCache.get(path)
|
|
45
50
|
if (!pending) {
|
|
46
|
-
pending = fetch(`__data/${path}`, { cache: 'no-store' })
|
|
47
|
-
(response) => response.
|
|
48
|
-
|
|
51
|
+
pending = fetch(`__data/${path}`, { cache: 'no-store' })
|
|
52
|
+
.then((response) => response.text())
|
|
53
|
+
.then((json) => parseSnapshot(json))
|
|
54
|
+
.then(async (node) => {
|
|
55
|
+
await restoreStyles(node)
|
|
56
|
+
|
|
57
|
+
return node
|
|
58
|
+
})
|
|
49
59
|
|
|
50
60
|
snapshotCache.set(path, pending)
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
return pending
|
|
54
64
|
}
|
|
65
|
+
|
|
66
|
+
function parseSnapshot(json: string): SerializedNode {
|
|
67
|
+
return JSON.parse(resolveAssetUrls(json)) as SerializedNode
|
|
68
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { manifestDiagnostics } from '@describe-me/core/diagnostics'
|
|
2
|
+
import type { Manifest } from '@describe-me/core/types'
|
|
3
|
+
import { el } from './el.js'
|
|
4
|
+
import { select } from './state.js'
|
|
5
|
+
|
|
6
|
+
/** Long lists are cut here; the rest is summed up in one line. */
|
|
7
|
+
const LIST_LIMIT = 40
|
|
8
|
+
|
|
9
|
+
function section(title: string, hint: string, items: HTMLElement[]): HTMLElement {
|
|
10
|
+
const shown = items.slice(0, LIST_LIMIT)
|
|
11
|
+
const rest = items.length - shown.length
|
|
12
|
+
const list = el('ul', { class: 'issues-list' }, ...shown)
|
|
13
|
+
|
|
14
|
+
if (rest > 0) {
|
|
15
|
+
list.append(el('li', { class: 'issues-more' }, `+${rest} more`))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return el(
|
|
19
|
+
'section',
|
|
20
|
+
{ class: 'issues-section' },
|
|
21
|
+
el('h3', {}, title, el('span', { class: 'issues-count' }, String(items.length))),
|
|
22
|
+
el('p', { class: 'issues-hint' }, hint),
|
|
23
|
+
list,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A header chip that opens the list of what this manifest could not document:
|
|
29
|
+
* anonymous components, components without props docs and assets that will not
|
|
30
|
+
* load. Absent when there is nothing to report.
|
|
31
|
+
*/
|
|
32
|
+
export function renderDiagnosticsMenu(manifest: Manifest): HTMLElement | null {
|
|
33
|
+
const { anonymous, undocumented, assetsMissing } = manifestDiagnostics(manifest)
|
|
34
|
+
const count = anonymous.length + undocumented.length + assetsMissing.length
|
|
35
|
+
|
|
36
|
+
if (count === 0) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sections: HTMLElement[] = []
|
|
41
|
+
|
|
42
|
+
if (anonymous.length > 0) {
|
|
43
|
+
const items = anonymous.map((test) =>
|
|
44
|
+
el(
|
|
45
|
+
'li',
|
|
46
|
+
{},
|
|
47
|
+
el('button', { class: 'issues-link', click: () => select(test.testId) }, test.fullName),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
sections.push(
|
|
52
|
+
section(
|
|
53
|
+
'Anonymous components',
|
|
54
|
+
'Export the component from a project module (it is named after its export) or give it a displayName.',
|
|
55
|
+
items,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (undocumented.length > 0) {
|
|
61
|
+
const items = undocumented.map((component) =>
|
|
62
|
+
el(
|
|
63
|
+
'li',
|
|
64
|
+
{},
|
|
65
|
+
el('span', { class: 'mono' }, component.name),
|
|
66
|
+
el('span', { class: 'issues-meta' }, `${component.tests} tests`),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
sections.push(
|
|
71
|
+
section(
|
|
72
|
+
'No props docs',
|
|
73
|
+
'Its props could not be read: is it exported from a TypeScript module, and is typescript installed?',
|
|
74
|
+
items,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (assetsMissing.length > 0) {
|
|
80
|
+
const items = assetsMissing.map((path) => el('li', { class: 'mono' }, path))
|
|
81
|
+
|
|
82
|
+
sections.push(
|
|
83
|
+
section(
|
|
84
|
+
'Assets not found',
|
|
85
|
+
'Not found under the project root or public/, so they will not load here.',
|
|
86
|
+
items,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return el(
|
|
92
|
+
'details',
|
|
93
|
+
{ class: 'issues' },
|
|
94
|
+
el(
|
|
95
|
+
'summary',
|
|
96
|
+
{ class: 'stat warn' },
|
|
97
|
+
el('b', {}, String(count)),
|
|
98
|
+
count === 1 ? 'issue' : 'issues',
|
|
99
|
+
),
|
|
100
|
+
el('div', { class: 'issues-panel' }, ...sections),
|
|
101
|
+
)
|
|
102
|
+
}
|
package/src/header.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderDiagnosticsMenu } from './diagnostics-menu.js'
|
|
1
2
|
import { el } from './el.js'
|
|
2
3
|
import { allTests, state } from './state.js'
|
|
3
4
|
|
|
@@ -5,7 +6,7 @@ function stat(count: number, label: string, tone?: 'pass' | 'fail'): HTMLElement
|
|
|
5
6
|
return el('span', { class: tone ? `stat ${tone}` : 'stat' }, el('b', {}, String(count)), label)
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
/** Wordmark, pass/fail counts and the time of the last run. */
|
|
9
|
+
/** Wordmark, pass/fail counts, what could not be documented and the time of the last run. */
|
|
9
10
|
export function renderHeader(): HTMLElement {
|
|
10
11
|
const manifest = state.manifest
|
|
11
12
|
const tests = manifest ? allTests(manifest) : []
|
|
@@ -26,6 +27,7 @@ export function renderHeader(): HTMLElement {
|
|
|
26
27
|
{ class: 'header' },
|
|
27
28
|
wordmark,
|
|
28
29
|
summary,
|
|
30
|
+
manifest && renderDiagnosticsMenu(manifest),
|
|
29
31
|
el('span', { class: 'right live' }, 'updated ', el('span', { class: 'mono' }, when)),
|
|
30
32
|
)
|
|
31
33
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolveAssetUrls } from './resolve-asset-urls.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chunk texts by hash, kept as promises so parallel thumbnails share one
|
|
5
|
+
* request. Never cleared, because a hash always names the same text.
|
|
6
|
+
*/
|
|
7
|
+
const chunks = new Map<string, Promise<string | null>>()
|
|
8
|
+
|
|
9
|
+
/** Joined sheets by reference, so every snapshot using a sheet shares one string. */
|
|
10
|
+
const sheets = new Map<string, Promise<string>>()
|
|
11
|
+
|
|
12
|
+
async function fetchChunk(hash: string): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
// Default caching: the name is a content hash, so a cached copy is always right.
|
|
15
|
+
const response = await fetch(`__data/styles/${hash}.css`)
|
|
16
|
+
|
|
17
|
+
return response.ok ? resolveAssetUrls(await response.text()) : null
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadChunk(hash: string): Promise<string | null> {
|
|
24
|
+
const memo = chunks.get(hash)
|
|
25
|
+
if (memo) {
|
|
26
|
+
return memo
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pending = fetchChunk(hash).then((css) => {
|
|
30
|
+
if (css === null) {
|
|
31
|
+
console.warn(`describe-me: style chunk styles/${hash}.css did not load`)
|
|
32
|
+
chunks.delete(hash)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return css
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
chunks.set(hash, pending)
|
|
39
|
+
|
|
40
|
+
return pending
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The stylesheet behind a reference, the text after `STYLE_URL_PREFIX`. A chunk
|
|
45
|
+
* that fails to load leaves a gap rather than failing the frame, and is fetched
|
|
46
|
+
* again next time.
|
|
47
|
+
*/
|
|
48
|
+
export function loadStyle(reference: string): Promise<string> {
|
|
49
|
+
const memo = sheets.get(reference)
|
|
50
|
+
if (memo) {
|
|
51
|
+
return memo
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const loads = reference.split('+').map((hash) => loadChunk(hash))
|
|
55
|
+
const pending = Promise.all(loads).then((texts) => {
|
|
56
|
+
if (texts.includes(null)) {
|
|
57
|
+
sheets.delete(reference)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return texts.map((text) => text ?? '').join('')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
sheets.set(reference, pending)
|
|
64
|
+
|
|
65
|
+
return pending
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ASSET_URL_PREFIX } from '@describe-me/core/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Make stored asset URLs absolute against the page, because a snapshot replays
|
|
5
|
+
* in a sandboxed iframe that has no base URL of its own.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveAssetUrls(text: string): string {
|
|
8
|
+
return text.replaceAll(ASSET_URL_PREFIX, new URL('__data/assets/', location.href).href)
|
|
9
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { STYLE_URL_PREFIX } from '@describe-me/core/types'
|
|
2
|
+
import { loadStyle } from './load-style.js'
|
|
3
|
+
import type { SerializedNode } from './serialized-node.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the walk reads. `SerializedNode` is `any` here: rrweb-snapshot's types
|
|
7
|
+
* import `@rrweb/types`, which it does not install.
|
|
8
|
+
*/
|
|
9
|
+
interface WalkedNode {
|
|
10
|
+
attributes?: Record<string, unknown>
|
|
11
|
+
childNodes?: WalkedNode[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface StoredSheet {
|
|
15
|
+
attributes: Record<string, unknown>
|
|
16
|
+
reference: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function storedSheets(node: WalkedNode, found: StoredSheet[] = []): StoredSheet[] {
|
|
20
|
+
const cssText = node.attributes?._cssText
|
|
21
|
+
if (node.attributes && typeof cssText === 'string' && cssText.startsWith(STYLE_URL_PREFIX)) {
|
|
22
|
+
found.push({ attributes: node.attributes, reference: cssText.slice(STYLE_URL_PREFIX.length) })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const child of node.childNodes ?? []) {
|
|
26
|
+
storedSheets(child, found)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return found
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Put every stylesheet a snapshot keeps in `styles/` back in place, before
|
|
34
|
+
* rrweb rebuilds it. Snapshots without references pass through unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export async function restoreStyles(node: SerializedNode): Promise<void> {
|
|
37
|
+
const stored = storedSheets(node)
|
|
38
|
+
|
|
39
|
+
await Promise.all(
|
|
40
|
+
stored.map(async ({ attributes, reference }) => {
|
|
41
|
+
attributes._cssText = await loadStyle(reference)
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
}
|
package/src/stage.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMirror, rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
|
|
2
2
|
import type { ManifestFrame } from '@describe-me/core/types'
|
|
3
3
|
import { loadSnapshot } from './data.js'
|
|
4
4
|
import { fitStage } from './fit-stage.js'
|
|
5
|
+
import { replayCache } from './replay-cache.js'
|
|
5
6
|
import { state } from './state.js'
|
|
6
7
|
|
|
7
8
|
let paintToken = 0
|
|
@@ -18,7 +19,7 @@ export async function paintFrame(stage: HTMLElement, frame: ManifestFrame): Prom
|
|
|
18
19
|
const { iframe } = rebuildIntoSandboxedIframe(node, {
|
|
19
20
|
root: stage,
|
|
20
21
|
iframeAttributes: { title: 'snapshot' },
|
|
21
|
-
cache:
|
|
22
|
+
cache: replayCache,
|
|
22
23
|
mirror: createMirror(),
|
|
23
24
|
hackCss: true,
|
|
24
25
|
})
|
package/src/states-gallery.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMirror, rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
|
|
2
2
|
import type { ManifestFrame, ManifestTest } from '@describe-me/core/types'
|
|
3
3
|
import { contentBox } from './content-box.js'
|
|
4
4
|
import { loadSnapshot } from './data.js'
|
|
5
5
|
import { el } from './el.js'
|
|
6
|
+
import { replayCache } from './replay-cache.js'
|
|
6
7
|
import { select } from './state.js'
|
|
7
8
|
|
|
8
9
|
/** Bumped on every gallery render, so snapshots that arrive late are dropped. */
|
|
@@ -17,7 +18,7 @@ async function paintThumb(thumb: HTMLElement, frame: ManifestFrame, token: numbe
|
|
|
17
18
|
const { iframe } = rebuildIntoSandboxedIframe(node, {
|
|
18
19
|
root: thumb,
|
|
19
20
|
iframeAttributes: { title: frame.label },
|
|
20
|
-
cache:
|
|
21
|
+
cache: replayCache,
|
|
21
22
|
mirror: createMirror(),
|
|
22
23
|
hackCss: true,
|
|
23
24
|
})
|
package/src/style.css
CHANGED
|
@@ -208,6 +208,104 @@ a {
|
|
|
208
208
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--pass) 18%, transparent);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/* ---------------------------------------------------------- diagnostics -- */
|
|
212
|
+
|
|
213
|
+
.issues {
|
|
214
|
+
position: relative;
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
}
|
|
217
|
+
.issues > summary {
|
|
218
|
+
padding: 2px 9px;
|
|
219
|
+
border-radius: var(--r-pill);
|
|
220
|
+
background: color-mix(in srgb, var(--warn) 12%, transparent);
|
|
221
|
+
color: var(--warn);
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
list-style: none;
|
|
224
|
+
}
|
|
225
|
+
.issues > summary::-webkit-details-marker {
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
.issues > summary b {
|
|
229
|
+
color: inherit;
|
|
230
|
+
}
|
|
231
|
+
.issues[open] > summary {
|
|
232
|
+
box-shadow: 0 0 0 2px var(--ring);
|
|
233
|
+
}
|
|
234
|
+
.issues-panel {
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: calc(100% + 8px);
|
|
237
|
+
left: 0;
|
|
238
|
+
z-index: 20;
|
|
239
|
+
width: 440px;
|
|
240
|
+
max-height: 70vh;
|
|
241
|
+
overflow: auto;
|
|
242
|
+
padding: 6px 0;
|
|
243
|
+
border: 1px solid var(--line);
|
|
244
|
+
border-radius: var(--r-lg);
|
|
245
|
+
background: var(--raised);
|
|
246
|
+
box-shadow: var(--shadow-md);
|
|
247
|
+
}
|
|
248
|
+
.issues-section {
|
|
249
|
+
padding: 10px 16px;
|
|
250
|
+
}
|
|
251
|
+
.issues-section + .issues-section {
|
|
252
|
+
border-top: 1px solid var(--line-soft);
|
|
253
|
+
}
|
|
254
|
+
.issues-section h3 {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: baseline;
|
|
257
|
+
gap: 8px;
|
|
258
|
+
margin: 0;
|
|
259
|
+
color: var(--ink);
|
|
260
|
+
font-size: 12px;
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
}
|
|
263
|
+
.issues-count {
|
|
264
|
+
color: var(--ink-3);
|
|
265
|
+
font-weight: 400;
|
|
266
|
+
font-variant-numeric: tabular-nums;
|
|
267
|
+
}
|
|
268
|
+
.issues-hint {
|
|
269
|
+
margin: 4px 0 8px;
|
|
270
|
+
color: var(--ink-3);
|
|
271
|
+
line-height: 1.45;
|
|
272
|
+
}
|
|
273
|
+
.issues-list {
|
|
274
|
+
display: grid;
|
|
275
|
+
gap: 2px;
|
|
276
|
+
margin: 0;
|
|
277
|
+
padding: 0;
|
|
278
|
+
list-style: none;
|
|
279
|
+
}
|
|
280
|
+
.issues-list li {
|
|
281
|
+
display: flex;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
gap: 12px;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
color: var(--ink-2);
|
|
286
|
+
text-overflow: ellipsis;
|
|
287
|
+
white-space: nowrap;
|
|
288
|
+
}
|
|
289
|
+
.issues-link {
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
padding: 2px 0;
|
|
292
|
+
border: 0;
|
|
293
|
+
background: none;
|
|
294
|
+
color: var(--accent);
|
|
295
|
+
font: inherit;
|
|
296
|
+
text-align: left;
|
|
297
|
+
text-overflow: ellipsis;
|
|
298
|
+
white-space: nowrap;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
}
|
|
301
|
+
.issues-link:hover {
|
|
302
|
+
text-decoration: underline;
|
|
303
|
+
}
|
|
304
|
+
.issues-meta,
|
|
305
|
+
.issues-more {
|
|
306
|
+
color: var(--ink-3);
|
|
307
|
+
}
|
|
308
|
+
|
|
211
309
|
/* -------------------------------------------------------------- sidebar -- */
|
|
212
310
|
|
|
213
311
|
.sidebar {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Manifest } from '@describe-me/core/types'
|
|
2
|
+
|
|
3
|
+
export function withoutEmptyTests(manifest: Manifest): Manifest {
|
|
4
|
+
const modules = manifest.modules
|
|
5
|
+
.map((mod) => ({
|
|
6
|
+
...mod,
|
|
7
|
+
tests: mod.tests.filter((test) => test.frames.some((frame) => frame.kind !== 'end')),
|
|
8
|
+
}))
|
|
9
|
+
.filter((mod) => mod.tests.length > 0)
|
|
10
|
+
|
|
11
|
+
return { ...manifest, modules }
|
|
12
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { cpSync, existsSync, readFileSync, statSync, watch } from 'node:fs'
|
|
2
|
-
import { dirname, join, resolve, sep } from 'node:path'
|
|
2
|
+
import { dirname, extname, join, resolve, sep } from 'node:path'
|
|
3
3
|
import { defineConfig, type Plugin } from 'vite'
|
|
4
4
|
|
|
5
|
+
/** Content types of what the data directory holds: the manifest, snapshots and copied project assets. */
|
|
6
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
7
|
+
'.json': 'application/json',
|
|
8
|
+
'.css': 'text/css',
|
|
9
|
+
'.svg': 'image/svg+xml',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.webp': 'image/webp',
|
|
15
|
+
'.avif': 'image/avif',
|
|
16
|
+
'.ico': 'image/x-icon',
|
|
17
|
+
'.woff': 'font/woff',
|
|
18
|
+
'.woff2': 'font/woff2',
|
|
19
|
+
'.ttf': 'font/ttf',
|
|
20
|
+
'.otf': 'font/otf',
|
|
21
|
+
'.mp4': 'video/mp4',
|
|
22
|
+
'.webm': 'video/webm',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function contentType(file: string): string {
|
|
26
|
+
return CONTENT_TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream'
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
/** Where the manifest and snapshots live. The CLI sets this; the default is the example project. */
|
|
6
30
|
function dataDir(): string {
|
|
7
31
|
return resolve(process.env.DESCRIBE_ME_DIR ?? '../../examples/react-browser/.describe-me')
|
|
@@ -60,7 +84,7 @@ function describeMeData(): Plugin {
|
|
|
60
84
|
return next()
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
response.setHeader('Content-Type',
|
|
87
|
+
response.setHeader('Content-Type', contentType(absolute))
|
|
64
88
|
response.setHeader('Cache-Control', 'no-store')
|
|
65
89
|
response.end(readFileSync(absolute))
|
|
66
90
|
})
|