@estiva-app/ui 0.26.0 → 0.27.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/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-GTQZEHPC.js → chunk-I7CJLUPM.js} +13 -1
- package/dist/gates/chunk-I7CJLUPM.js.map +7 -0
- package/dist/gates/cli.js +1 -1
- package/dist/gates/index.js +7 -3
- package/dist/gates/index.js.map +2 -2
- package/dist/gates/status.d.ts +2 -0
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/registry/{app-DQI6WLHR.js → app-FXN4EI34.js} +2 -2
- package/dist/registry/app.d.ts.map +1 -1
- package/dist/registry/{chunk-W2B2G7OE.js → chunk-6OH3TWGF.js} +59 -1
- package/dist/registry/chunk-6OH3TWGF.js.map +7 -0
- package/dist/registry/{chunk-WMFF3MPP.js → chunk-XCBN2EKW.js} +3 -2
- package/dist/registry/{chunk-WMFF3MPP.js.map → chunk-XCBN2EKW.js.map} +2 -2
- package/dist/registry/cli.js +69 -8
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/contract.d.ts +29 -0
- package/dist/registry/contract.d.ts.map +1 -0
- package/dist/registry/gated-5CPIUQAW.js +230 -0
- package/dist/registry/gated-5CPIUQAW.js.map +7 -0
- package/dist/registry/gated.d.ts +31 -0
- package/dist/registry/gated.d.ts.map +1 -0
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +15 -3
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/CollapsibleSection.stories.tsx +6 -0
- package/src/ListColumn.stories.tsx +6 -0
- package/src/MenuItem.stories.tsx +151 -135
- package/src/Person.stories.tsx +7 -0
- package/src/PersonTrigger.stories.tsx +7 -0
- package/src/SectionHeader.stories.tsx +5 -0
- package/src/gates/app-checks.ts +6 -2
- package/src/gates/status.ts +15 -0
- package/src/registry/app.test.ts +14 -1
- package/src/registry/app.ts +5 -1
- package/src/registry/cli.ts +79 -7
- package/src/registry/contract.test.ts +158 -0
- package/src/registry/contract.ts +108 -0
- package/src/registry/gated.test.ts +98 -0
- package/src/registry/gated.ts +308 -0
- package/src/registry/index.ts +1 -0
- package/dist/gates/chunk-GTQZEHPC.js.map +0 -7
- package/dist/registry/chunk-W2B2G7OE.js.map +0 -7
- /package/dist/registry/{app-DQI6WLHR.js.map → app-FXN4EI34.js.map} +0 -0
package/dist/registry/cli.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONTRACT_KINDS,
|
|
4
|
+
contractProblems,
|
|
3
5
|
findInRegistries,
|
|
4
|
-
formatFindings
|
|
5
|
-
|
|
6
|
+
formatFindings,
|
|
7
|
+
linkProblems
|
|
8
|
+
} from "./chunk-6OH3TWGF.js";
|
|
6
9
|
import {
|
|
7
10
|
CLASSES,
|
|
8
11
|
validateRegistry
|
|
9
12
|
} from "./chunk-E4JNV7PC.js";
|
|
10
13
|
|
|
11
14
|
// src/registry/cli.ts
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { basename, join, resolve } from "node:path";
|
|
14
19
|
import { fileURLToPath } from "node:url";
|
|
15
20
|
var packageBuilder = () => import("./build-LGFCCOLR.js");
|
|
16
|
-
var appBuilder = () => import("./app-
|
|
21
|
+
var appBuilder = () => import("./app-FXN4EI34.js");
|
|
22
|
+
var gatedScan = () => import("./gated-5CPIUQAW.js");
|
|
17
23
|
var [command, ...rest] = process.argv.slice(2);
|
|
18
24
|
var VALUED = ["--limit", "--out", "--file", "--root", "--repo", "--also"];
|
|
19
25
|
var flag = (name) => rest.includes(`--${name}`);
|
|
@@ -59,6 +65,46 @@ async function buildApp(folder, repo) {
|
|
|
59
65
|
${problems.join("\n ")}`);
|
|
60
66
|
return registry;
|
|
61
67
|
}
|
|
68
|
+
function checkLinks(registry, folder) {
|
|
69
|
+
const bin = resolve(folder, "node_modules", ".bin", process.platform === "win32" ? "storybook.cmd" : "storybook");
|
|
70
|
+
if (!existsSync(resolve(folder, ".storybook")) || !existsSync(bin)) {
|
|
71
|
+
process.stdout.write("story links: no Storybook here, nothing to check\n");
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const out = join(mkdtempSync(join(tmpdir(), "estiva-ui-index-")), "index.json");
|
|
75
|
+
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" });
|
|
76
|
+
if (run.status !== 0 || !existsSync(out)) {
|
|
77
|
+
process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:
|
|
78
|
+
${(run.stderr || run.stdout || "").trim()}
|
|
79
|
+
`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const ids = new Set(Object.keys(JSON.parse(readFileSync(out, "utf8")).entries));
|
|
83
|
+
const dead = linkProblems(registry, ids);
|
|
84
|
+
const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0);
|
|
85
|
+
if (dead.length) {
|
|
86
|
+
process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:
|
|
87
|
+
${dead.join("\n ")}
|
|
88
|
+
`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(`story links: all ${count} open a story or docs page Storybook has
|
|
92
|
+
`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
async function checkGated(registry, folder, kinds) {
|
|
96
|
+
const { gatedFindings } = await gatedScan();
|
|
97
|
+
const found = gatedFindings(registry, folder, { ...kinds ? { kinds } : {}, pictured: "own" });
|
|
98
|
+
if (found.length) {
|
|
99
|
+
const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \`${f.prop}\`, and no story that draws it passes \`${f.prop}\`: pass it in a story`);
|
|
100
|
+
process.stderr.write(`stories: ${found.length === 1 ? "one thing a part draws is" : `${found.length} things parts draw are`} never shown:
|
|
101
|
+
${lines.join("\n ")}
|
|
102
|
+
`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write("stories: everything a part draws behind a prop is shown by a story\n");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
62
108
|
function summary(registry) {
|
|
63
109
|
const count = (cls) => registry.entries.filter((entry) => entry.app?.class === cls).length;
|
|
64
110
|
const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(", ");
|
|
@@ -133,9 +179,22 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
133
179
|
}
|
|
134
180
|
case "check": {
|
|
135
181
|
if (!isPackage) {
|
|
136
|
-
|
|
182
|
+
const registry = await buildApp(root, value("repo"));
|
|
183
|
+
process.stdout.write(`${summary(registry)}
|
|
137
184
|
`);
|
|
138
|
-
|
|
185
|
+
const broken = contractProblems(registry, root);
|
|
186
|
+
if (broken.length) {
|
|
187
|
+
process.stderr.write(`${broken.length === 1 ? "a part breaks" : `${broken.length} parts break`} the usage-page contract:
|
|
188
|
+
${broken.join("\n ")}
|
|
189
|
+
`);
|
|
190
|
+
} else {
|
|
191
|
+
const owed = registry.entries.filter((entry) => CONTRACT_KINDS.includes(entry.app?.class ?? "")).length;
|
|
192
|
+
process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere
|
|
193
|
+
`);
|
|
194
|
+
}
|
|
195
|
+
const linked2 = checkLinks(registry, root);
|
|
196
|
+
const shown2 = await checkGated(registry, root);
|
|
197
|
+
return broken.length || !linked2 || !shown2 ? 1 : 0;
|
|
139
198
|
}
|
|
140
199
|
const { buildRegistry, serializeRegistry } = await packageBuilder();
|
|
141
200
|
const path = packageRegistryPath();
|
|
@@ -159,7 +218,9 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
159
218
|
}
|
|
160
219
|
process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports
|
|
161
220
|
`);
|
|
162
|
-
|
|
221
|
+
const linked = checkLinks(built, root);
|
|
222
|
+
const shown = await checkGated(built, root, ["component"]);
|
|
223
|
+
return linked && shown ? 0 : 1;
|
|
163
224
|
}
|
|
164
225
|
default:
|
|
165
226
|
process.stderr.write("estiva-ui <find | build | check>\n");
|
package/dist/registry/cli.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/registry/cli.ts"],
|
|
4
|
-
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--also [name=]<folder>]\u2026 [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted; CI's\n * `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus each `--also` app that sits beside it.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { findInRegistries, formatFindings } from './find'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n if (!isPackage) await add(root, value('repo'))\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n process.stdout.write(`${summary(await buildApp(root, value('repo')))}\\n`)\n return 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n return 0\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["serializeRegistry", "registry"]
|
|
4
|
+
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--also [name=]<folder>]\u2026 [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted, or\n * when a reusable part breaks the usage-page contract (UIG-19, `./contract`);\n * CI's `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus each `--also` app that sits beside it.\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { CONTRACT_KINDS, contractProblems, linkProblems } from './contract'\nimport { findInRegistries, formatFindings } from './find'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\nconst gatedScan = () => import('./gated')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/**\n * Every story and docs link in a catalogue, against the Storybook beside it\n * (UIG-19). `storybook index` writes the ids Storybook itself would serve \u2014 no\n * server, no browser, seconds \u2014 so a renamed story, or a page that no longer\n * compiles, fails here rather than as a dead link somebody finds later.\n * Reports, and says whether it passed. A folder with no Storybook has nothing\n * to check.\n */\nfunction checkLinks(registry: Registry, folder: string): boolean {\n const bin = resolve(folder, 'node_modules', '.bin', process.platform === 'win32' ? 'storybook.cmd' : 'storybook')\n if (!existsSync(resolve(folder, '.storybook')) || !existsSync(bin)) {\n process.stdout.write('story links: no Storybook here, nothing to check\\n')\n return true\n }\n const out = join(mkdtempSync(join(tmpdir(), 'estiva-ui-index-')), 'index.json')\n // A `.cmd` needs a shell on Windows, and Node wants that as one quoted line\n // rather than an argument list. Both paths are ours: the app's folder and a temp file.\n const run =\n process.platform === 'win32'\n ? spawnSync(`\"${bin}\" index -o \"${out}\" --quiet`, { cwd: folder, encoding: 'utf8', shell: true })\n : spawnSync(bin, ['index', '-o', out, '--quiet'], { cwd: folder, encoding: 'utf8' })\n if (run.status !== 0 || !existsSync(out)) {\n process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:\\n${(run.stderr || run.stdout || '').trim()}\\n`)\n return false\n }\n const ids = new Set(Object.keys((JSON.parse(readFileSync(out, 'utf8')) as { entries: Record<string, unknown> }).entries))\n const dead = linkProblems(registry, ids)\n const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0)\n if (dead.length) {\n process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:\\n ${dead.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write(`story links: all ${count} open a story or docs page Storybook has\\n`)\n return true\n}\n\n/**\n * What a part draws only when a prop is passed, where no story passes it\n * (UIG-19, `./gated`). Strict, by Katerina's ruling: a part's own stories show\n * everything it can draw. In an app it reads the reusable parts; in the package,\n * every component (`kinds` is the package's one class, `component`).\n */\nasync function checkGated(registry: Registry, folder: string, kinds?: string[]): Promise<boolean> {\n const { gatedFindings } = await gatedScan()\n const found = gatedFindings(registry, folder, { ...(kinds ? { kinds } : {}), pictured: 'own' })\n if (found.length) {\n const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \\`${f.prop}\\`, and no story that draws it passes \\`${f.prop}\\`: pass it in a story`)\n process.stderr.write(`stories: ${found.length === 1 ? 'one thing a part draws is' : `${found.length} things parts draw are`} never shown:\\n ${lines.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('stories: everything a part draws behind a prop is shown by a story\\n')\n return true\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n if (!isPackage) await add(root, value('repo'))\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n const registry = await buildApp(root, value('repo'))\n process.stdout.write(`${summary(registry)}\\n`)\n // The usage-page contract and the story links (UIG-19): the one copy, run by every app's `gate` job.\n const broken = contractProblems(registry, root)\n if (broken.length) {\n process.stderr.write(`${broken.length === 1 ? 'a part breaks' : `${broken.length} parts break`} the usage-page contract:\\n ${broken.join('\\n ')}\\n`)\n } else {\n const owed = registry.entries.filter((entry) => (CONTRACT_KINDS as readonly string[]).includes(entry.app?.class ?? '')).length\n process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere\\n`)\n }\n const linked = checkLinks(registry, root)\n const shown = await checkGated(registry, root)\n return broken.length || !linked || !shown ? 1 : 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n const linked = checkLinks(built, root)\n const shown = await checkGated(built, root, ['component'])\n return linked && shown ? 0 : 1\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["serializeRegistry", "registry", "linked", "shown"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Registry } from './schema';
|
|
2
|
+
/** The four headings, in the order a page carries them. */
|
|
3
|
+
export declare const PAGE_SECTIONS: readonly ["When", "When not", "How", "What it owns"];
|
|
4
|
+
/** The classes that owe a page and a picture. */
|
|
5
|
+
export declare const CONTRACT_KINDS: readonly ["reusable", "promote-candidate"];
|
|
6
|
+
/**
|
|
7
|
+
* Why a page breaks the contract, or null when it keeps it.
|
|
8
|
+
*
|
|
9
|
+
* Read on normalised text: the apps are checked out with CRLF on Windows.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pageProblem(source: string): string | null;
|
|
12
|
+
/** Whether a page records where the part is drawn, in place of a story of its own. */
|
|
13
|
+
export declare function hasSeenIn(source: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Every catalogue link that names a story or a docs page its Storybook does not
|
|
16
|
+
* have (UIG-19: a dead link is a build failure, not a dead end found six weeks
|
|
17
|
+
* later). `ids` is every entry id in the Storybook's own index — what
|
|
18
|
+
* `storybook index` writes — so this is Storybook's answer, not a guess at how
|
|
19
|
+
* it builds an id.
|
|
20
|
+
*/
|
|
21
|
+
export declare function linkProblems(registry: Registry, ids: ReadonlySet<string>): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Every way an app's catalogue breaks the contract, one line each, naming the
|
|
24
|
+
* part, its file and what to do. Empty when it keeps it.
|
|
25
|
+
*
|
|
26
|
+
* `root` is the app's own folder: the one its catalogue's paths are relative to.
|
|
27
|
+
*/
|
|
28
|
+
export declare function contractProblems(registry: Registry, root: string): string[];
|
|
29
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/registry/contract.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,2DAA2D;AAC3D,eAAO,MAAM,aAAa,sDAAuD,CAAA;AAEjF,iDAAiD;AACjD,eAAO,MAAM,cAAc,4CAA6C,CAAA;AAKxE;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBzD;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CASnF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB3E"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/registry/gated.ts
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
var parse = (file, text) => ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
8
|
+
function declared(sf, name) {
|
|
9
|
+
let found = null;
|
|
10
|
+
const unwrap = (e) => {
|
|
11
|
+
if (!e) return null;
|
|
12
|
+
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return e;
|
|
13
|
+
if (ts.isCallExpression(e)) return unwrap(e.arguments[0]);
|
|
14
|
+
if (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) return unwrap(e.expression);
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
for (const s of sf.statements) {
|
|
18
|
+
if (ts.isFunctionDeclaration(s) && s.name?.text === name) found = s;
|
|
19
|
+
if (ts.isVariableStatement(s)) {
|
|
20
|
+
for (const d of s.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === name) found = unwrap(d.initializer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
function propsOf(fn) {
|
|
26
|
+
const locals = /* @__PURE__ */ new Map();
|
|
27
|
+
const first = fn.parameters[0];
|
|
28
|
+
if (!first) return { locals, object: null };
|
|
29
|
+
if (ts.isObjectBindingPattern(first.name)) {
|
|
30
|
+
for (const el of first.name.elements) {
|
|
31
|
+
if (el.dotDotDotToken || !ts.isIdentifier(el.name)) continue;
|
|
32
|
+
const prop = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
33
|
+
locals.set(el.name.text, prop);
|
|
34
|
+
}
|
|
35
|
+
return { locals, object: null };
|
|
36
|
+
}
|
|
37
|
+
return { locals, object: ts.isIdentifier(first.name) ? first.name.text : null };
|
|
38
|
+
}
|
|
39
|
+
var drawsJsx = (node) => {
|
|
40
|
+
let yes = false;
|
|
41
|
+
const walk = (n) => {
|
|
42
|
+
if (yes) return;
|
|
43
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) yes = true;
|
|
44
|
+
else ts.forEachChild(n, walk);
|
|
45
|
+
};
|
|
46
|
+
walk(node);
|
|
47
|
+
return yes;
|
|
48
|
+
};
|
|
49
|
+
function gates(fn) {
|
|
50
|
+
const { locals, object } = propsOf(fn);
|
|
51
|
+
const out = /* @__PURE__ */ new Set();
|
|
52
|
+
const propIn = (e) => {
|
|
53
|
+
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
|
54
|
+
if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken && ts.isPrefixUnaryExpression(e.operand)) return propIn(e.operand.operand);
|
|
55
|
+
if (ts.isBinaryExpression(e) && [ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(e.operatorToken.kind)) return propIn(e.left);
|
|
56
|
+
if (ts.isIdentifier(e)) return locals.get(e.text) ?? null;
|
|
57
|
+
if (object && ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === object) return e.name.text;
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
const walk = (n) => {
|
|
61
|
+
if (n !== fn && ts.isFunctionDeclaration(n)) return;
|
|
62
|
+
if (ts.isJsxExpression(n) && n.expression) {
|
|
63
|
+
const e = n.expression;
|
|
64
|
+
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && drawsJsx(e.right)) {
|
|
65
|
+
const p = propIn(e.left);
|
|
66
|
+
if (p) out.add(p);
|
|
67
|
+
}
|
|
68
|
+
if (ts.isConditionalExpression(e) && drawsJsx(e.whenTrue) && !drawsJsx(e.whenFalse)) {
|
|
69
|
+
const p = propIn(e.condition);
|
|
70
|
+
if (p) out.add(p);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(n, walk);
|
|
74
|
+
};
|
|
75
|
+
if (fn.body) walk(fn.body);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
var ANY = "*";
|
|
79
|
+
function passed(sf, tags) {
|
|
80
|
+
const out = /* @__PURE__ */ new Set();
|
|
81
|
+
const consts = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const s of sf.statements) {
|
|
83
|
+
if (!ts.isVariableStatement(s)) continue;
|
|
84
|
+
for (const d of s.declarationList.declarations) if (ts.isIdentifier(d.name) && d.initializer && ts.isObjectLiteralExpression(d.initializer)) consts.set(d.name.text, d.initializer);
|
|
85
|
+
}
|
|
86
|
+
const keysOf = (o, seen) => {
|
|
87
|
+
for (const p of o.properties) {
|
|
88
|
+
if (ts.isSpreadAssignment(p)) {
|
|
89
|
+
const e = p.expression;
|
|
90
|
+
const name = ts.isIdentifier(e) ? e.text : ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.name.text === "args" ? `${e.expression.text}.args` : null;
|
|
91
|
+
let target2;
|
|
92
|
+
if (name && !seen.has(name)) {
|
|
93
|
+
if (!name.endsWith(".args")) target2 = consts.get(name);
|
|
94
|
+
else {
|
|
95
|
+
const story = consts.get(name.slice(0, -".args".length));
|
|
96
|
+
const args = story?.properties.find((q) => ts.isPropertyAssignment(q) && ts.isIdentifier(q.name) && q.name.text === "args");
|
|
97
|
+
if (args && ts.isPropertyAssignment(args) && ts.isObjectLiteralExpression(args.initializer)) target2 = args.initializer;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (target2) keysOf(target2, /* @__PURE__ */ new Set([...seen, name]));
|
|
101
|
+
else out.add(ANY);
|
|
102
|
+
} else if (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) out.add(p.name.text);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const walk = (n) => {
|
|
106
|
+
if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name) && n.name.text === "args") {
|
|
107
|
+
if (ts.isObjectLiteralExpression(n.initializer)) keysOf(n.initializer, /* @__PURE__ */ new Set());
|
|
108
|
+
else out.add(ANY);
|
|
109
|
+
}
|
|
110
|
+
if ((ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) && tags.includes(n.tagName.getText(sf))) {
|
|
111
|
+
for (const a of n.attributes.properties) {
|
|
112
|
+
if (ts.isJsxAttribute(a)) out.add(a.name.getText(sf));
|
|
113
|
+
else if (ts.isJsxSpreadAttribute(a)) {
|
|
114
|
+
const target2 = ts.isIdentifier(a.expression) ? consts.get(a.expression.text) : void 0;
|
|
115
|
+
if (target2) keysOf(target2, /* @__PURE__ */ new Set());
|
|
116
|
+
else if (!(ts.isIdentifier(a.expression) && a.expression.text === "args")) out.add(ANY);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ts.forEachChild(n, walk);
|
|
121
|
+
};
|
|
122
|
+
walk(sf);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function storyFiles(root) {
|
|
126
|
+
const out = [];
|
|
127
|
+
const walk = (dir) => {
|
|
128
|
+
for (const name of readdirSync(dir)) {
|
|
129
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
130
|
+
const path = join(dir, name);
|
|
131
|
+
if (statSync(path).isDirectory()) walk(path);
|
|
132
|
+
else if (/\.stories\.tsx?$/.test(name)) out.push(path);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
if (existsSync(join(root, "src"))) walk(join(root, "src"));
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function handsOn(fn, sf) {
|
|
139
|
+
const { locals, object } = propsOf(fn);
|
|
140
|
+
const out = [];
|
|
141
|
+
const walk = (n) => {
|
|
142
|
+
if (n !== fn && ts.isFunctionDeclaration(n)) return;
|
|
143
|
+
if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {
|
|
144
|
+
const tag = n.tagName.getText(sf);
|
|
145
|
+
if (/^[A-Z]/.test(tag)) {
|
|
146
|
+
for (const a of n.attributes.properties) {
|
|
147
|
+
if (!ts.isJsxAttribute(a) || !a.initializer || !ts.isJsxExpression(a.initializer) || !a.initializer.expression) continue;
|
|
148
|
+
const e = a.initializer.expression;
|
|
149
|
+
const prop = ts.isIdentifier(e) ? locals.get(e.text) : object && ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === object ? e.name.text : void 0;
|
|
150
|
+
if (prop) out.push({ tag, attr: a.name.getText(sf), prop });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
ts.forEachChild(n, walk);
|
|
155
|
+
};
|
|
156
|
+
if (fn.body) walk(fn.body);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function target(from, spec, root) {
|
|
160
|
+
if (spec.startsWith(".")) return join(dirname(from), spec).replace(/\.[jt]sx?$/, "");
|
|
161
|
+
if (spec.startsWith("@/")) return join(root, "src", spec.slice(2)).replace(/\.[jt]sx?$/, "");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function gatedFindings(registry, root, { kinds = ["reusable", "promote-candidate"], pictured = "own" } = {}) {
|
|
165
|
+
const findings = [];
|
|
166
|
+
const stories = storyFiles(root).map((file) => ({ file, sf: parse(file, readFileSync(file, "utf8")) }));
|
|
167
|
+
const sources = /* @__PURE__ */ new Map();
|
|
168
|
+
const sourceOf = (rel) => {
|
|
169
|
+
const path = join(root, rel);
|
|
170
|
+
if (!sources.has(path)) {
|
|
171
|
+
if (!existsSync(path)) return null;
|
|
172
|
+
sources.set(path, parse(path, readFileSync(path, "utf8")));
|
|
173
|
+
}
|
|
174
|
+
return sources.get(path);
|
|
175
|
+
};
|
|
176
|
+
const fileOf = new Map(registry.entries.filter((e) => e.kind === "component").map((e) => [e.name, e.sourceFile]));
|
|
177
|
+
const givenTo = /* @__PURE__ */ new Map();
|
|
178
|
+
const storiesOf = (name, rel) => {
|
|
179
|
+
const key = `${rel}#${name}`;
|
|
180
|
+
if (!givenTo.has(key)) {
|
|
181
|
+
const own = join(root, rel).replace(/\.[jt]sx?$/, "");
|
|
182
|
+
const drawing = stories.filter(
|
|
183
|
+
({ file, sf: s }) => s.statements.some((st) => ts.isImportDeclaration(st) && ts.isStringLiteral(st.moduleSpecifier) && target(file, st.moduleSpecifier.text, root) === own)
|
|
184
|
+
);
|
|
185
|
+
givenTo.set(key, { drawn: drawing.length > 0, given: new Set(drawing.flatMap(({ sf: s }) => [...passed(s, [name, `${name}View`])])) });
|
|
186
|
+
}
|
|
187
|
+
return givenTo.get(key);
|
|
188
|
+
};
|
|
189
|
+
const memo = /* @__PURE__ */ new Map();
|
|
190
|
+
const gatesOf = (name, sf, depth) => {
|
|
191
|
+
const key = `${sf.fileName}#${name}`;
|
|
192
|
+
if (memo.has(key)) return memo.get(key);
|
|
193
|
+
memo.set(key, /* @__PURE__ */ new Set());
|
|
194
|
+
const fn = declared(sf, name);
|
|
195
|
+
const out = fn ? gates(fn) : /* @__PURE__ */ new Set();
|
|
196
|
+
if (fn && depth < 3) {
|
|
197
|
+
for (const { tag, attr, prop } of handsOn(fn, sf)) {
|
|
198
|
+
if (out.has(prop) || /Dialog$/.test(tag)) continue;
|
|
199
|
+
const here = declared(sf, tag) ? sf : fileOf.has(tag) ? sourceOf(fileOf.get(tag)) : null;
|
|
200
|
+
if (!here || !gatesOf(tag, here, depth + 1).has(attr)) continue;
|
|
201
|
+
const theirs = fileOf.get(tag);
|
|
202
|
+
if (pictured === "anywhere" && theirs && here !== sf) {
|
|
203
|
+
const { given } = storiesOf(tag, theirs);
|
|
204
|
+
if (given.has(attr) || given.has(ANY)) continue;
|
|
205
|
+
}
|
|
206
|
+
out.add(prop);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
memo.set(key, out);
|
|
210
|
+
return out;
|
|
211
|
+
};
|
|
212
|
+
for (const entry of registry.entries) {
|
|
213
|
+
const cls = entry.app?.class ?? "component";
|
|
214
|
+
if (!kinds.includes(cls) || entry.kind !== "component") continue;
|
|
215
|
+
const sf = sourceOf(entry.sourceFile);
|
|
216
|
+
if (!sf) continue;
|
|
217
|
+
const { drawn, given } = storiesOf(entry.name, entry.sourceFile);
|
|
218
|
+
if (!drawn || given.has(ANY)) continue;
|
|
219
|
+
for (const name of [entry.name, `${entry.name}View`]) {
|
|
220
|
+
for (const prop of gatesOf(name, sf, 0)) {
|
|
221
|
+
if (!given.has(prop)) findings.push({ part: entry.name, file: entry.sourceFile.split("\\").join("/"), prop, in: name });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return findings;
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
gatedFindings
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=gated-5CPIUQAW.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/registry/gated.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * What a part draws only when a prop is passed, and no story passes it (UIG-19,\n * Katerina's ruling 5).\n *\n * `{onReply && <IconButton \u2026 />}` works in the app, because the app passes\n * `onReply`. In Storybook nothing does, so the button is simply not there. It\n * reads as a broken behaviour rather than a missing fixture, and it hides the\n * very state a reviewer came to see. UIG-17 found five in Peek by hand: Reply on\n * ConversationCard, Edit and Delete on ReplyCard, Rename and Copy link on\n * TopicMoreMenu, Open original on ThreadPanel.\n *\n * Read with TypeScript, part by part. The gate is looked for only inside the\n * part's own function (and its `\u2026View` half, when the stories draw that),\n * because a file is not a component: UIG-17's first scan blamed ReplyCard for\n * two props that belong to a menu declared beside it. A story passes a prop by\n * naming it in an `args` object or as an attribute on the part's tag, anywhere\n * in a story file that imports the part's file.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'\nimport { basename, dirname, join, relative } from 'node:path'\nimport ts from 'typescript'\nimport type { Registry } from './schema'\n\nexport interface GatedFinding {\n part: string\n file: string\n prop: string\n /** The part or view whose body holds the gate. */\n in: string\n}\n\nconst parse = (file: string, text: string) => ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)\n\n/** The function a name is declared as: `function X`, `const X = (\u2026) =>`, `memo(\u2026)`, `forwardRef(\u2026)`. */\nfunction declared(sf: ts.SourceFile, name: string): ts.FunctionLikeDeclaration | null {\n let found: ts.FunctionLikeDeclaration | null = null\n const unwrap = (e: ts.Expression | undefined): ts.FunctionLikeDeclaration | null => {\n if (!e) return null\n if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return e\n if (ts.isCallExpression(e)) return unwrap(e.arguments[0])\n if (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) return unwrap(e.expression)\n return null\n }\n for (const s of sf.statements) {\n if (ts.isFunctionDeclaration(s) && s.name?.text === name) found = s\n if (ts.isVariableStatement(s)) for (const d of s.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === name) found = unwrap(d.initializer)\n }\n return found\n}\n\n/** The props a function takes, by the name its body uses: `{ onReply, owner: who }` \u2192 onReply\u2192onReply, who\u2192owner. */\nfunction propsOf(fn: ts.FunctionLikeDeclaration): { locals: Map<string, string>; object: string | null } {\n const locals = new Map<string, string>()\n const first = fn.parameters[0]\n if (!first) return { locals, object: null }\n if (ts.isObjectBindingPattern(first.name)) {\n for (const el of first.name.elements) {\n if (el.dotDotDotToken || !ts.isIdentifier(el.name)) continue\n const prop = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text\n locals.set(el.name.text, prop)\n }\n return { locals, object: null }\n }\n return { locals, object: ts.isIdentifier(first.name) ? first.name.text : null }\n}\n\nconst drawsJsx = (node: ts.Node): boolean => {\n let yes = false\n const walk = (n: ts.Node) => {\n if (yes) return\n if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) yes = true\n else ts.forEachChild(n, walk)\n }\n walk(node)\n return yes\n}\n\n/** Every prop that gates something drawn: `{p && <X/>}`, `{p ? <X/> : null}`, `{!!p && \u2026}`, `{p != null && \u2026}`. */\nfunction gates(fn: ts.FunctionLikeDeclaration): Set<string> {\n const { locals, object } = propsOf(fn)\n const out = new Set<string>()\n const propIn = (e: ts.Expression): string | null => {\n while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression\n if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken && ts.isPrefixUnaryExpression(e.operand)) return propIn(e.operand.operand)\n if (ts.isBinaryExpression(e) && [ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(e.operatorToken.kind)) return propIn(e.left)\n if (ts.isIdentifier(e)) return locals.get(e.text) ?? null\n if (object && ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === object) return e.name.text\n return null\n }\n const walk = (n: ts.Node) => {\n // A function declared inside is its own component or handler: not this part's markup.\n if (n !== fn && ts.isFunctionDeclaration(n)) return\n if (ts.isJsxExpression(n) && n.expression) {\n const e = n.expression\n if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && drawsJsx(e.right)) {\n const p = propIn(e.left)\n if (p) out.add(p)\n }\n if (ts.isConditionalExpression(e) && drawsJsx(e.whenTrue) && !drawsJsx(e.whenFalse)) {\n const p = propIn(e.condition)\n if (p) out.add(p)\n }\n }\n ts.forEachChild(n, walk)\n }\n if (fn.body) walk(fn.body)\n return out\n}\n\n/** Stands for \"every prop\": a spread the reader cannot open might pass any of them. */\nconst ANY = '*'\n\n/**\n * Every prop name a story file passes: keys of any `args` object, and\n * attributes on the named tags. A spread of an object declared in the same\n * file is opened (`args: { replyCount: 3, ...replyRow }`); one it cannot open\n * \u2014 an import, a call \u2014 counts as passing everything, so the check stays quiet\n * rather than wrong.\n */\nfunction passed(sf: ts.SourceFile, tags: string[]): Set<string> {\n const out = new Set<string>()\n const consts = new Map<string, ts.ObjectLiteralExpression>()\n for (const s of sf.statements) {\n if (!ts.isVariableStatement(s)) continue\n for (const d of s.declarationList.declarations) if (ts.isIdentifier(d.name) && d.initializer && ts.isObjectLiteralExpression(d.initializer)) consts.set(d.name.text, d.initializer)\n }\n const keysOf = (o: ts.ObjectLiteralExpression, seen: Set<string>) => {\n for (const p of o.properties) {\n if (ts.isSpreadAssignment(p)) {\n // `...replyRow`, or another story's args: `...Topic.args`.\n const e = p.expression\n const name = ts.isIdentifier(e) ? e.text : ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.name.text === 'args' ? `${e.expression.text}.args` : null\n let target: ts.ObjectLiteralExpression | undefined\n if (name && !seen.has(name)) {\n if (!name.endsWith('.args')) target = consts.get(name)\n else {\n const story = consts.get(name.slice(0, -'.args'.length))\n const args = story?.properties.find((q) => ts.isPropertyAssignment(q) && ts.isIdentifier(q.name) && q.name.text === 'args')\n if (args && ts.isPropertyAssignment(args) && ts.isObjectLiteralExpression(args.initializer)) target = args.initializer\n }\n }\n if (target) keysOf(target, new Set([...seen, name!]))\n else out.add(ANY)\n } else if (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) out.add(p.name.text)\n }\n }\n const walk = (n: ts.Node) => {\n if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name) && n.name.text === 'args') {\n if (ts.isObjectLiteralExpression(n.initializer)) keysOf(n.initializer, new Set())\n else out.add(ANY)\n }\n if ((ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) && tags.includes(n.tagName.getText(sf))) {\n for (const a of n.attributes.properties) {\n if (ts.isJsxAttribute(a)) out.add(a.name.getText(sf))\n else if (ts.isJsxSpreadAttribute(a)) {\n const target = ts.isIdentifier(a.expression) ? consts.get(a.expression.text) : undefined\n if (target) keysOf(target, new Set())\n // `{...args}` hands on what `args` holds, and those are read where they are written.\n else if (!(ts.isIdentifier(a.expression) && a.expression.text === 'args')) out.add(ANY)\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n return out\n}\n\nfunction storyFiles(root: string): string[] {\n const out: string[] = []\n const walk = (dir: string) => {\n for (const name of readdirSync(dir)) {\n if (name === 'node_modules' || name.startsWith('.')) continue\n const path = join(dir, name)\n if (statSync(path).isDirectory()) walk(path)\n else if (/\\.stories\\.tsx?$/.test(name)) out.push(path)\n }\n }\n if (existsSync(join(root, 'src'))) walk(join(root, 'src'))\n return out\n}\n\n/** The props a part hands straight on to another tag: `<QuickActions onReply={onReply} />` \u2192 QuickActions, onReply, onReply. */\nfunction handsOn(fn: ts.FunctionLikeDeclaration, sf: ts.SourceFile): { tag: string; attr: string; prop: string }[] {\n const { locals, object } = propsOf(fn)\n const out: { tag: string; attr: string; prop: string }[] = []\n const walk = (n: ts.Node) => {\n if (n !== fn && ts.isFunctionDeclaration(n)) return\n if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {\n const tag = n.tagName.getText(sf)\n if (/^[A-Z]/.test(tag)) {\n for (const a of n.attributes.properties) {\n if (!ts.isJsxAttribute(a) || !a.initializer || !ts.isJsxExpression(a.initializer) || !a.initializer.expression) continue\n const e = a.initializer.expression\n const prop = ts.isIdentifier(e) ? locals.get(e.text) : object && ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === object ? e.name.text : undefined\n if (prop) out.push({ tag, attr: a.name.getText(sf), prop })\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n if (fn.body) walk(fn.body)\n return out\n}\n\n/** Where a module specifier points, without its extension: `./X`, or Vite's `@/` for `src/`. */\nfunction target(from: string, spec: string, root: string): string | null {\n if (spec.startsWith('.')) return join(dirname(from), spec).replace(/\\.[jt]sx?$/, '')\n if (spec.startsWith('@/')) return join(root, 'src', spec.slice(2)).replace(/\\.[jt]sx?$/, '')\n return null\n}\n\n/**\n * Every prop that gates something a part draws, where some story draws the\n * part and none of those stories passes the prop.\n *\n * \"Draws the part\" means a story file imports the part's own file \u2014 its own\n * stories, or an umbrella sheet such as Peek's Overlays/Menus, which is how\n * TopicMoreMenu lost Rename and Copy link. A gate a part only hands on counts\n * as its own: ConversationCard hands `onReply` to the strip that draws Reply,\n * so a ConversationCard story that never passes it shows no Reply. Followed\n * three parts deep at most.\n */\nexport interface GatedOptions {\n /** The classes checked. The contract's by default. */\n kinds?: readonly string[]\n /**\n * `own`: a part's own stories show everything it can draw \u2014 ConversationCard's\n * stories must show Reply. `anywhere`: a handed-on gate counts as shown when\n * the part it is handed to has a story that shows it. Katerina rules which.\n */\n pictured?: 'own' | 'anywhere'\n}\n\nexport function gatedFindings(registry: Registry, root: string, { kinds = ['reusable', 'promote-candidate'], pictured = 'own' }: GatedOptions = {}): GatedFinding[] {\n const findings: GatedFinding[] = []\n const stories = storyFiles(root).map((file) => ({ file, sf: parse(file, readFileSync(file, 'utf8')) }))\n const sources = new Map<string, ts.SourceFile>()\n const sourceOf = (rel: string): ts.SourceFile | null => {\n const path = join(root, rel)\n if (!sources.has(path)) {\n if (!existsSync(path)) return null\n sources.set(path, parse(path, readFileSync(path, 'utf8')))\n }\n return sources.get(path)!\n }\n const fileOf = new Map(registry.entries.filter((e) => e.kind === 'component').map((e) => [e.name, e.sourceFile]))\n\n /** The story files that import a file, and every prop they pass to the part or its view. */\n const givenTo = new Map<string, { drawn: boolean; given: Set<string> }>()\n const storiesOf = (name: string, rel: string) => {\n const key = `${rel}#${name}`\n if (!givenTo.has(key)) {\n const own = join(root, rel).replace(/\\.[jt]sx?$/, '')\n const drawing = stories.filter(({ file, sf: s }) =>\n s.statements.some((st) => ts.isImportDeclaration(st) && ts.isStringLiteral(st.moduleSpecifier) && target(file, st.moduleSpecifier.text, root) === own),\n )\n givenTo.set(key, { drawn: drawing.length > 0, given: new Set(drawing.flatMap(({ sf: s }) => [...passed(s, [name, `${name}View`])])) })\n }\n return givenTo.get(key)!\n }\n\n const memo = new Map<string, Set<string>>()\n /** The props a named part gates on, its own and those it hands to a part that gates. */\n const gatesOf = (name: string, sf: ts.SourceFile, depth: number): Set<string> => {\n const key = `${sf.fileName}#${name}`\n if (memo.has(key)) return memo.get(key)!\n memo.set(key, new Set())\n const fn = declared(sf, name)\n const out = fn ? gates(fn) : new Set<string>()\n if (fn && depth < 3) {\n for (const { tag, attr, prop } of handsOn(fn, sf)) {\n // A dialog is shut in a story until someone opens it, so nothing it draws\n // is missing from the picture (Katerina, 23 September).\n if (out.has(prop) || /Dialog$/.test(tag)) continue\n const here = declared(sf, tag) ? sf : fileOf.has(tag) ? sourceOf(fileOf.get(tag)!) : null\n if (!here || !gatesOf(tag, here, depth + 1).has(attr)) continue\n // Already pictured one level down: CollapsibleSection hands `trailing` to\n // SectionHeader, and SectionHeader's own story shows the slot.\n const theirs = fileOf.get(tag)\n if (pictured === 'anywhere' && theirs && here !== sf) {\n const { given } = storiesOf(tag, theirs)\n if (given.has(attr) || given.has(ANY)) continue\n }\n out.add(prop)\n }\n }\n memo.set(key, out)\n return out\n }\n\n for (const entry of registry.entries) {\n // The package's entries carry no app class: every one of its components is one.\n const cls = entry.app?.class ?? 'component'\n if (!kinds.includes(cls) || entry.kind !== 'component') continue\n const sf = sourceOf(entry.sourceFile)\n if (!sf) continue\n const { drawn, given } = storiesOf(entry.name, entry.sourceFile)\n // No story draws it at all: the contract's \"drawn nowhere\" says so, not this.\n if (!drawn || given.has(ANY)) continue\n for (const name of [entry.name, `${entry.name}View`]) {\n for (const prop of gatesOf(name, sf, 0)) {\n if (!given.has(prop)) findings.push({ part: entry.name, file: entry.sourceFile.split('\\\\').join('/'), prop, in: name })\n }\n }\n }\n return findings\n}\n"],
|
|
5
|
+
"mappings": ";;;AAkBA,SAAS,YAAY,aAAa,cAAc,gBAAgB;AAChE,SAAmB,SAAS,YAAsB;AAClD,OAAO,QAAQ;AAWf,IAAM,QAAQ,CAAC,MAAc,SAAiB,GAAG,iBAAiB,MAAM,MAAM,GAAG,aAAa,QAAQ,MAAM,GAAG,WAAW,GAAG;AAG7H,SAAS,SAAS,IAAmB,MAAiD;AACpF,MAAI,QAA2C;AAC/C,QAAM,SAAS,CAAC,MAAoE;AAClF,QAAI,CAAC,EAAG,QAAO;AACf,QAAI,GAAG,gBAAgB,CAAC,KAAK,GAAG,qBAAqB,CAAC,EAAG,QAAO;AAChE,QAAI,GAAG,iBAAiB,CAAC,EAAG,QAAO,OAAO,EAAE,UAAU,CAAC,CAAC;AACxD,QAAI,GAAG,0BAA0B,CAAC,KAAK,GAAG,eAAe,CAAC,KAAK,GAAG,sBAAsB,CAAC,EAAG,QAAO,OAAO,EAAE,UAAU;AACtH,WAAO;AAAA,EACT;AACA,aAAW,KAAK,GAAG,YAAY;AAC7B,QAAI,GAAG,sBAAsB,CAAC,KAAK,EAAE,MAAM,SAAS,KAAM,SAAQ;AAClE,QAAI,GAAG,oBAAoB,CAAC;AAAG,iBAAW,KAAK,EAAE,gBAAgB,aAAc,KAAI,GAAG,aAAa,EAAE,IAAI,KAAK,EAAE,KAAK,SAAS,KAAM,SAAQ,OAAO,EAAE,WAAW;AAAA;AAAA,EAClK;AACA,SAAO;AACT;AAGA,SAAS,QAAQ,IAAwF;AACvG,QAAM,SAAS,oBAAI,IAAoB;AACvC,QAAM,QAAQ,GAAG,WAAW,CAAC;AAC7B,MAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,QAAQ,KAAK;AAC1C,MAAI,GAAG,uBAAuB,MAAM,IAAI,GAAG;AACzC,eAAW,MAAM,MAAM,KAAK,UAAU;AACpC,UAAI,GAAG,kBAAkB,CAAC,GAAG,aAAa,GAAG,IAAI,EAAG;AACpD,YAAM,OAAO,GAAG,gBAAgB,GAAG,aAAa,GAAG,YAAY,IAAI,GAAG,aAAa,OAAO,GAAG,KAAK;AAClG,aAAO,IAAI,GAAG,KAAK,MAAM,IAAI;AAAA,IAC/B;AACA,WAAO,EAAE,QAAQ,QAAQ,KAAK;AAAA,EAChC;AACA,SAAO,EAAE,QAAQ,QAAQ,GAAG,aAAa,MAAM,IAAI,IAAI,MAAM,KAAK,OAAO,KAAK;AAChF;AAEA,IAAM,WAAW,CAAC,SAA2B;AAC3C,MAAI,MAAM;AACV,QAAM,OAAO,CAAC,MAAe;AAC3B,QAAI,IAAK;AACT,QAAI,GAAG,aAAa,CAAC,KAAK,GAAG,wBAAwB,CAAC,KAAK,GAAG,cAAc,CAAC,EAAG,OAAM;AAAA,QACjF,IAAG,aAAa,GAAG,IAAI;AAAA,EAC9B;AACA,OAAK,IAAI;AACT,SAAO;AACT;AAGA,SAAS,MAAM,IAA6C;AAC1D,QAAM,EAAE,QAAQ,OAAO,IAAI,QAAQ,EAAE;AACrC,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,SAAS,CAAC,MAAoC;AAClD,WAAO,GAAG,0BAA0B,CAAC,KAAK,GAAG,oBAAoB,CAAC,EAAG,KAAI,EAAE;AAC3E,QAAI,GAAG,wBAAwB,CAAC,KAAK,EAAE,aAAa,GAAG,WAAW,oBAAoB,GAAG,wBAAwB,EAAE,OAAO,EAAG,QAAO,OAAO,EAAE,QAAQ,OAAO;AAC5J,QAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,GAAG,WAAW,wBAAwB,GAAG,WAAW,4BAA4B,EAAE,SAAS,EAAE,cAAc,IAAI,EAAG,QAAO,OAAO,EAAE,IAAI;AACvK,QAAI,GAAG,aAAa,CAAC,EAAG,QAAO,OAAO,IAAI,EAAE,IAAI,KAAK;AACrD,QAAI,UAAU,GAAG,2BAA2B,CAAC,KAAK,GAAG,aAAa,EAAE,UAAU,KAAK,EAAE,WAAW,SAAS,OAAQ,QAAO,EAAE,KAAK;AAC/H,WAAO;AAAA,EACT;AACA,QAAM,OAAO,CAAC,MAAe;AAE3B,QAAI,MAAM,MAAM,GAAG,sBAAsB,CAAC,EAAG;AAC7C,QAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,YAAY;AACzC,YAAM,IAAI,EAAE;AACZ,UAAI,GAAG,mBAAmB,CAAC,KAAK,EAAE,cAAc,SAAS,GAAG,WAAW,2BAA2B,SAAS,EAAE,KAAK,GAAG;AACnH,cAAM,IAAI,OAAO,EAAE,IAAI;AACvB,YAAI,EAAG,KAAI,IAAI,CAAC;AAAA,MAClB;AACA,UAAI,GAAG,wBAAwB,CAAC,KAAK,SAAS,EAAE,QAAQ,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG;AACnF,cAAM,IAAI,OAAO,EAAE,SAAS;AAC5B,YAAI,EAAG,KAAI,IAAI,CAAC;AAAA,MAClB;AAAA,IACF;AACA,OAAG,aAAa,GAAG,IAAI;AAAA,EACzB;AACA,MAAI,GAAG,KAAM,MAAK,GAAG,IAAI;AACzB,SAAO;AACT;AAGA,IAAM,MAAM;AASZ,SAAS,OAAO,IAAmB,MAA6B;AAC9D,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,SAAS,oBAAI,IAAwC;AAC3D,aAAW,KAAK,GAAG,YAAY;AAC7B,QAAI,CAAC,GAAG,oBAAoB,CAAC,EAAG;AAChC,eAAW,KAAK,EAAE,gBAAgB,aAAc,KAAI,GAAG,aAAa,EAAE,IAAI,KAAK,EAAE,eAAe,GAAG,0BAA0B,EAAE,WAAW,EAAG,QAAO,IAAI,EAAE,KAAK,MAAM,EAAE,WAAW;AAAA,EACpL;AACA,QAAM,SAAS,CAAC,GAA+B,SAAsB;AACnE,eAAW,KAAK,EAAE,YAAY;AAC5B,UAAI,GAAG,mBAAmB,CAAC,GAAG;AAE5B,cAAM,IAAI,EAAE;AACZ,cAAM,OAAO,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,2BAA2B,CAAC,KAAK,GAAG,aAAa,EAAE,UAAU,KAAK,EAAE,KAAK,SAAS,SAAS,GAAG,EAAE,WAAW,IAAI,UAAU;AACvK,YAAIA;AACJ,YAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,GAAG;AAC3B,cAAI,CAAC,KAAK,SAAS,OAAO,EAAG,CAAAA,UAAS,OAAO,IAAI,IAAI;AAAA,eAChD;AACH,kBAAM,QAAQ,OAAO,IAAI,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;AACvD,kBAAM,OAAO,OAAO,WAAW,KAAK,CAAC,MAAM,GAAG,qBAAqB,CAAC,KAAK,GAAG,aAAa,EAAE,IAAI,KAAK,EAAE,KAAK,SAAS,MAAM;AAC1H,gBAAI,QAAQ,GAAG,qBAAqB,IAAI,KAAK,GAAG,0BAA0B,KAAK,WAAW,EAAG,CAAAA,UAAS,KAAK;AAAA,UAC7G;AAAA,QACF;AACA,YAAIA,QAAQ,QAAOA,SAAQ,oBAAI,IAAI,CAAC,GAAG,MAAM,IAAK,CAAC,CAAC;AAAA,YAC/C,KAAI,IAAI,GAAG;AAAA,MAClB,WAAW,EAAE,SAAS,GAAG,aAAa,EAAE,IAAI,KAAK,GAAG,gBAAgB,EAAE,IAAI,GAAI,KAAI,IAAI,EAAE,KAAK,IAAI;AAAA,IACnG;AAAA,EACF;AACA,QAAM,OAAO,CAAC,MAAe;AAC3B,QAAI,GAAG,qBAAqB,CAAC,KAAK,GAAG,aAAa,EAAE,IAAI,KAAK,EAAE,KAAK,SAAS,QAAQ;AACnF,UAAI,GAAG,0BAA0B,EAAE,WAAW,EAAG,QAAO,EAAE,aAAa,oBAAI,IAAI,CAAC;AAAA,UAC3E,KAAI,IAAI,GAAG;AAAA,IAClB;AACA,SAAK,GAAG,wBAAwB,CAAC,KAAK,GAAG,oBAAoB,CAAC,MAAM,KAAK,SAAS,EAAE,QAAQ,QAAQ,EAAE,CAAC,GAAG;AACxG,iBAAW,KAAK,EAAE,WAAW,YAAY;AACvC,YAAI,GAAG,eAAe,CAAC,EAAG,KAAI,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;AAAA,iBAC3C,GAAG,qBAAqB,CAAC,GAAG;AACnC,gBAAMA,UAAS,GAAG,aAAa,EAAE,UAAU,IAAI,OAAO,IAAI,EAAE,WAAW,IAAI,IAAI;AAC/E,cAAIA,QAAQ,QAAOA,SAAQ,oBAAI,IAAI,CAAC;AAAA,mBAE3B,EAAE,GAAG,aAAa,EAAE,UAAU,KAAK,EAAE,WAAW,SAAS,QAAS,KAAI,IAAI,GAAG;AAAA,QACxF;AAAA,MACF;AAAA,IACF;AACA,OAAG,aAAa,GAAG,IAAI;AAAA,EACzB;AACA,OAAK,EAAE;AACP,SAAO;AACT;AAEA,SAAS,WAAW,MAAwB;AAC1C,QAAM,MAAgB,CAAC;AACvB,QAAM,OAAO,CAAC,QAAgB;AAC5B,eAAW,QAAQ,YAAY,GAAG,GAAG;AACnC,UAAI,SAAS,kBAAkB,KAAK,WAAW,GAAG,EAAG;AACrD,YAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,UAAI,SAAS,IAAI,EAAE,YAAY,EAAG,MAAK,IAAI;AAAA,eAClC,mBAAmB,KAAK,IAAI,EAAG,KAAI,KAAK,IAAI;AAAA,IACvD;AAAA,EACF;AACA,MAAI,WAAW,KAAK,MAAM,KAAK,CAAC,EAAG,MAAK,KAAK,MAAM,KAAK,CAAC;AACzD,SAAO;AACT;AAGA,SAAS,QAAQ,IAAgC,IAAkE;AACjH,QAAM,EAAE,QAAQ,OAAO,IAAI,QAAQ,EAAE;AACrC,QAAM,MAAqD,CAAC;AAC5D,QAAM,OAAO,CAAC,MAAe;AAC3B,QAAI,MAAM,MAAM,GAAG,sBAAsB,CAAC,EAAG;AAC7C,QAAI,GAAG,wBAAwB,CAAC,KAAK,GAAG,oBAAoB,CAAC,GAAG;AAC9D,YAAM,MAAM,EAAE,QAAQ,QAAQ,EAAE;AAChC,UAAI,SAAS,KAAK,GAAG,GAAG;AACtB,mBAAW,KAAK,EAAE,WAAW,YAAY;AACvC,cAAI,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC,GAAG,gBAAgB,EAAE,WAAW,KAAK,CAAC,EAAE,YAAY,WAAY;AAChH,gBAAM,IAAI,EAAE,YAAY;AACxB,gBAAM,OAAO,GAAG,aAAa,CAAC,IAAI,OAAO,IAAI,EAAE,IAAI,IAAI,UAAU,GAAG,2BAA2B,CAAC,KAAK,GAAG,aAAa,EAAE,UAAU,KAAK,EAAE,WAAW,SAAS,SAAS,EAAE,KAAK,OAAO;AACnL,cAAI,KAAM,KAAI,KAAK,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,GAAG,KAAK,CAAC;AAAA,QAC5D;AAAA,MACF;AAAA,IACF;AACA,OAAG,aAAa,GAAG,IAAI;AAAA,EACzB;AACA,MAAI,GAAG,KAAM,MAAK,GAAG,IAAI;AACzB,SAAO;AACT;AAGA,SAAS,OAAO,MAAc,MAAc,MAA6B;AACvE,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO,KAAK,QAAQ,IAAI,GAAG,IAAI,EAAE,QAAQ,cAAc,EAAE;AACnF,MAAI,KAAK,WAAW,IAAI,EAAG,QAAO,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,CAAC,EAAE,QAAQ,cAAc,EAAE;AAC3F,SAAO;AACT;AAwBO,SAAS,cAAc,UAAoB,MAAc,EAAE,QAAQ,CAAC,YAAY,mBAAmB,GAAG,WAAW,MAAM,IAAkB,CAAC,GAAmB;AAClK,QAAM,WAA2B,CAAC;AAClC,QAAM,UAAU,WAAW,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,IAAI,MAAM,MAAM,aAAa,MAAM,MAAM,CAAC,EAAE,EAAE;AACtG,QAAM,UAAU,oBAAI,IAA2B;AAC/C,QAAM,WAAW,CAAC,QAAsC;AACtD,UAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,QAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;AACtB,UAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,cAAQ,IAAI,MAAM,MAAM,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC;AAAA,IAC3D;AACA,WAAO,QAAQ,IAAI,IAAI;AAAA,EACzB;AACA,QAAM,SAAS,IAAI,IAAI,SAAS,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;AAGhH,QAAM,UAAU,oBAAI,IAAoD;AACxE,QAAM,YAAY,CAAC,MAAc,QAAgB;AAC/C,UAAM,MAAM,GAAG,GAAG,IAAI,IAAI;AAC1B,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,YAAM,MAAM,KAAK,MAAM,GAAG,EAAE,QAAQ,cAAc,EAAE;AACpD,YAAM,UAAU,QAAQ;AAAA,QAAO,CAAC,EAAE,MAAM,IAAI,EAAE,MAC5C,EAAE,WAAW,KAAK,CAAC,OAAO,GAAG,oBAAoB,EAAE,KAAK,GAAG,gBAAgB,GAAG,eAAe,KAAK,OAAO,MAAM,GAAG,gBAAgB,MAAM,IAAI,MAAM,GAAG;AAAA,MACvJ;AACA,cAAQ,IAAI,KAAK,EAAE,OAAO,QAAQ,SAAS,GAAG,OAAO,IAAI,IAAI,QAAQ,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,IACvI;AACA,WAAO,QAAQ,IAAI,GAAG;AAAA,EACxB;AAEA,QAAM,OAAO,oBAAI,IAAyB;AAE1C,QAAM,UAAU,CAAC,MAAc,IAAmB,UAA+B;AAC/E,UAAM,MAAM,GAAG,GAAG,QAAQ,IAAI,IAAI;AAClC,QAAI,KAAK,IAAI,GAAG,EAAG,QAAO,KAAK,IAAI,GAAG;AACtC,SAAK,IAAI,KAAK,oBAAI,IAAI,CAAC;AACvB,UAAM,KAAK,SAAS,IAAI,IAAI;AAC5B,UAAM,MAAM,KAAK,MAAM,EAAE,IAAI,oBAAI,IAAY;AAC7C,QAAI,MAAM,QAAQ,GAAG;AACnB,iBAAW,EAAE,KAAK,MAAM,KAAK,KAAK,QAAQ,IAAI,EAAE,GAAG;AAGjD,YAAI,IAAI,IAAI,IAAI,KAAK,UAAU,KAAK,GAAG,EAAG;AAC1C,cAAM,OAAO,SAAS,IAAI,GAAG,IAAI,KAAK,OAAO,IAAI,GAAG,IAAI,SAAS,OAAO,IAAI,GAAG,CAAE,IAAI;AACrF,YAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,MAAM,QAAQ,CAAC,EAAE,IAAI,IAAI,EAAG;AAGvD,cAAM,SAAS,OAAO,IAAI,GAAG;AAC7B,YAAI,aAAa,cAAc,UAAU,SAAS,IAAI;AACpD,gBAAM,EAAE,MAAM,IAAI,UAAU,KAAK,MAAM;AACvC,cAAI,MAAM,IAAI,IAAI,KAAK,MAAM,IAAI,GAAG,EAAG;AAAA,QACzC;AACA,YAAI,IAAI,IAAI;AAAA,MACd;AAAA,IACF;AACA,SAAK,IAAI,KAAK,GAAG;AACjB,WAAO;AAAA,EACT;AAEA,aAAW,SAAS,SAAS,SAAS;AAEpC,UAAM,MAAM,MAAM,KAAK,SAAS;AAChC,QAAI,CAAC,MAAM,SAAS,GAAG,KAAK,MAAM,SAAS,YAAa;AACxD,UAAM,KAAK,SAAS,MAAM,UAAU;AACpC,QAAI,CAAC,GAAI;AACT,UAAM,EAAE,OAAO,MAAM,IAAI,UAAU,MAAM,MAAM,MAAM,UAAU;AAE/D,QAAI,CAAC,SAAS,MAAM,IAAI,GAAG,EAAG;AAC9B,eAAW,QAAQ,CAAC,MAAM,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG;AACpD,iBAAW,QAAQ,QAAQ,MAAM,IAAI,CAAC,GAAG;AACvC,YAAI,CAAC,MAAM,IAAI,IAAI,EAAG,UAAS,KAAK,EAAE,MAAM,MAAM,MAAM,MAAM,MAAM,WAAW,MAAM,IAAI,EAAE,KAAK,GAAG,GAAG,MAAM,IAAI,KAAK,CAAC;AAAA,MACxH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": ["target"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Registry } from './schema';
|
|
2
|
+
export interface GatedFinding {
|
|
3
|
+
part: string;
|
|
4
|
+
file: string;
|
|
5
|
+
prop: string;
|
|
6
|
+
/** The part or view whose body holds the gate. */
|
|
7
|
+
in: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Every prop that gates something a part draws, where some story draws the
|
|
11
|
+
* part and none of those stories passes the prop.
|
|
12
|
+
*
|
|
13
|
+
* "Draws the part" means a story file imports the part's own file — its own
|
|
14
|
+
* stories, or an umbrella sheet such as Peek's Overlays/Menus, which is how
|
|
15
|
+
* TopicMoreMenu lost Rename and Copy link. A gate a part only hands on counts
|
|
16
|
+
* as its own: ConversationCard hands `onReply` to the strip that draws Reply,
|
|
17
|
+
* so a ConversationCard story that never passes it shows no Reply. Followed
|
|
18
|
+
* three parts deep at most.
|
|
19
|
+
*/
|
|
20
|
+
export interface GatedOptions {
|
|
21
|
+
/** The classes checked. The contract's by default. */
|
|
22
|
+
kinds?: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* `own`: a part's own stories show everything it can draw — ConversationCard's
|
|
25
|
+
* stories must show Reply. `anywhere`: a handed-on gate counts as shown when
|
|
26
|
+
* the part it is handed to has a story that shows it. Katerina rules which.
|
|
27
|
+
*/
|
|
28
|
+
pictured?: 'own' | 'anywhere';
|
|
29
|
+
}
|
|
30
|
+
export declare function gatedFindings(registry: Registry, root: string, { kinds, pictured }?: GatedOptions): GatedFinding[];
|
|
31
|
+
//# sourceMappingURL=gated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gated.d.ts","sourceRoot":"","sources":["../../src/registry/gated.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAA;CACX;AAuLD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,UAAU,CAAA;CAC9B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,KAAyC,EAAE,QAAgB,EAAE,GAAE,YAAiB,GAAG,YAAY,EAAE,CAyElK"}
|
package/dist/registry/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export { buildRegistry, readIndexExports, serializeRegistry, type BuildOptions } from './build';
|
|
14
14
|
export { buildAppRegistry, type AppBuildOptions } from './app';
|
|
15
|
+
export { CONTRACT_KINDS, PAGE_SECTIONS, contractProblems, hasSeenIn, linkProblems, pageProblem } from './contract';
|
|
15
16
|
export { docsLink, findInRegistries, findInRegistry, formatFindings, type Finding } from './find';
|
|
16
17
|
export { CLASSES, SCHEMA_VERSION, validateRegistry, type AppFacts, type EntryBehaviour, type EntryClass, type FileWithoutPart, type EntryKind, type EntryStatus, type EntryVariant, type PurposeSource, type Registry, type RegistryEntry, type RegistryExclusion, } from './schema';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AAC/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAA;AACjG,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAA;AAC/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAClH,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAA;AACjG,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAA"}
|
package/dist/registry/index.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONTRACT_KINDS,
|
|
4
|
+
PAGE_SECTIONS,
|
|
5
|
+
contractProblems,
|
|
3
6
|
docsLink,
|
|
4
7
|
findInRegistries,
|
|
5
8
|
findInRegistry,
|
|
6
|
-
formatFindings
|
|
7
|
-
|
|
9
|
+
formatFindings,
|
|
10
|
+
hasSeenIn,
|
|
11
|
+
linkProblems,
|
|
12
|
+
pageProblem
|
|
13
|
+
} from "./chunk-6OH3TWGF.js";
|
|
8
14
|
import {
|
|
9
15
|
buildAppRegistry
|
|
10
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-XCBN2EKW.js";
|
|
11
17
|
import {
|
|
12
18
|
buildRegistry,
|
|
13
19
|
readIndexExports,
|
|
@@ -20,13 +26,19 @@ import {
|
|
|
20
26
|
} from "./chunk-E4JNV7PC.js";
|
|
21
27
|
export {
|
|
22
28
|
CLASSES,
|
|
29
|
+
CONTRACT_KINDS,
|
|
30
|
+
PAGE_SECTIONS,
|
|
23
31
|
SCHEMA_VERSION,
|
|
24
32
|
buildAppRegistry,
|
|
25
33
|
buildRegistry,
|
|
34
|
+
contractProblems,
|
|
26
35
|
docsLink,
|
|
27
36
|
findInRegistries,
|
|
28
37
|
findInRegistry,
|
|
29
38
|
formatFindings,
|
|
39
|
+
hasSeenIn,
|
|
40
|
+
linkProblems,
|
|
41
|
+
pageProblem,
|
|
30
42
|
readIndexExports,
|
|
31
43
|
serializeRegistry,
|
|
32
44
|
validateRegistry
|
package/package.json
CHANGED