@estiva-app/ui 0.35.1 → 0.35.2
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/gates/{chunk-GI6M4P7H.js → chunk-DBMUQAR6.js} +4 -3
- package/dist/gates/chunk-DBMUQAR6.js.map +7 -0
- package/dist/gates/cli.js +1 -1
- package/dist/gates/gate-config.d.ts.map +1 -1
- package/dist/gates/hook.d.ts.map +1 -1
- package/dist/gates/index.js +5 -3
- package/dist/gates/index.js.map +2 -2
- package/dist/registry/{chunk-KMZIMB3G.js → chunk-5K2TRR4X.js} +30 -2
- package/dist/registry/chunk-5K2TRR4X.js.map +7 -0
- package/dist/registry/cli.js +89 -31
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/contract.d.ts +14 -0
- package/dist/registry/contract.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/siblings.d.ts +9 -0
- package/dist/registry/siblings.d.ts.map +1 -1
- package/dist/registry/storymap.d.ts +19 -5
- package/dist/registry/storymap.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/gates/gate-config.ts +4 -2
- package/src/gates/gates.test.ts +7 -0
- package/src/gates/hook.ts +6 -2
- package/src/pages.test.ts +11 -0
- package/src/registry/cli.ts +34 -17
- package/src/registry/contract.test.ts +37 -2
- package/src/registry/contract.ts +49 -1
- package/src/registry/siblings.test.ts +24 -1
- package/src/registry/siblings.ts +23 -0
- package/src/registry/storymap.test.ts +52 -31
- package/src/registry/storymap.ts +76 -37
- package/stories/Choosing.mdx +8 -2
- package/dist/gates/chunk-GI6M4P7H.js.map +0 -7
- package/dist/registry/chunk-KMZIMB3G.js.map +0 -7
|
@@ -15,7 +15,8 @@ function pageProblem(source) {
|
|
|
15
15
|
if (at.some((i, n) => n > 0 && i < at[n - 1])) return `sections out of order: they go ${PAGE_SECTIONS.join(", ")}`;
|
|
16
16
|
const opening = text.split(/^# .*$/m)[1]?.split(/^##\s/m)[0]?.split(/\n\s*\n/).map((s) => s.trim()).find((s) => s && !s.startsWith("<") && !s.startsWith("import "));
|
|
17
17
|
if (!opening) return "no opening line under the title saying what the part is";
|
|
18
|
-
|
|
18
|
+
const how = text.split(/^##\s+How\s*$/m)[1]?.split(/^##\s/m)[0] ?? "";
|
|
19
|
+
if (!/```tsx?\n/.test(how)) return 'no code under "How"';
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
21
22
|
function hasSeenIn(source) {
|
|
@@ -51,6 +52,32 @@ function contractProblems(registry, root) {
|
|
|
51
52
|
}
|
|
52
53
|
return problems;
|
|
53
54
|
}
|
|
55
|
+
var toStoryId = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
56
|
+
function namedParts(section) {
|
|
57
|
+
return [...section.matchAll(/\*\*([A-Z][A-Za-z0-9]+)\*\*|`([A-Z][A-Za-z0-9]+)`/g)].map((m) => m[1] ?? m[2]);
|
|
58
|
+
}
|
|
59
|
+
function nameProblems(registry, root, { known, titles, packageIds = [] }) {
|
|
60
|
+
const problems = [];
|
|
61
|
+
for (const entry of registry.entries) {
|
|
62
|
+
if (!entry.docPage || !existsSync(join(root, entry.docPage))) continue;
|
|
63
|
+
const text = readFileSync(join(root, entry.docPage), "utf8").replace(/\r\n/g, "\n");
|
|
64
|
+
for (const heading of ["When", "When not"]) {
|
|
65
|
+
const section = text.split(new RegExp(`^##\\s+${heading}\\s*$`, "m"))[1]?.split(/^##\s/m)[0] ?? "";
|
|
66
|
+
for (const name of new Set(namedParts(section).filter((n) => !known.has(n)))) {
|
|
67
|
+
problems.push(`${entry.docPage}: "${heading}" names ${name}, which is no part of the package or this app`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!titles) continue;
|
|
71
|
+
const seenIn = text.match(/^\*\*Seen in\*\*[^\n]*(?:\n(?!\n)[^\n]*)*/m)?.[0] ?? "";
|
|
72
|
+
for (const [, raw] of seenIn.matchAll(/\*([A-Z][^*/]*\/[^*]+)\*/g)) {
|
|
73
|
+
const title = raw.replace(/\s+/g, " ").replace(/\s*\/\s*/g, "/").trim();
|
|
74
|
+
const id = toStoryId(title);
|
|
75
|
+
if (titles.has(title) || packageIds.some((p) => p.startsWith(`${id}--`))) continue;
|
|
76
|
+
problems.push(`${entry.docPage}: "Seen in" names ${title}, which no Storybook has`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return problems;
|
|
80
|
+
}
|
|
54
81
|
|
|
55
82
|
// src/registry/find.ts
|
|
56
83
|
var STOP = /* @__PURE__ */ new Set([
|
|
@@ -241,9 +268,10 @@ export {
|
|
|
241
268
|
hasSeenIn,
|
|
242
269
|
linkProblems,
|
|
243
270
|
contractProblems,
|
|
271
|
+
nameProblems,
|
|
244
272
|
findInRegistry,
|
|
245
273
|
findInRegistries,
|
|
246
274
|
docsLink,
|
|
247
275
|
formatFindings
|
|
248
276
|
};
|
|
249
|
-
//# sourceMappingURL=chunk-
|
|
277
|
+
//# sourceMappingURL=chunk-5K2TRR4X.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/registry/contract.ts", "../../src/registry/find.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The usage-page contract, checked against an app's catalogue (UIG-19).\n *\n * Every part an app classifies **reusable** (or **promote-candidate**) has a\n * page beside it \u2014 `<PartName>.mdx` in the component's own folder \u2014 that opens\n * with a line saying what the part is, then `When`, `When not`, `How` (with\n * code) and `What it owns`, in that order. And it is drawn somewhere: a story\n * of its own, or a **Seen in** line on its page naming the stories that draw\n * it, or saying why none can (Katerina, 21 and 23 September 2026). A one-off\n * needs only its class and a purpose line, which the catalogue already refuses\n * to build without.\n *\n * This is the one copy. Peek and Ship each used to carry their own, pasted into\n * their `scripts/gates-checks.mjs` and run only by `gates:status`, so a page\n * could lose its `When not` and still merge. `estiva-ui check` runs this in\n * every app's `gate` job (docs/GATES.md \u00A723: one copy, in the package).\n *\n * The package's own pages keep the fuller contract in `src/pages.test.ts`,\n * which also reads `What it owns` back against the lint rule.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Registry } from './schema'\n\n/** The four headings, in the order a page carries them. */\nexport const PAGE_SECTIONS = ['When', 'When not', 'How', 'What it owns'] as const\n\n/** The classes that owe a page and a picture. */\nexport const CONTRACT_KINDS = ['reusable', 'promote-candidate'] as const\n\n/** The line that stands in for a story of the part's own. */\nconst SEEN_IN = /^\\*\\*Seen in\\*\\*/m\n\n/**\n * Why a page breaks the contract, or null when it keeps it.\n *\n * Read on normalised text: the apps are checked out with CRLF on Windows.\n */\nexport function pageProblem(source: string): string | null {\n const text = source.replace(/\\r\\n/g, '\\n')\n const heads = [...text.matchAll(/^##+\\s+(.+?)\\s*$/gm)].map((m) => m[1])\n const at = PAGE_SECTIONS.map((s) => heads.indexOf(s))\n const missing = PAGE_SECTIONS.filter((_, i) => at[i] < 0)\n if (missing.length) return `no ${missing.map((s) => `\"${s}\"`).join(', ')} section`\n if (at.some((i, n) => n > 0 && i < at[n - 1])) return `sections out of order: they go ${PAGE_SECTIONS.join(', ')}`\n // The opening line: the first paragraph under the title that is not markup.\n const opening = text\n .split(/^# .*$/m)[1]\n ?.split(/^##\\s/m)[0]\n ?.split(/\\n\\s*\\n/)\n .map((s) => s.trim())\n .find((s) => s && !s.startsWith('<') && !s.startsWith('import '))\n if (!opening) return 'no opening line under the title saying what the part is'\n // Only the How section: the code block must sit under How, not in a later section\n // (a page with none under How and one under What it owns passed, R18).\n const how = text.split(/^##\\s+How\\s*$/m)[1]?.split(/^##\\s/m)[0] ?? ''\n if (!/```tsx?\\n/.test(how)) return 'no code under \"How\"'\n return null\n}\n\n/** Whether a page records where the part is drawn, in place of a story of its own. */\nexport function hasSeenIn(source: string): boolean {\n return SEEN_IN.test(source.replace(/\\r\\n/g, '\\n'))\n}\n\n/**\n * Every catalogue link that names a story or a docs page its Storybook does not\n * have (UIG-19: a dead link is a build failure, not a dead end found six weeks\n * later). `ids` is every entry id in the Storybook's own index \u2014 what\n * `storybook index` writes \u2014 so this is Storybook's answer, not a guess at how\n * it builds an id.\n */\nexport function linkProblems(registry: Registry, ids: ReadonlySet<string>): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n for (const key of ['storyId', 'docsId'] as const) {\n const id = entry[key]\n if (id && !ids.has(id)) problems.push(`${entry.name} (${entry.sourceFile}) links to ${key === 'storyId' ? 'a story' : 'a docs page'} its Storybook does not have: ${id}`)\n }\n }\n return problems\n}\n\n/**\n * Every way an app's catalogue breaks the contract, one line each, naming the\n * part, its file and what to do. Empty when it keeps it.\n *\n * `root` is the app's own folder: the one its catalogue's paths are relative to.\n */\nexport function contractProblems(registry: Registry, root: string): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n const cls = entry.app?.class\n if (!cls || !(CONTRACT_KINDS as readonly string[]).includes(cls)) continue\n // Where the page must be: beside the component, named after the part \u2014\n // the same place the catalogue looks (src/registry/app.ts).\n const page = join(entry.sourceFile.replace(/[^/\\\\]+$/, ''), `${entry.name}.mdx`).replace(/\\\\/g, '/')\n const where = `${entry.name} (${cls}, ${entry.sourceFile})`\n if (!existsSync(join(root, page))) {\n problems.push(`${where} has no usage page: write ${page}, with an opening line, then ${PAGE_SECTIONS.join(', ')}`)\n continue\n }\n const text = readFileSync(join(root, page), 'utf8')\n const problem = pageProblem(text)\n if (problem) problems.push(`${where}: ${page} has ${problem}`)\n if (entry.storyId === null && !hasSeenIn(text)) {\n problems.push(`${where} is drawn nowhere: give it a story, or write a \"**Seen in**\" line on ${page} naming the stories that draw it, or why none can`)\n }\n }\n return problems\n}\n\n/** Storybook's id for a title: lower case, every run of other characters one hyphen. */\nconst toStoryId = (title: string) =>\n title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\n/** The names a page's section writes as a part: bold or in backticks, starting with a capital. */\nfunction namedParts(section: string): string[] {\n return [...section.matchAll(/\\*\\*([A-Z][A-Za-z0-9]+)\\*\\*|`([A-Z][A-Za-z0-9]+)`/g)].map((m) => m[1] ?? m[2])\n}\n\n/**\n * What an app's usage pages name that is not there (the re-review after the audit\n * before UIG-26, R14): a part named under When or When not that no catalogue has \u2014\n * Peek's StartTopicDialog sent readers to CreateTopicDialog, deleted by FOL-23 \u2014\n * and a story a **Seen in** line names that no Storybook has. `known` is every part\n * of the package and the app; `titles` is the app's Storybook titles, when there\n * is an index to read them from; `packageIds` the package's story and docs ids, so\n * a page may send readers to the package's Storybook too.\n */\nexport function nameProblems(registry: Registry, root: string, { known, titles, packageIds = [] }: { known: Set<string>; titles?: Set<string>; packageIds?: string[] }): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n if (!entry.docPage || !existsSync(join(root, entry.docPage))) continue\n const text = readFileSync(join(root, entry.docPage), 'utf8').replace(/\\r\\n/g, '\\n')\n for (const heading of ['When', 'When not']) {\n const section = text.split(new RegExp(`^##\\\\s+${heading}\\\\s*$`, 'm'))[1]?.split(/^##\\s/m)[0] ?? ''\n for (const name of new Set(namedParts(section).filter((n) => !known.has(n)))) {\n problems.push(`${entry.docPage}: \"${heading}\" names ${name}, which is no part of the package or this app`)\n }\n }\n if (!titles) continue\n const seenIn = text.match(/^\\*\\*Seen in\\*\\*[^\\n]*(?:\\n(?!\\n)[^\\n]*)*/m)?.[0] ?? ''\n for (const [, raw] of seenIn.matchAll(/\\*([A-Z][^*/]*\\/[^*]+)\\*/g)) {\n // A title may wrap across lines, and a line may break after its slash.\n const title = raw.replace(/\\s+/g, ' ').replace(/\\s*\\/\\s*/g, '/').trim()\n const id = toStoryId(title)\n if (titles.has(title) || packageIds.some((p) => p.startsWith(`${id}--`))) continue\n problems.push(`${entry.docPage}: \"Seen in\" names ${title}, which no Storybook has`)\n }\n }\n return problems\n}\n", "/**\n * The catalogue's reader (UIG-12): `ui:find <words>`.\n *\n * It exists to prove the format. A schema nobody consumes is a schema nobody\n * has tested, and the questions it has to answer are regression tests, not the\n * scope: \"floating panel\" must find `Popover`, \"scrolling\" `ScrollArea`,\n * \"empty\" `EmptyState`.\n *\n * It is also the answer to \"what do we have for this?\", which is the question\n * the wall cannot answer: a lint rule refuses the wrong thing, and this is\n * where the right one is named (UIG-20 makes a session ask it first).\n */\nimport type { Registry, RegistryEntry } from './schema'\n\nexport interface Finding {\n entry: RegistryEntry\n /** How many of the query's words matched anywhere. Ranked on this first. */\n matched: number\n score: number\n /** Which fields matched, for the reader to say why. */\n where: string[]\n /**\n * The props the query named **by name**, so the answer shows those rather\n * than all of them. A prop whose note merely carries one of the words counts\n * towards the score and is not shown: on \"floating panel\", six of `Popover`'s\n * notes say \"panel\", and printing all six buries the answer.\n */\n props: string[]\n}\n\n/**\n * Words that are in nearly every sentence, and so tell a search nothing.\n *\n * Measured: `ui:find \"truncate a long link\"` printed `Link`'s `external` prop,\n * because its note says \"in **a** new tab\" and the one-letter word matched\n * itself exactly. The minimum length in `alike` only guards a *prefix* match;\n * an exact match short-circuits it, which is right for `tab` and useless for `a`.\n */\nconst STOP = new Set([\n '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',\n 'on', 'or', 'our', 'so', 'that', 'the', 'their', 'them', 'then', 'there', 'they', 'this', 'to', 'up', 'was', 'we', 'what', 'when', 'which', 'with', 'you', 'your',\n])\n\n/**\n * The words of a piece of text, with names taken apart.\n *\n * `CommandPalette` is kept whole *and* split, so \"palette\" reaches it and\n * \"commandpalette\" still does. Prose is already words; this costs it nothing.\n */\nfunction words(text: string): string[] {\n const found: string[] = []\n for (const token of text.match(/[A-Za-z0-9]+/g) ?? []) {\n found.push(token.toLowerCase())\n const parts = token.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)\n if (parts.length > 1) found.push(...parts.map((part) => part.toLowerCase()))\n }\n return found\n}\n\n/**\n * Whether a word of the query means a word of the text.\n *\n * Either being a prefix of the other covers \"scroll\" against \"scrolling\"; the\n * five-letter key covers \"scrolls\" against \"scrolling\", where neither is. Crude\n * on purpose \u2014 a stemmer is a dependency and a surprise, and the failure here\n * is an extra row in a list of five, not a wrong answer.\n *\n * **Three letters at least**, or a prefix match is no match at all: `person's`\n * puts the word \"s\" in the text, and every query on earth starts with some\n * letter. Measured \u2014 before this, \"scrolling\" found `Avatar`.\n */\nfunction alike(query: string, text: string): boolean {\n if (query === text) return true\n if (Math.min(query.length, text.length) < 3) return false\n if (text.startsWith(query) || query.startsWith(text)) return true\n return query.slice(0, 5) === text.slice(0, 5)\n}\n\nconst hit = (query: string, text: string) => words(text).some((word) => alike(query, word))\n\n/**\n * Search name, purpose, behaviours and variants.\n *\n * A row that answers more of the question comes first, whatever it scored:\n * \"floating panel\" has to reach `Popover`, whose purpose carries both words,\n * over `MenuPanel`, whose name carries one of them loudly.\n */\nexport function findInRegistry(registry: Registry, query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n return findInRegistries([registry], query, { limit })\n}\n\n/**\n * Search several catalogues as one: the package's, the app's own, and any app\n * beside it (UIG-13).\n *\n * An app's pass-on is not a result of its own \u2014 it is the package's part, and\n * the package's entry answers for it, saying which apps hand it on. At the\n * same strength the package's part comes first: it is the one to reach for.\n * And a package part whose name carries one of the words comes before an app's\n * part answering as many words, whatever each scored: \"panel header\" put an\n * app's own `Header`, whose whole name is one of the words, above the package's\n * `ContainerHeader` (UIG-20). Only by name: a package part the words reach\n * through its notes alone would bury the app part that is the answer.\n */\nexport function findInRegistries(registries: Registry[], query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n const all = words(query)\n // Drop the words that mean nothing. If the question was only those, keep them\n // rather than answer nothing at all.\n const carrying = all.filter((word) => word.length >= 3 && !STOP.has(word))\n const asked = carrying.length ? carrying : all\n if (asked.length === 0) return []\n\n const findings: Finding[] = []\n const entries = registries.flatMap((registry) => registry.entries).filter((entry) => entry.app?.class !== 're-export')\n for (const entry of entries) {\n let score = 0\n let matched = 0\n const where = new Set<string>()\n const byName = new Set<string>()\n const byNote = new Set<string>()\n for (const word of asked) {\n let best = 0\n if (entry.name.toLowerCase() === word) {\n best = 100\n where.add('name')\n } else if (hit(word, entry.name)) {\n best = 40\n where.add('name')\n }\n if (hit(word, entry.purpose)) {\n best = Math.max(best, 20)\n where.add('purpose')\n }\n for (const owned of entry.ownsBehaviours) {\n if (hit(word, owned.behaviour) || hit(word, owned.id)) {\n best = Math.max(best, 15)\n where.add('behaviour')\n }\n }\n for (const variant of entry.variants) {\n if (variant.values.some((value) => hit(word, value)) || hit(word, variant.prop)) {\n best = Math.max(best, 8)\n where.add('variant')\n byName.add(variant.prop)\n }\n }\n // What it can do. \"does Link already truncate?\" is the question this\n // answers, and answering it is what stops the thing being built twice.\n for (const prop of entry.props) {\n // A prop the question named by name is the answer; one whose note\n // happens to carry the word is a hint. Named ones go first, so a short\n // answer shows the right lines.\n if (hit(word, prop.name)) {\n best = Math.max(best, 14)\n where.add('prop')\n byName.add(prop.name)\n } else if (prop.note !== null && hit(word, prop.note)) {\n best = Math.max(best, 10)\n where.add('prop')\n byNote.add(prop.name)\n }\n }\n if (best > 0) matched += 1\n score += best\n }\n if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] })\n }\n\n const fromPackage = (finding: Finding) => (finding.entry.app === null ? 0 : 1)\n const namedInPackage = (finding: Finding) => (finding.entry.app === null && finding.where.includes('name') ? 0 : 1)\n findings.sort(\n (a, b) =>\n b.matched - a.matched || namedInPackage(a) - namedInPackage(b) || b.score - a.score || fromPackage(a) - fromPackage(b) || a.entry.name.localeCompare(b.entry.name),\n )\n return findings.slice(0, limit)\n}\n\n/** `http://localhost:6008/?path=/docs/overlays-popover--docs`, or `null` where the entry has no page. */\nexport function docsLink(registry: Registry, entry: RegistryEntry): string | null {\n if (!entry.docsId) return null\n return registry.storybook.devUrl + registry.storybook.docsPath.replace('{docsId}', entry.docsId)\n}\n\n/** What an app's part is, in the words the search prints. */\nconst CLASS_WORDS: Record<string, string> = {\n 'one-off': 'used in one place',\n reusable: 'used in several places',\n 'promote-candidate': 'used in several places; could move into the package',\n unused: 'used nowhere in the app',\n}\n\n/** What the command prints: the import line first, because that is what the reader came for. */\nexport function formatFindings(searched: Registry | Registry[], findings: Finding[], query: string): string {\n const registries = Array.isArray(searched) ? searched : [searched]\n if (findings.length === 0) {\n return [\n `Nothing in ${registries.map((registry) => registry.builtFrom.repo).join(', ')} matches \"${query}\".`,\n 'If nothing here does what you need, say so and ask \u2014 do not invent a component.',\n ].join('\\n')\n }\n const registryOf = (entry: RegistryEntry) => registries.find((registry) => registry.builtFrom.repo === entry.repo) ?? registries[0]\n // Which apps hand a package part on, and from where: in Peek, `Button` is\n // imported from `@/components/ui/Button`, and the answer should say so.\n const handedOn = new Map<string, string[]>()\n for (const registry of registries) {\n for (const entry of registry.entries) {\n if (entry.app?.class !== 're-export' || entry.app.handsOn === null) continue\n handedOn.set(entry.app.handsOn, [...(handedOn.get(entry.app.handsOn) ?? []), `${entry.repo} hands it on${entry.name === entry.app.handsOn ? '' : ` as ${entry.name}`} from ${entry.importPath}`])\n }\n }\n\n const blocks = findings.map((finding) => {\n const { entry } = finding\n const facts = entry.app\n const lines = [\n `${entry.name} \u00B7 ${facts ? CLASS_WORDS[facts.class] ?? facts.class : entry.kind} \u00B7 ${entry.repo} (matched ${finding.where.join(', ')})`,\n ` ${entry.purpose}`,\n facts?.defaultExport ? ` import ${entry.name} from '${entry.importPath}'` : ` import { ${entry.name} } from '${entry.importPath}'`,\n ]\n if (!facts) for (const said of handedOn.get(entry.name) ?? []) lines.push(` ${said}`)\n if (facts?.packageNamesake) lines.push(` the package has a ${facts.packageNamesake} too: check it first`)\n if (facts && facts.usedIn.length) lines.push(` used in ${facts.usedIn.slice(0, 2).join(', ')}${facts.usedIn.length > 2 ? ` and ${facts.usedIn.length - 2} more` : ''}`)\n if (entry.ownsBehaviours.length) lines.push(` owns: ${entry.ownsBehaviours.map((owned) => owned.behaviour).join(' \u00B7 ')}`)\n if (entry.variants.length) lines.push(` ${entry.variants.map((variant) => `${variant.prop}: ${variant.values.join(' | ')}`).join(' ')}`)\n // The props the question named, with their own line \u2014 not all of them. A\n // component with twenty props would bury its own answer.\n const shown = finding.props\n .filter((name) => !entry.variants.some((variant) => variant.prop === name))\n .slice(0, 3)\n .map((name) => entry.props.find((prop) => prop.name === name))\n for (const prop of shown) {\n if (prop) lines.push(` ${prop.name}: ${prop.takes}${prop.note ? ` \u2014 ${prop.note}` : ''}`)\n }\n if (entry.props.length) lines.push(` ${entry.props.length} props in all; see ${entry.docsId ? 'the page' : entry.sourceFile} for the rest`)\n const link = docsLink(registryOf(entry), entry)\n if (link) lines.push(` ${link}`)\n return lines.join('\\n')\n })\n\n return blocks.join('\\n\\n')\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/registry/cli.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
contractProblems,
|
|
5
5
|
findInRegistries,
|
|
6
6
|
formatFindings,
|
|
7
|
-
linkProblems
|
|
8
|
-
|
|
7
|
+
linkProblems,
|
|
8
|
+
nameProblems
|
|
9
|
+
} from "./chunk-5K2TRR4X.js";
|
|
9
10
|
import {
|
|
10
11
|
CLASSES,
|
|
11
12
|
validateRegistry
|
|
@@ -19,6 +20,7 @@ import { basename as basename2, dirname as dirname3, join as join4, relative as
|
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
20
21
|
|
|
21
22
|
// src/registry/siblings.ts
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
22
24
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
23
25
|
import { basename, dirname, join, resolve } from "node:path";
|
|
24
26
|
var PACKAGE_NAME = "@estiva-app/ui";
|
|
@@ -76,6 +78,19 @@ function findSiblings(workspace, { skip = [] } = {}) {
|
|
|
76
78
|
}
|
|
77
79
|
return [...byPackage.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
78
80
|
}
|
|
81
|
+
function behindBy(folder) {
|
|
82
|
+
const git = (...a) => {
|
|
83
|
+
try {
|
|
84
|
+
return execFileSync("git", ["-C", folder, ...a], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }).toString().trim();
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const main2 = git("symbolic-ref", "--short", "refs/remotes/origin/HEAD") ?? (git("rev-parse", "--verify", "--quiet", "origin/main") ? "origin/main" : null);
|
|
90
|
+
if (!main2) return null;
|
|
91
|
+
const count = Number(git("rev-list", "--count", `HEAD..${main2}`));
|
|
92
|
+
return Number.isFinite(count) ? count : null;
|
|
93
|
+
}
|
|
79
94
|
|
|
80
95
|
// src/registry/skill.ts
|
|
81
96
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -144,7 +159,7 @@ function loaderProblem(folder, packageRoot2) {
|
|
|
144
159
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
145
160
|
import { join as join3 } from "node:path";
|
|
146
161
|
var PREVIEW = ".storybook/preview.tsx";
|
|
147
|
-
var
|
|
162
|
+
var INTRODUCTIONS = ["src/stories/Introduction.mdx", "stories/Introduction.mdx"];
|
|
148
163
|
var WORTH_REUSING = ["reusable", "promote-candidate"];
|
|
149
164
|
function storyFiles(root2) {
|
|
150
165
|
const out = [];
|
|
@@ -158,6 +173,7 @@ function storyFiles(root2) {
|
|
|
158
173
|
}
|
|
159
174
|
};
|
|
160
175
|
walk(join3(root2, "src"));
|
|
176
|
+
walk(join3(root2, "stories"));
|
|
161
177
|
return out;
|
|
162
178
|
}
|
|
163
179
|
function sidebarHeadings(root2) {
|
|
@@ -171,46 +187,71 @@ function sidebarHeadings(root2) {
|
|
|
171
187
|
}
|
|
172
188
|
return real;
|
|
173
189
|
}
|
|
190
|
+
function indexHeadings(entries) {
|
|
191
|
+
return new Set(Object.values(entries).flatMap((e) => e.title ? [e.title.split("/")[0].trim()] : []));
|
|
192
|
+
}
|
|
193
|
+
function topLevelStrings(text, open) {
|
|
194
|
+
const out = [];
|
|
195
|
+
let depth = 0;
|
|
196
|
+
for (let i = open; i < text.length; i++) {
|
|
197
|
+
const c = text[i];
|
|
198
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
199
|
+
const end = text.indexOf("*/", i + 2);
|
|
200
|
+
if (end < 0) break;
|
|
201
|
+
i = end + 1;
|
|
202
|
+
} else if (c === "/" && text[i + 1] === "/") i = text.indexOf("\n", i);
|
|
203
|
+
else if (c === "[") depth++;
|
|
204
|
+
else if (c === "]") {
|
|
205
|
+
if (--depth === 0) return out;
|
|
206
|
+
} else if (c === "'" || c === '"') {
|
|
207
|
+
const end = text.indexOf(c, i + 1);
|
|
208
|
+
if (depth === 1) out.push(text.slice(i + 1, end));
|
|
209
|
+
i = end;
|
|
210
|
+
}
|
|
211
|
+
if (i < 0) break;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
174
215
|
function orderedHeadings(previewText) {
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
177
|
-
|
|
178
|
-
return [...listed.matchAll(/'([^']+)'/g)].map((m) => m[1]);
|
|
216
|
+
const list = /const HEADINGS = \[/.exec(previewText) ?? /\border:\s*\[/.exec(previewText);
|
|
217
|
+
if (!list) return null;
|
|
218
|
+
return topLevelStrings(previewText, list.index + list[0].length - 1);
|
|
179
219
|
}
|
|
180
220
|
function introductionHeadings(introText) {
|
|
181
|
-
|
|
221
|
+
const rows = [...introText.matchAll(/^\|\s*\*\*([^*]+)\*\*\s*\|/gm)].map((m) => m[1]);
|
|
222
|
+
const bullets = [...introText.matchAll(/^[-*]\s+\*\*([^*]+)\*\*\s*[—–-]/gm)].flatMap((m) => m[1].split("\xB7"));
|
|
223
|
+
return new Set([...rows, ...bullets].map((h) => h.trim()).filter(Boolean));
|
|
182
224
|
}
|
|
183
225
|
function introductionCount(introText) {
|
|
184
226
|
const said = introText.match(/\*\*(\d+) parts worth reusing\*\*/);
|
|
185
227
|
return said ? Number(said[1]) : null;
|
|
186
228
|
}
|
|
187
|
-
function storyMapProblems(registry, root2) {
|
|
229
|
+
function storyMapProblems(root2, { registry, real = sidebarHeadings(root2) } = {}) {
|
|
188
230
|
const problems = [];
|
|
189
|
-
const real = sidebarHeadings(root2);
|
|
190
231
|
const previewPath = join3(root2, PREVIEW);
|
|
191
232
|
if (existsSync3(previewPath)) {
|
|
192
233
|
const ordered = orderedHeadings(readFileSync3(previewPath, "utf8"));
|
|
193
234
|
if (ordered) {
|
|
194
235
|
const unordered = [...real].filter((r) => !ordered.includes(r));
|
|
195
236
|
const empty = ordered.filter((o) => !real.has(o));
|
|
196
|
-
if (unordered.length) problems.push(`${PREVIEW}: not
|
|
197
|
-
if (empty.length) problems.push(`${PREVIEW}:
|
|
237
|
+
if (unordered.length) problems.push(`${PREVIEW}: not in the heading order, so they fall to the bottom of the sidebar: ${unordered.join(", ")}`);
|
|
238
|
+
if (empty.length) problems.push(`${PREVIEW}: in the heading order but no story uses them: ${empty.join(", ")}`);
|
|
198
239
|
}
|
|
199
240
|
}
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
const
|
|
203
|
-
const named = introductionHeadings(
|
|
241
|
+
const intro = INTRODUCTIONS.find((p) => existsSync3(join3(root2, p)));
|
|
242
|
+
if (intro) {
|
|
243
|
+
const text = readFileSync3(join3(root2, intro), "utf8");
|
|
244
|
+
const named = introductionHeadings(text);
|
|
204
245
|
if (named.size) {
|
|
205
246
|
const missing = [...real].filter((r) => !named.has(r));
|
|
206
247
|
const stale = [...named].filter((n) => !real.has(n));
|
|
207
|
-
if (missing.length) problems.push(`${
|
|
208
|
-
if (stale.length) problems.push(`${
|
|
248
|
+
if (missing.length) problems.push(`${intro}: the sidebar has, the page does not name: ${missing.join(", ")}`);
|
|
249
|
+
if (stale.length) problems.push(`${intro}: the page names, the sidebar does not have: ${stale.join(", ")}`);
|
|
209
250
|
}
|
|
210
|
-
const said = introductionCount(
|
|
211
|
-
if (said !== null) {
|
|
251
|
+
const said = introductionCount(text);
|
|
252
|
+
if (said !== null && registry) {
|
|
212
253
|
const reusable = registry.entries.filter((entry) => WORTH_REUSING.includes(entry.app?.class ?? "")).length;
|
|
213
|
-
if (said !== reusable) problems.push(`${
|
|
254
|
+
if (said !== reusable) problems.push(`${intro}: says ${said} parts worth reusing; the catalogue builds ${reusable}`);
|
|
214
255
|
}
|
|
215
256
|
}
|
|
216
257
|
return problems;
|
|
@@ -280,7 +321,7 @@ function checkLinks(registry, folder) {
|
|
|
280
321
|
const bin = resolve3(folder, "node_modules", ".bin", process.platform === "win32" ? "storybook.cmd" : "storybook");
|
|
281
322
|
if (!existsSync4(resolve3(folder, ".storybook")) || !existsSync4(bin)) {
|
|
282
323
|
process.stdout.write("story links: no Storybook here, nothing to check\n");
|
|
283
|
-
return true;
|
|
324
|
+
return { ok: true };
|
|
284
325
|
}
|
|
285
326
|
const out = join4(mkdtempSync(join4(tmpdir(), "estiva-ui-index-")), "index.json");
|
|
286
327
|
const run = process.platform === "win32" ? spawnSync(`"${bin}" index -o "${out}" --quiet`, { cwd: folder, encoding: "utf8", shell: true }) : spawnSync(bin, ["index", "-o", out, "--quiet"], { cwd: folder, encoding: "utf8" });
|
|
@@ -288,20 +329,23 @@ function checkLinks(registry, folder) {
|
|
|
288
329
|
process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:
|
|
289
330
|
${(run.stderr || run.stdout || "").trim()}
|
|
290
331
|
`);
|
|
291
|
-
return false;
|
|
332
|
+
return { ok: false };
|
|
292
333
|
}
|
|
293
|
-
const
|
|
334
|
+
const entries = JSON.parse(readFileSync4(out, "utf8")).entries;
|
|
335
|
+
const ids = new Set(Object.keys(entries));
|
|
336
|
+
const headings = indexHeadings(entries);
|
|
337
|
+
const titles = new Set(Object.values(entries).flatMap((e) => e.title ? [e.title] : []));
|
|
294
338
|
const dead = linkProblems(registry, ids);
|
|
295
339
|
const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0);
|
|
296
340
|
if (dead.length) {
|
|
297
341
|
process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:
|
|
298
342
|
${dead.join("\n ")}
|
|
299
343
|
`);
|
|
300
|
-
return false;
|
|
344
|
+
return { ok: false, headings, titles };
|
|
301
345
|
}
|
|
302
346
|
process.stdout.write(`story links: all ${count} open a story or docs page Storybook has
|
|
303
347
|
`);
|
|
304
|
-
return true;
|
|
348
|
+
return { ok: true, headings, titles };
|
|
305
349
|
}
|
|
306
350
|
async function checkGated(registry, folder, kinds) {
|
|
307
351
|
const { gatedFindings } = await gatedScan();
|
|
@@ -316,8 +360,8 @@ async function checkGated(registry, folder, kinds) {
|
|
|
316
360
|
process.stdout.write("stories: everything a part draws behind a prop is shown by a story\n");
|
|
317
361
|
return true;
|
|
318
362
|
}
|
|
319
|
-
function checkStoryMap(registry,
|
|
320
|
-
const problems = storyMapProblems(registry,
|
|
363
|
+
function checkStoryMap(folder, registry, headings) {
|
|
364
|
+
const problems = storyMapProblems(folder, { ...registry ? { registry } : {}, ...headings ? { real: headings } : {} });
|
|
321
365
|
if (problems.length) {
|
|
322
366
|
process.stderr.write(`storybook map: ${problems.length === 1 ? "one line no longer matches" : `${problems.length} lines no longer match`}:
|
|
323
367
|
${problems.join("\n ")}
|
|
@@ -343,6 +387,9 @@ async function main() {
|
|
|
343
387
|
}
|
|
344
388
|
const registries = [readPackageRegistry()];
|
|
345
389
|
const add = async (folder, repo) => {
|
|
390
|
+
const behind = behindBy(folder);
|
|
391
|
+
if (behind) process.stderr.write(`note: ${repo ?? basename2(folder)} (${folder}) is ${behind} commit${behind === 1 ? "" : "s"} behind main as last fetched \u2014 its answers may name parts that are gone; pull it
|
|
392
|
+
`);
|
|
346
393
|
try {
|
|
347
394
|
registries.push(await buildApp(folder, repo));
|
|
348
395
|
} catch (error) {
|
|
@@ -421,8 +468,18 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
421
468
|
const linked2 = checkLinks(registry, root);
|
|
422
469
|
const shown2 = await checkGated(registry, root);
|
|
423
470
|
const loaded2 = checkLoader();
|
|
424
|
-
const
|
|
425
|
-
|
|
471
|
+
const mapped2 = checkStoryMap(root, registry, linked2.headings);
|
|
472
|
+
const pkg = readPackageRegistry();
|
|
473
|
+
const stale = nameProblems(registry, root, {
|
|
474
|
+
known: new Set([...pkg.entries, ...registry.entries].map((e) => e.name)),
|
|
475
|
+
...linked2.titles ? { titles: linked2.titles } : {},
|
|
476
|
+
packageIds: pkg.entries.flatMap((e) => [e.storyId, e.docsId].filter((id) => Boolean(id)))
|
|
477
|
+
});
|
|
478
|
+
if (stale.length) process.stderr.write(`usage pages: ${stale.length === 1 ? "one name leads" : `${stale.length} names lead`} nowhere:
|
|
479
|
+
${stale.join("\n ")}
|
|
480
|
+
`);
|
|
481
|
+
else process.stdout.write("usage pages: every part and story they name is there\n");
|
|
482
|
+
return broken.length || stale.length || !linked2.ok || !shown2 || !loaded2 || !mapped2 ? 1 : 0;
|
|
426
483
|
}
|
|
427
484
|
const { buildRegistry, serializeRegistry } = await packageBuilder();
|
|
428
485
|
const path = packageRegistryPath();
|
|
@@ -449,7 +506,8 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
449
506
|
const linked = checkLinks(built, root);
|
|
450
507
|
const shown = await checkGated(built, root, ["component"]);
|
|
451
508
|
const loaded = checkLoader();
|
|
452
|
-
|
|
509
|
+
const mapped = checkStoryMap(root, void 0, linked.headings);
|
|
510
|
+
return linked.ok && shown && loaded && mapped ? 0 : 1;
|
|
453
511
|
}
|
|
454
512
|
case "skill": {
|
|
455
513
|
const out = value("out");
|