@estiva-app/ui 0.21.1 → 0.22.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/dist/Avatar.d.ts +3 -0
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -0
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts +6 -0
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +3 -0
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +7 -0
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Skeleton.d.ts +2 -0
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/Toast.d.ts +5 -0
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +4 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +4 -0
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/registry/build-GOVLABI6.js +13 -0
- package/dist/registry/build-GOVLABI6.js.map +7 -0
- package/dist/registry/build.d.ts +29 -0
- package/dist/registry/build.d.ts.map +1 -0
- package/dist/registry/chunk-IJNCYVH4.js +163 -0
- package/dist/registry/chunk-IJNCYVH4.js.map +7 -0
- package/dist/registry/chunk-MRSBS5OP.js +99 -0
- package/dist/registry/chunk-MRSBS5OP.js.map +7 -0
- package/dist/registry/chunk-QDYGB3QN.js +352 -0
- package/dist/registry/chunk-QDYGB3QN.js.map +7 -0
- package/dist/registry/cli.d.ts +2 -0
- package/dist/registry/cli.d.ts.map +1 -0
- package/dist/registry/cli.js +121 -0
- package/dist/registry/cli.js.map +7 -0
- package/dist/registry/find.d.ts +43 -0
- package/dist/registry/find.d.ts.map +1 -0
- package/dist/registry/index.d.ts +14 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +26 -0
- package/dist/registry/index.js.map +7 -0
- package/dist/registry/schema.d.ts +147 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/package.json +15 -2
- package/registry.json +4470 -0
- package/src/Avatar.tsx +3 -0
- package/src/ChipInput.tsx +3 -0
- package/src/CommandPalette.tsx +6 -0
- package/src/IdentityMenu.tsx +3 -0
- package/src/Menu.tsx +7 -0
- package/src/Skeleton.tsx +2 -0
- package/src/Toast.tsx +5 -0
- package/src/Toolbar.tsx +4 -0
- package/src/Tooltip.tsx +4 -0
- package/src/registry/build.ts +590 -0
- package/src/registry/cli.ts +141 -0
- package/src/registry/find.ts +195 -0
- package/src/registry/index.ts +24 -0
- package/src/registry/registry.test.ts +448 -0
- package/src/registry/schema.ts +268 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* `estiva-ui` — the catalogue as a command (UIG-12; docs/GATES.md §23 for why
|
|
4
|
+
* it ships in the package rather than as a script pasted into each repo).
|
|
5
|
+
*
|
|
6
|
+
* estiva-ui find <words…> [--json] [--limit n] what do we have for this?
|
|
7
|
+
* estiva-ui build [--out <path>] write registry.json
|
|
8
|
+
* estiva-ui check rebuild and compare; CI runs this
|
|
9
|
+
*
|
|
10
|
+
* `find` reads the committed `registry.json`: the one in the folder it is run
|
|
11
|
+
* from, else the one shipped inside the installed package. So an app can ask
|
|
12
|
+
* the question without a checkout of this repo. `build` and `check` read the
|
|
13
|
+
* source, so they only mean anything inside the library they describe.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { resolve } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import { findInRegistry, formatFindings } from './find'
|
|
19
|
+
import { validateRegistry, type Registry } from './schema'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The builder, loaded only when it is used.
|
|
23
|
+
*
|
|
24
|
+
* It reads TypeScript with TypeScript, and `typescript` is a dev dependency of
|
|
25
|
+
* the app that installs this package — a real one in Peek, Ship and this repo,
|
|
26
|
+
* and possibly absent elsewhere. `find` needs none of it: it reads a JSON file.
|
|
27
|
+
* A static import made the bundler put the whole builder in the same chunk as
|
|
28
|
+
* `find`, so `npx estiva-ui find …` died with ERR_MODULE_NOT_FOUND before
|
|
29
|
+
* opening the file it needed.
|
|
30
|
+
*/
|
|
31
|
+
const builder = () => import('./build')
|
|
32
|
+
|
|
33
|
+
const [command, ...rest] = process.argv.slice(2)
|
|
34
|
+
const flag = (name: string) => rest.includes(`--${name}`)
|
|
35
|
+
const value = (name: string) => {
|
|
36
|
+
const at = rest.indexOf(`--${name}`)
|
|
37
|
+
return at === -1 ? undefined : rest[at + 1]
|
|
38
|
+
}
|
|
39
|
+
/** Everything that is not a flag or a flag's value: the words of the question. */
|
|
40
|
+
const plain = () => {
|
|
41
|
+
const out: string[] = []
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
if (rest[i].startsWith('--')) {
|
|
44
|
+
if (rest[i] === '--limit' || rest[i] === '--out' || rest[i] === '--file') i += 1
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
out.push(rest[i])
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The committed file: where it is run, else the copy inside the installed package. */
|
|
53
|
+
function registryPath(): string {
|
|
54
|
+
const named = value('file')
|
|
55
|
+
if (named) return resolve(named)
|
|
56
|
+
const here = resolve(process.cwd(), 'registry.json')
|
|
57
|
+
if (existsSync(here)) return here
|
|
58
|
+
// dist/registry/cli.js → the package's own root.
|
|
59
|
+
return fileURLToPath(new URL('../../registry.json', import.meta.url))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readRegistry(): Registry {
|
|
63
|
+
const path = registryPath()
|
|
64
|
+
if (!existsSync(path)) throw new Error(`no registry.json at ${path} — run "estiva-ui build" in the library first`)
|
|
65
|
+
const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry
|
|
66
|
+
const problems = validateRegistry(registry)
|
|
67
|
+
// A malformed catalogue is worse than none: it would answer, wrongly.
|
|
68
|
+
if (problems.length) throw new Error(`${path} does not match the schema:\n ${problems.join('\n ')}`)
|
|
69
|
+
return registry
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main(): Promise<number> {
|
|
73
|
+
switch (command) {
|
|
74
|
+
case 'find': {
|
|
75
|
+
const query = plain().join(' ')
|
|
76
|
+
if (!query.trim()) {
|
|
77
|
+
process.stderr.write('estiva-ui find <words…> — what the thing you need does, in words\n')
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
const registry = readRegistry()
|
|
81
|
+
const limit = Number(value('limit') ?? 5)
|
|
82
|
+
const findings = findInRegistry(registry, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })
|
|
83
|
+
if (flag('json')) {
|
|
84
|
+
process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\n`)
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`${formatFindings(registry, findings, query)}\n`)
|
|
88
|
+
// Nothing found is not a failure — it is the answer that sends a session
|
|
89
|
+
// to ask rather than to invent.
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'build': {
|
|
94
|
+
const { buildRegistry, serializeRegistry } = await builder()
|
|
95
|
+
const out = resolve(value('out') ?? 'registry.json')
|
|
96
|
+
const registry = buildRegistry()
|
|
97
|
+
const problems = validateRegistry(registry)
|
|
98
|
+
if (problems.length) {
|
|
99
|
+
process.stderr.write(`the registry this build produced does not match its own schema:\n ${problems.join('\n ')}\n`)
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(out, serializeRegistry(registry), 'utf8')
|
|
103
|
+
process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\n`)
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'check': {
|
|
108
|
+
const { buildRegistry, serializeRegistry } = await builder()
|
|
109
|
+
const path = registryPath()
|
|
110
|
+
const built = buildRegistry()
|
|
111
|
+
const problems = validateRegistry(built)
|
|
112
|
+
if (problems.length) {
|
|
113
|
+
process.stderr.write(`the registry does not match its own schema:\n ${problems.join('\n ')}\n`)
|
|
114
|
+
return 1
|
|
115
|
+
}
|
|
116
|
+
if (!existsSync(path)) {
|
|
117
|
+
process.stderr.write(`${path} is missing — run "npm run registry"\n`)
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
// Byte for byte. The point of the check is that the committed file cannot
|
|
121
|
+
// drift from the code; "near enough" is drift.
|
|
122
|
+
if (readFileSync(path, 'utf8').replace(/\r\n/g, '\n') !== serializeRegistry(built)) {
|
|
123
|
+
process.stderr.write(`${path} is not what the code produces — run "npm run registry" and commit the result\n`)
|
|
124
|
+
return 1
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\n`)
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
process.stderr.write('estiva-ui <find | build | check>\n')
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
process.exitCode = await main()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
140
|
+
process.exitCode = 1
|
|
141
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue's reader (UIG-12): `ui:find <words>`.
|
|
3
|
+
*
|
|
4
|
+
* It exists to prove the format. A schema nobody consumes is a schema nobody
|
|
5
|
+
* has tested, and the questions it has to answer are regression tests, not the
|
|
6
|
+
* scope: "floating panel" must find `Popover`, "scrolling" `ScrollArea`,
|
|
7
|
+
* "empty" `EmptyState`.
|
|
8
|
+
*
|
|
9
|
+
* It is also the answer to "what do we have for this?", which is the question
|
|
10
|
+
* the wall cannot answer: a lint rule refuses the wrong thing, and this is
|
|
11
|
+
* where the right one is named (UIG-20 makes a session ask it first).
|
|
12
|
+
*/
|
|
13
|
+
import type { Registry, RegistryEntry } from './schema'
|
|
14
|
+
|
|
15
|
+
export interface Finding {
|
|
16
|
+
entry: RegistryEntry
|
|
17
|
+
/** How many of the query's words matched anywhere. Ranked on this first. */
|
|
18
|
+
matched: number
|
|
19
|
+
score: number
|
|
20
|
+
/** Which fields matched, for the reader to say why. */
|
|
21
|
+
where: string[]
|
|
22
|
+
/**
|
|
23
|
+
* The props the query named **by name**, so the answer shows those rather
|
|
24
|
+
* than all of them. A prop whose note merely carries one of the words counts
|
|
25
|
+
* towards the score and is not shown: on "floating panel", six of `Popover`'s
|
|
26
|
+
* notes say "panel", and printing all six buries the answer.
|
|
27
|
+
*/
|
|
28
|
+
props: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Words that are in nearly every sentence, and so tell a search nothing.
|
|
33
|
+
*
|
|
34
|
+
* Measured: `ui:find "truncate a long link"` printed `Link`'s `external` prop,
|
|
35
|
+
* because its note says "in **a** new tab" and the one-letter word matched
|
|
36
|
+
* itself exactly. The minimum length in `alike` only guards a *prefix* match;
|
|
37
|
+
* an exact match short-circuits it, which is right for `tab` and useless for `a`.
|
|
38
|
+
*/
|
|
39
|
+
const STOP = new Set([
|
|
40
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'can', 'do', 'for', 'from', 'how', 'i', 'if', 'in', 'is', 'it', 'its', 'me', 'my', 'no', 'not', 'of',
|
|
41
|
+
'on', 'or', 'our', 'so', 'that', 'the', 'their', 'them', 'then', 'there', 'they', 'this', 'to', 'up', 'was', 'we', 'what', 'when', 'which', 'with', 'you', 'your',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The words of a piece of text, with names taken apart.
|
|
46
|
+
*
|
|
47
|
+
* `CommandPalette` is kept whole *and* split, so "palette" reaches it and
|
|
48
|
+
* "commandpalette" still does. Prose is already words; this costs it nothing.
|
|
49
|
+
*/
|
|
50
|
+
function words(text: string): string[] {
|
|
51
|
+
const found: string[] = []
|
|
52
|
+
for (const token of text.match(/[A-Za-z0-9]+/g) ?? []) {
|
|
53
|
+
found.push(token.toLowerCase())
|
|
54
|
+
const parts = token.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)
|
|
55
|
+
if (parts.length > 1) found.push(...parts.map((part) => part.toLowerCase()))
|
|
56
|
+
}
|
|
57
|
+
return found
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a word of the query means a word of the text.
|
|
62
|
+
*
|
|
63
|
+
* Either being a prefix of the other covers "scroll" against "scrolling"; the
|
|
64
|
+
* five-letter key covers "scrolls" against "scrolling", where neither is. Crude
|
|
65
|
+
* on purpose — a stemmer is a dependency and a surprise, and the failure here
|
|
66
|
+
* is an extra row in a list of five, not a wrong answer.
|
|
67
|
+
*
|
|
68
|
+
* **Three letters at least**, or a prefix match is no match at all: `person's`
|
|
69
|
+
* puts the word "s" in the text, and every query on earth starts with some
|
|
70
|
+
* letter. Measured — before this, "scrolling" found `Avatar`.
|
|
71
|
+
*/
|
|
72
|
+
function alike(query: string, text: string): boolean {
|
|
73
|
+
if (query === text) return true
|
|
74
|
+
if (Math.min(query.length, text.length) < 3) return false
|
|
75
|
+
if (text.startsWith(query) || query.startsWith(text)) return true
|
|
76
|
+
return query.slice(0, 5) === text.slice(0, 5)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hit = (query: string, text: string) => words(text).some((word) => alike(query, word))
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Search name, purpose, behaviours and variants.
|
|
83
|
+
*
|
|
84
|
+
* A row that answers more of the question comes first, whatever it scored:
|
|
85
|
+
* "floating panel" has to reach `Popover`, whose purpose carries both words,
|
|
86
|
+
* over `MenuPanel`, whose name carries one of them loudly.
|
|
87
|
+
*/
|
|
88
|
+
export function findInRegistry(registry: Registry, query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {
|
|
89
|
+
const all = words(query)
|
|
90
|
+
// Drop the words that mean nothing. If the question was only those, keep them
|
|
91
|
+
// rather than answer nothing at all.
|
|
92
|
+
const carrying = all.filter((word) => word.length >= 3 && !STOP.has(word))
|
|
93
|
+
const asked = carrying.length ? carrying : all
|
|
94
|
+
if (asked.length === 0) return []
|
|
95
|
+
|
|
96
|
+
const findings: Finding[] = []
|
|
97
|
+
for (const entry of registry.entries) {
|
|
98
|
+
let score = 0
|
|
99
|
+
let matched = 0
|
|
100
|
+
const where = new Set<string>()
|
|
101
|
+
const byName = new Set<string>()
|
|
102
|
+
const byNote = new Set<string>()
|
|
103
|
+
for (const word of asked) {
|
|
104
|
+
let best = 0
|
|
105
|
+
if (entry.name.toLowerCase() === word) {
|
|
106
|
+
best = 100
|
|
107
|
+
where.add('name')
|
|
108
|
+
} else if (hit(word, entry.name)) {
|
|
109
|
+
best = 40
|
|
110
|
+
where.add('name')
|
|
111
|
+
}
|
|
112
|
+
if (hit(word, entry.purpose)) {
|
|
113
|
+
best = Math.max(best, 20)
|
|
114
|
+
where.add('purpose')
|
|
115
|
+
}
|
|
116
|
+
for (const owned of entry.ownsBehaviours) {
|
|
117
|
+
if (hit(word, owned.behaviour) || hit(word, owned.id)) {
|
|
118
|
+
best = Math.max(best, 15)
|
|
119
|
+
where.add('behaviour')
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const variant of entry.variants) {
|
|
123
|
+
if (variant.values.some((value) => hit(word, value)) || hit(word, variant.prop)) {
|
|
124
|
+
best = Math.max(best, 8)
|
|
125
|
+
where.add('variant')
|
|
126
|
+
byName.add(variant.prop)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// What it can do. "does Link already truncate?" is the question this
|
|
130
|
+
// answers, and answering it is what stops the thing being built twice.
|
|
131
|
+
for (const prop of entry.props) {
|
|
132
|
+
// A prop the question named by name is the answer; one whose note
|
|
133
|
+
// happens to carry the word is a hint. Named ones go first, so a short
|
|
134
|
+
// answer shows the right lines.
|
|
135
|
+
if (hit(word, prop.name)) {
|
|
136
|
+
best = Math.max(best, 14)
|
|
137
|
+
where.add('prop')
|
|
138
|
+
byName.add(prop.name)
|
|
139
|
+
} else if (prop.note !== null && hit(word, prop.note)) {
|
|
140
|
+
best = Math.max(best, 10)
|
|
141
|
+
where.add('prop')
|
|
142
|
+
byNote.add(prop.name)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (best > 0) matched += 1
|
|
146
|
+
score += best
|
|
147
|
+
}
|
|
148
|
+
if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
findings.sort((a, b) => b.matched - a.matched || b.score - a.score || a.entry.name.localeCompare(b.entry.name))
|
|
152
|
+
return findings.slice(0, limit)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `http://localhost:6008/?path=/docs/overlays-popover--docs`, or `null` where the entry has no page. */
|
|
156
|
+
export function docsLink(registry: Registry, entry: RegistryEntry): string | null {
|
|
157
|
+
if (!entry.docsId) return null
|
|
158
|
+
return registry.storybook.devUrl + registry.storybook.docsPath.replace('{docsId}', entry.docsId)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** What the command prints: the import line first, because that is what the reader came for. */
|
|
162
|
+
export function formatFindings(registry: Registry, findings: Finding[], query: string): string {
|
|
163
|
+
if (findings.length === 0) {
|
|
164
|
+
return [
|
|
165
|
+
`Nothing in ${registry.builtFrom.repo} matches "${query}".`,
|
|
166
|
+
'If nothing here does what you need, say so and ask — do not invent a component.',
|
|
167
|
+
].join('\n')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const blocks = findings.map((finding) => {
|
|
171
|
+
const { entry } = finding
|
|
172
|
+
const lines = [
|
|
173
|
+
`${entry.name} · ${entry.kind} · ${entry.repo} (matched ${finding.where.join(', ')})`,
|
|
174
|
+
` ${entry.purpose}`,
|
|
175
|
+
` import { ${entry.name} } from '${entry.importPath}'`,
|
|
176
|
+
]
|
|
177
|
+
if (entry.ownsBehaviours.length) lines.push(` owns: ${entry.ownsBehaviours.map((owned) => owned.behaviour).join(' · ')}`)
|
|
178
|
+
if (entry.variants.length) lines.push(` ${entry.variants.map((variant) => `${variant.prop}: ${variant.values.join(' | ')}`).join(' ')}`)
|
|
179
|
+
// The props the question named, with their own line — not all of them. A
|
|
180
|
+
// component with twenty props would bury its own answer.
|
|
181
|
+
const shown = finding.props
|
|
182
|
+
.filter((name) => !entry.variants.some((variant) => variant.prop === name))
|
|
183
|
+
.slice(0, 3)
|
|
184
|
+
.map((name) => entry.props.find((prop) => prop.name === name))
|
|
185
|
+
for (const prop of shown) {
|
|
186
|
+
if (prop) lines.push(` ${prop.name}: ${prop.takes}${prop.note ? ` — ${prop.note}` : ''}`)
|
|
187
|
+
}
|
|
188
|
+
if (entry.props.length) lines.push(` ${entry.props.length} props in all; see the page for the rest`)
|
|
189
|
+
const link = docsLink(registry, entry)
|
|
190
|
+
if (link) lines.push(` ${link}`)
|
|
191
|
+
return lines.join('\n')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return blocks.join('\n\n')
|
|
195
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@estiva-app/ui/registry` — the component catalogue (UIG-12, seam S2).
|
|
3
|
+
*
|
|
4
|
+
* The one copy, here, as every gate piece is (docs/GATES.md §23): the skill
|
|
5
|
+
* (UIG-20), the copy detector (UIG-25) and the merged search (UIG-19) read
|
|
6
|
+
* this, and no repo keeps a parser of its own.
|
|
7
|
+
*
|
|
8
|
+
* The data itself is `registry.json` at the package's root, committed and
|
|
9
|
+
* shipped. Read it with `estiva-ui find`, or import these to read it yourself.
|
|
10
|
+
*/
|
|
11
|
+
export { buildRegistry, readIndexExports, serializeRegistry, type BuildOptions } from './build'
|
|
12
|
+
export { docsLink, findInRegistry, formatFindings, type Finding } from './find'
|
|
13
|
+
export {
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
validateRegistry,
|
|
16
|
+
type EntryBehaviour,
|
|
17
|
+
type EntryKind,
|
|
18
|
+
type EntryStatus,
|
|
19
|
+
type EntryVariant,
|
|
20
|
+
type PurposeSource,
|
|
21
|
+
type Registry,
|
|
22
|
+
type RegistryEntry,
|
|
23
|
+
type RegistryExclusion,
|
|
24
|
+
} from './schema'
|