@olwiba/dx 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <p align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/public/olwibaDX--light.gif" />
4
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/public/olwibaDX.gif" />
5
- <img src="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/public/olwibaDX.gif" alt="olwibaDX" style="width: 100%;" />
3
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/.github/assets/olwibaDX--light.gif" />
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/.github/assets/olwibaDX.gif" />
5
+ <img src="https://raw.githubusercontent.com/Olwiba/olwibaDX/master/.github/assets/olwibaDX.gif" alt="olwibaDX" style="width: 100%;" />
6
6
  </picture>
7
7
  </p>
8
8
 
@@ -78,7 +78,7 @@ Render an animated ASCII GIF from any text. This is what produced the README ban
78
78
  bunx @olwiba/dx ascii-gif \
79
79
  --text "olwibaDX" \
80
80
  --accent "DX" \
81
- --out ./public/olwibaDX.gif
81
+ --out ./.github/assets/olwibaDX.gif
82
82
  ```
83
83
 
84
84
  ### Asset Generator
package/dist/cli.js CHANGED
@@ -2487,6 +2487,143 @@ var init_env_check = __esm({
2487
2487
  }
2488
2488
  });
2489
2489
 
2490
+ // src/docs-check.ts
2491
+ var docs_check_exports = {};
2492
+ __export(docs_check_exports, {
2493
+ checkDocs: () => checkDocs,
2494
+ formatDocsReport: () => formatDocsReport
2495
+ });
2496
+ function withoutCode(source) {
2497
+ return source.replace(/^[ \t]*(`{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm, "").replace(/`[^`\n]*`/g, "");
2498
+ }
2499
+ function optOutIsExplained(source) {
2500
+ const match = source.match(/api-reference:\s*none\b([\s\S]{0,400})/);
2501
+ if (!match) return false;
2502
+ const tail = match[1] ?? "";
2503
+ const end = tail.indexOf("*/");
2504
+ const within = end === -1 ? tail : tail.slice(0, end);
2505
+ const words = within.replace(/^[\s—\-–:,.]+/, "").replace(/^\s*\*/gm, "").trim();
2506
+ return words.split(/\s+/).filter(Boolean).length >= 3;
2507
+ }
2508
+ function extractPropsExpressions(source) {
2509
+ const found = [];
2510
+ const marker = "props=";
2511
+ let index = source.indexOf(marker);
2512
+ while (index !== -1) {
2513
+ let cursor = index + marker.length;
2514
+ if (source[cursor] !== "{") {
2515
+ index = source.indexOf(marker, cursor);
2516
+ continue;
2517
+ }
2518
+ let depth = 0;
2519
+ const start = cursor;
2520
+ for (; cursor < source.length; cursor += 1) {
2521
+ if (source[cursor] === "{") depth += 1;
2522
+ else if (source[cursor] === "}") {
2523
+ depth -= 1;
2524
+ if (depth === 0) break;
2525
+ }
2526
+ }
2527
+ found.push(source.slice(start + 1, cursor));
2528
+ index = source.indexOf(marker, cursor);
2529
+ }
2530
+ return found;
2531
+ }
2532
+ function propsFailure(source) {
2533
+ for (const expression of extractPropsExpressions(source)) {
2534
+ let value;
2535
+ try {
2536
+ value = new Function(`return (${expression});`)();
2537
+ } catch (error) {
2538
+ return error instanceof Error ? error.message : String(error);
2539
+ }
2540
+ if (!Array.isArray(value)) return "props is not an array";
2541
+ for (const entry of value) {
2542
+ if (typeof entry?.name !== "string" || typeof entry?.type !== "string") {
2543
+ return `a prop entry is missing name or type: ${JSON.stringify(entry)}`;
2544
+ }
2545
+ }
2546
+ }
2547
+ return null;
2548
+ }
2549
+ function checkDocs(pages) {
2550
+ const findings = [];
2551
+ let okCount = 0;
2552
+ let exemptCount = 0;
2553
+ let skippedCount = 0;
2554
+ for (const page of pages) {
2555
+ const rendered = withoutCode(page.source);
2556
+ const hasPreview = PREVIEW.test(rendered);
2557
+ const hasReference = API_REFERENCE.test(rendered);
2558
+ const optedOut = OPT_OUT.test(rendered);
2559
+ const broken = hasReference ? propsFailure(page.source) : null;
2560
+ if (broken) {
2561
+ findings.push({ kind: "broken-props", file: page.file, reason: broken });
2562
+ continue;
2563
+ }
2564
+ if (optedOut && hasReference) {
2565
+ findings.push({ kind: "redundant-opt-out", file: page.file });
2566
+ continue;
2567
+ }
2568
+ if (!hasPreview) {
2569
+ skippedCount += 1;
2570
+ continue;
2571
+ }
2572
+ if (optedOut) {
2573
+ if (optOutIsExplained(rendered)) exemptCount += 1;
2574
+ else findings.push({ kind: "unexplained-opt-out", file: page.file });
2575
+ continue;
2576
+ }
2577
+ if (hasReference) okCount += 1;
2578
+ else findings.push({ kind: "missing", file: page.file });
2579
+ }
2580
+ return { findings, okCount, exemptCount, skippedCount };
2581
+ }
2582
+ function formatDocsReport(result) {
2583
+ const lines = [];
2584
+ for (const finding of result.findings) {
2585
+ if (finding.kind === "missing") {
2586
+ lines.push(
2587
+ ` ${finding.file}`,
2588
+ " Shows a preview but has no <APIReference>. Add one, or opt out with",
2589
+ " {/* api-reference: none \u2014 why */} if there is no public API here."
2590
+ );
2591
+ } else if (finding.kind === "broken-props") {
2592
+ lines.push(
2593
+ ` ${finding.file}`,
2594
+ ` Its <APIReference> props are not valid JavaScript, so the page will not render.`,
2595
+ ` ${finding.reason}`
2596
+ );
2597
+ } else if (finding.kind === "unexplained-opt-out") {
2598
+ lines.push(
2599
+ ` ${finding.file}`,
2600
+ " Opts out of the API reference without a reason. Write one after the marker."
2601
+ );
2602
+ } else {
2603
+ lines.push(
2604
+ ` ${finding.file}`,
2605
+ " Opts out of the API reference and then has one. Remove the stale marker."
2606
+ );
2607
+ }
2608
+ }
2609
+ const tally = `${result.okCount} documented, ${result.exemptCount} exempt, ${result.skippedCount} without a preview`;
2610
+ if (result.findings.length === 0) {
2611
+ return `All documentation pages that show a component document it (${tally}).`;
2612
+ }
2613
+ return `${result.findings.length} documentation page(s) need attention:
2614
+ ${lines.join("\n")}
2615
+
2616
+ ${tally}`;
2617
+ }
2618
+ var OPT_OUT, PREVIEW, API_REFERENCE;
2619
+ var init_docs_check = __esm({
2620
+ "src/docs-check.ts"() {
2621
+ OPT_OUT = /api-reference:\s*none\b/;
2622
+ PREVIEW = /<(Sandbox|ComponentPreview)\b/;
2623
+ API_REFERENCE = /<APIReference\b/;
2624
+ }
2625
+ });
2626
+
2490
2627
  // src/skills.ts
2491
2628
  function isSafeSkillSlug(slug) {
2492
2629
  return slug !== "." && slug !== ".." && /^[A-Za-z0-9._-]+$/.test(slug);
@@ -2513,9 +2650,11 @@ if (command === "skills" && subcommand === "install") {
2513
2650
  await runGenerateAssets();
2514
2651
  } else if (command === "env-check" || command === "env") {
2515
2652
  process.exitCode = await runEnvCheck();
2653
+ } else if (command === "docs-check") {
2654
+ process.exitCode = await runDocsCheck();
2516
2655
  } else {
2517
2656
  process.stdout.write(
2518
- "Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx worktree cleanup [repo-name-or-path] [--repos-root <path>] [--remote <name>] [--dry-run] [--force] [--no-fetch]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
2657
+ "Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx worktree cleanup [repo-name-or-path] [--repos-root <path>] [--remote <name>] [--dry-run] [--force] [--no-fetch]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n dx env-check [--example <.env.example>] [--file <.env>] [--optional a,b,c]\n dx docs-check [--dir <content/docs>]\n"
2519
2658
  );
2520
2659
  }
2521
2660
  async function runSkillsInstall() {
@@ -2773,6 +2912,38 @@ async function runEnvCheck() {
2773
2912
  process.stdout.write(`${formatEnvReport2(result)}${BREAK}`);
2774
2913
  return result.findings.length > 0 ? 1 : 0;
2775
2914
  }
2915
+ async function runDocsCheck() {
2916
+ const { readFileSync: readFileSync3, existsSync: existsSync2, readdirSync: readdirSync2 } = await import('fs');
2917
+ const { join: join4, relative: relative2, sep: sep2 } = await import('path');
2918
+ const { checkDocs: checkDocs2, formatDocsReport: formatDocsReport2 } = await Promise.resolve().then(() => (init_docs_check(), docs_check_exports));
2919
+ const flags = parseFlags(process.argv.slice(3));
2920
+ const dir = flags.dir ?? join4("content", "docs");
2921
+ if (!existsSync2(dir)) {
2922
+ process.stderr.write(
2923
+ `No documentation directory at ${dir}.${BREAK}Pass --dir if this project keeps its pages elsewhere.` + BREAK
2924
+ );
2925
+ return 1;
2926
+ }
2927
+ const files = [];
2928
+ const walk = (current) => {
2929
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
2930
+ const full = join4(current, entry.name);
2931
+ if (entry.isDirectory()) walk(full);
2932
+ else if (entry.name.endsWith(".mdx")) files.push(full);
2933
+ }
2934
+ };
2935
+ walk(dir);
2936
+ const result = checkDocs2(
2937
+ files.sort().map((file) => ({
2938
+ // Reported the same way on every platform, so a Windows run and a CI run
2939
+ // produce comparable output.
2940
+ file: relative2(process.cwd(), file).split(sep2).join("/"),
2941
+ source: readFileSync3(file, "utf8")
2942
+ }))
2943
+ );
2944
+ process.stdout.write(`${formatDocsReport2(result)}${BREAK}`);
2945
+ return result.findings.length > 0 ? 1 : 0;
2946
+ }
2776
2947
  async function readAllStdin() {
2777
2948
  const chunks = [];
2778
2949
  for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Checks that every documentation page showing a component also documents it.
3
+ *
4
+ * A docs page that renders a preview is making a promise: this is a component
5
+ * you can use. The properties table is the other half of that promise, and it
6
+ * is the half that goes missing, because a page reads as finished long before
7
+ * anyone writes the props down. Fourteen marketing pages and an `ErrorPage`
8
+ * page that said in prose "takes no props" — it has seven — shipped that way
9
+ * and nobody noticed until someone opened one on a phone.
10
+ *
11
+ * So the rule is mechanical: a preview implies an `<APIReference>`. Pages that
12
+ * genuinely have no public surface to document opt out in the file, in a
13
+ * comment that has to carry a reason.
14
+ */
15
+ type DocsFinding = {
16
+ kind: 'missing';
17
+ file: string;
18
+ }
19
+ /** Opted out without saying why, which is how an opt-out becomes a habit. */
20
+ | {
21
+ kind: 'unexplained-opt-out';
22
+ file: string;
23
+ }
24
+ /** Opted out and then documented it anyway — the comment is stale. */
25
+ | {
26
+ kind: 'redundant-opt-out';
27
+ file: string;
28
+ }
29
+ /** The props array is not valid JavaScript, so the page cannot render. */
30
+ | {
31
+ kind: 'broken-props';
32
+ file: string;
33
+ reason: string;
34
+ };
35
+ interface DocsCheckResult {
36
+ findings: DocsFinding[];
37
+ /** Pages with a preview and a reference. Counted, not listed. */
38
+ okCount: number;
39
+ /** Pages with a preview and an explained opt-out. */
40
+ exemptCount: number;
41
+ /** Pages with no preview, which this says nothing about. */
42
+ skippedCount: number;
43
+ }
44
+ interface DocsPage {
45
+ /** Path as it should be reported — repository-relative, forward slashes. */
46
+ file: string;
47
+ source: string;
48
+ }
49
+ declare function checkDocs(pages: DocsPage[]): DocsCheckResult;
50
+ declare function formatDocsReport(result: DocsCheckResult): string;
51
+
52
+ export { type DocsCheckResult, type DocsFinding, type DocsPage, checkDocs, formatDocsReport };
@@ -0,0 +1,130 @@
1
+ // src/docs-check.ts
2
+ var OPT_OUT = /api-reference:\s*none\b/;
3
+ var PREVIEW = /<(Sandbox|ComponentPreview)\b/;
4
+ var API_REFERENCE = /<APIReference\b/;
5
+ function withoutCode(source) {
6
+ return source.replace(/^[ \t]*(`{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm, "").replace(/`[^`\n]*`/g, "");
7
+ }
8
+ function optOutIsExplained(source) {
9
+ const match = source.match(/api-reference:\s*none\b([\s\S]{0,400})/);
10
+ if (!match) return false;
11
+ const tail = match[1] ?? "";
12
+ const end = tail.indexOf("*/");
13
+ const within = end === -1 ? tail : tail.slice(0, end);
14
+ const words = within.replace(/^[\s—\-–:,.]+/, "").replace(/^\s*\*/gm, "").trim();
15
+ return words.split(/\s+/).filter(Boolean).length >= 3;
16
+ }
17
+ function extractPropsExpressions(source) {
18
+ const found = [];
19
+ const marker = "props=";
20
+ let index = source.indexOf(marker);
21
+ while (index !== -1) {
22
+ let cursor = index + marker.length;
23
+ if (source[cursor] !== "{") {
24
+ index = source.indexOf(marker, cursor);
25
+ continue;
26
+ }
27
+ let depth = 0;
28
+ const start = cursor;
29
+ for (; cursor < source.length; cursor += 1) {
30
+ if (source[cursor] === "{") depth += 1;
31
+ else if (source[cursor] === "}") {
32
+ depth -= 1;
33
+ if (depth === 0) break;
34
+ }
35
+ }
36
+ found.push(source.slice(start + 1, cursor));
37
+ index = source.indexOf(marker, cursor);
38
+ }
39
+ return found;
40
+ }
41
+ function propsFailure(source) {
42
+ for (const expression of extractPropsExpressions(source)) {
43
+ let value;
44
+ try {
45
+ value = new Function(`return (${expression});`)();
46
+ } catch (error) {
47
+ return error instanceof Error ? error.message : String(error);
48
+ }
49
+ if (!Array.isArray(value)) return "props is not an array";
50
+ for (const entry of value) {
51
+ if (typeof entry?.name !== "string" || typeof entry?.type !== "string") {
52
+ return `a prop entry is missing name or type: ${JSON.stringify(entry)}`;
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ function checkDocs(pages) {
59
+ const findings = [];
60
+ let okCount = 0;
61
+ let exemptCount = 0;
62
+ let skippedCount = 0;
63
+ for (const page of pages) {
64
+ const rendered = withoutCode(page.source);
65
+ const hasPreview = PREVIEW.test(rendered);
66
+ const hasReference = API_REFERENCE.test(rendered);
67
+ const optedOut = OPT_OUT.test(rendered);
68
+ const broken = hasReference ? propsFailure(page.source) : null;
69
+ if (broken) {
70
+ findings.push({ kind: "broken-props", file: page.file, reason: broken });
71
+ continue;
72
+ }
73
+ if (optedOut && hasReference) {
74
+ findings.push({ kind: "redundant-opt-out", file: page.file });
75
+ continue;
76
+ }
77
+ if (!hasPreview) {
78
+ skippedCount += 1;
79
+ continue;
80
+ }
81
+ if (optedOut) {
82
+ if (optOutIsExplained(rendered)) exemptCount += 1;
83
+ else findings.push({ kind: "unexplained-opt-out", file: page.file });
84
+ continue;
85
+ }
86
+ if (hasReference) okCount += 1;
87
+ else findings.push({ kind: "missing", file: page.file });
88
+ }
89
+ return { findings, okCount, exemptCount, skippedCount };
90
+ }
91
+ function formatDocsReport(result) {
92
+ const lines = [];
93
+ for (const finding of result.findings) {
94
+ if (finding.kind === "missing") {
95
+ lines.push(
96
+ ` ${finding.file}`,
97
+ " Shows a preview but has no <APIReference>. Add one, or opt out with",
98
+ " {/* api-reference: none \u2014 why */} if there is no public API here."
99
+ );
100
+ } else if (finding.kind === "broken-props") {
101
+ lines.push(
102
+ ` ${finding.file}`,
103
+ ` Its <APIReference> props are not valid JavaScript, so the page will not render.`,
104
+ ` ${finding.reason}`
105
+ );
106
+ } else if (finding.kind === "unexplained-opt-out") {
107
+ lines.push(
108
+ ` ${finding.file}`,
109
+ " Opts out of the API reference without a reason. Write one after the marker."
110
+ );
111
+ } else {
112
+ lines.push(
113
+ ` ${finding.file}`,
114
+ " Opts out of the API reference and then has one. Remove the stale marker."
115
+ );
116
+ }
117
+ }
118
+ const tally = `${result.okCount} documented, ${result.exemptCount} exempt, ${result.skippedCount} without a preview`;
119
+ if (result.findings.length === 0) {
120
+ return `All documentation pages that show a component document it (${tally}).`;
121
+ }
122
+ return `${result.findings.length} documentation page(s) need attention:
123
+ ${lines.join("\n")}
124
+
125
+ ${tally}`;
126
+ }
127
+
128
+ export { checkDocs, formatDocsReport };
129
+ //# sourceMappingURL=docs-check.js.map
130
+ //# sourceMappingURL=docs-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/docs-check.ts"],"names":[],"mappings":"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","file":"docs-check.js","sourcesContent":["/**\n * Checks that every documentation page showing a component also documents it.\n *\n * A docs page that renders a preview is making a promise: this is a component\n * you can use. The properties table is the other half of that promise, and it\n * is the half that goes missing, because a page reads as finished long before\n * anyone writes the props down. Fourteen marketing pages and an `ErrorPage`\n * page that said in prose \"takes no props\" — it has seven — shipped that way\n * and nobody noticed until someone opened one on a phone.\n *\n * So the rule is mechanical: a preview implies an `<APIReference>`. Pages that\n * genuinely have no public surface to document opt out in the file, in a\n * comment that has to carry a reason.\n */\n\n/** Marks a page as having nothing to document, and says why. */\nconst OPT_OUT = /api-reference:\\s*none\\b/;\n\n/** What counts as showing a component off. */\nconst PREVIEW = /<(Sandbox|ComponentPreview)\\b/;\n\nconst API_REFERENCE = /<APIReference\\b/;\n\n/**\n * Drops fenced and inline code before looking for markers.\n *\n * A page that quotes `<Sandbox>` in a table, or shows what the opt-out comment\n * looks like inside a fence, is describing the markers rather than using them —\n * nothing is rendered and nothing is claimed. Matching the raw source made the\n * checker's own documentation page its first false positive, which is the sort\n * of rule that gets switched off rather than fixed.\n *\n * Fences go first so an unpaired backtick inside one cannot swallow the rest of\n * the file.\n */\nfunction withoutCode(source: string): string {\n return source.replace(/^[ \\t]*(`{3,}|~{3,})[\\s\\S]*?^[ \\t]*\\1[ \\t]*$/gm, '').replace(/`[^`\\n]*`/g, '');\n}\n\nexport type DocsFinding =\n | { kind: 'missing'; file: string }\n /** Opted out without saying why, which is how an opt-out becomes a habit. */\n | { kind: 'unexplained-opt-out'; file: string }\n /** Opted out and then documented it anyway — the comment is stale. */\n | { kind: 'redundant-opt-out'; file: string }\n /** The props array is not valid JavaScript, so the page cannot render. */\n | { kind: 'broken-props'; file: string; reason: string };\n\nexport interface DocsCheckResult {\n findings: DocsFinding[];\n /** Pages with a preview and a reference. Counted, not listed. */\n okCount: number;\n /** Pages with a preview and an explained opt-out. */\n exemptCount: number;\n /** Pages with no preview, which this says nothing about. */\n skippedCount: number;\n}\n\nexport interface DocsPage {\n /** Path as it should be reported — repository-relative, forward slashes. */\n file: string;\n source: string;\n}\n\n/**\n * An opt-out needs a reason after the marker, on the same line or the ones\n * following it inside the same comment. `api-reference: none` on its own is\n * rejected: the point of the escape hatch is that using it is a decision\n * somebody wrote down.\n */\nfunction optOutIsExplained(source: string): boolean {\n const match = source.match(/api-reference:\\s*none\\b([\\s\\S]{0,400})/);\n if (!match) return false;\n\n const tail = match[1] ?? '';\n // Stop at the end of the comment the marker sits in, so prose further down\n // the page cannot pass for a justification.\n const end = tail.indexOf('*/');\n const within = end === -1 ? tail : tail.slice(0, end);\n\n // Strip the punctuation an author uses to introduce the reason, plus the\n // comment gutter, and see whether any words are left.\n const words = within\n .replace(/^[\\s—\\-–:,.]+/, '')\n .replace(/^\\s*\\*/gm, '')\n .trim();\n\n return words.split(/\\s+/).filter(Boolean).length >= 3;\n}\n\n/**\n * Every balanced `{...}` following a `props=` attribute.\n *\n * Matched by counting braces rather than by regex: the prop tables contain\n * object literals, so the first `}` is nowhere near the end of the attribute.\n */\nfunction extractPropsExpressions(source: string): string[] {\n const found: string[] = [];\n const marker = 'props=';\n let index = source.indexOf(marker);\n\n while (index !== -1) {\n let cursor = index + marker.length;\n if (source[cursor] !== '{') {\n index = source.indexOf(marker, cursor);\n continue;\n }\n\n let depth = 0;\n const start = cursor;\n for (; cursor < source.length; cursor += 1) {\n if (source[cursor] === '{') depth += 1;\n else if (source[cursor] === '}') {\n depth -= 1;\n if (depth === 0) break;\n }\n }\n\n found.push(source.slice(start + 1, cursor));\n index = source.indexOf(marker, cursor);\n }\n\n return found;\n}\n\n/**\n * Confirms the prop table is JavaScript that runs.\n *\n * MDX does not evaluate a page's expressions until something renders it, so a\n * mis-escaped quote inside a description is invisible to every build step and\n * only shows up as a blank page. Evaluating the array here is the cheapest way\n * to find that out — the source being run is the repository's own.\n */\nfunction propsFailure(source: string): string | null {\n for (const expression of extractPropsExpressions(source)) {\n let value: unknown;\n try {\n value = new Function(`return (${expression});`)();\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\n }\n\n if (!Array.isArray(value)) return 'props is not an array';\n\n for (const entry of value as Array<Record<string, unknown>>) {\n if (typeof entry?.name !== 'string' || typeof entry?.type !== 'string') {\n return `a prop entry is missing name or type: ${JSON.stringify(entry)}`;\n }\n }\n }\n\n return null;\n}\n\nexport function checkDocs(pages: DocsPage[]): DocsCheckResult {\n const findings: DocsFinding[] = [];\n let okCount = 0;\n let exemptCount = 0;\n let skippedCount = 0;\n\n for (const page of pages) {\n // Markers are looked for in what the page renders, not in what it quotes.\n const rendered = withoutCode(page.source);\n const hasPreview = PREVIEW.test(rendered);\n const hasReference = API_REFERENCE.test(rendered);\n const optedOut = OPT_OUT.test(rendered);\n\n // Checked before anything else: a page whose table cannot run has a worse\n // problem than a page that has no table. Read from the raw source: a props\n // table is an expression MDX evaluates, and stripping inline code would\n // change it.\n const broken = hasReference ? propsFailure(page.source) : null;\n if (broken) {\n findings.push({ kind: 'broken-props', file: page.file, reason: broken });\n continue;\n }\n\n if (optedOut && hasReference) {\n findings.push({ kind: 'redundant-opt-out', file: page.file });\n continue;\n }\n\n if (!hasPreview) {\n // Index pages, guides, concept pages. Nothing is being shown off, so\n // there is nothing this can reasonably demand.\n skippedCount += 1;\n continue;\n }\n\n if (optedOut) {\n if (optOutIsExplained(rendered)) exemptCount += 1;\n else findings.push({ kind: 'unexplained-opt-out', file: page.file });\n continue;\n }\n\n if (hasReference) okCount += 1;\n else findings.push({ kind: 'missing', file: page.file });\n }\n\n return { findings, okCount, exemptCount, skippedCount };\n}\n\nexport function formatDocsReport(result: DocsCheckResult): string {\n const lines: string[] = [];\n\n for (const finding of result.findings) {\n if (finding.kind === 'missing') {\n lines.push(\n ` ${finding.file}`,\n ' Shows a preview but has no <APIReference>. Add one, or opt out with',\n ' {/* api-reference: none — why */} if there is no public API here.',\n );\n } else if (finding.kind === 'broken-props') {\n lines.push(\n ` ${finding.file}`,\n ` Its <APIReference> props are not valid JavaScript, so the page will not render.`,\n ` ${finding.reason}`,\n );\n } else if (finding.kind === 'unexplained-opt-out') {\n lines.push(\n ` ${finding.file}`,\n ' Opts out of the API reference without a reason. Write one after the marker.',\n );\n } else {\n lines.push(\n ` ${finding.file}`,\n ' Opts out of the API reference and then has one. Remove the stale marker.',\n );\n }\n }\n\n const tally =\n `${result.okCount} documented, ${result.exemptCount} exempt, ` +\n `${result.skippedCount} without a preview`;\n\n if (result.findings.length === 0) {\n return `All documentation pages that show a component document it (${tally}).`;\n }\n\n return (\n `${result.findings.length} documentation page(s) need attention:\\n` +\n `${lines.join('\\n')}\\n\\n${tally}`\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/dx",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -52,16 +52,46 @@
52
52
  "build": "tsup",
53
53
  "dev": "tsup --watch",
54
54
  "test": "bun test",
55
- "tsc": "tsc --noEmit"
55
+ "tsc": "tsc --noEmit",
56
+ "web:build": "fumadocs-mdx && vite build",
57
+ "web:dev": "fumadocs-mdx && vite dev",
58
+ "start": "bun run server.ts",
59
+ "types:check": "fumadocs-mdx && tsc --noEmit && tsc --noEmit -p tsconfig.site.json",
60
+ "presets:generate": "bun scripts/generate-env-presets.ts",
61
+ "docs:check": "bun src/cli.ts docs-check",
62
+ "env:check": "bun src/cli.ts env-check",
63
+ "showcase:build": "bun scripts/build-showcase.ts"
56
64
  },
57
65
  "devDependencies": {
66
+ "@olwiba/cn": "0.1.41",
67
+ "@olwiba/docs": "0.1.47",
68
+ "@tailwindcss/vite": "^4.1.18",
69
+ "@tanstack/react-router": "1.154.8",
70
+ "@tanstack/react-start": "1.154.8",
71
+ "@types/mdx": "^2.0.13",
58
72
  "@types/node": "^22.5.4",
73
+ "@types/react": "^19.0.8",
74
+ "@types/react-dom": "^19.0.3",
75
+ "@vitejs/plugin-react": "^4.6.0",
59
76
  "eslint": "^9.39.5",
77
+ "fumadocs-core": "^16.4.7",
78
+ "fumadocs-mdx": "^14.2.6",
79
+ "fumadocs-ui": "^16.4.7",
80
+ "lucide-react": "^0.562.0",
60
81
  "lucide-static": "^0.488.0",
82
+ "next-themes": "^0.4.6",
61
83
  "puppeteer-core": "^25.9.0",
84
+ "react": "^19.0.0",
85
+ "react-dom": "^19.0.0",
86
+ "rehype-pretty-code": "^0.14.0",
62
87
  "sharp": "^0.35.0",
88
+ "shiki": "^3.0.0",
89
+ "tailwindcss": "^4.1.18",
63
90
  "tsup": "^8.4.0",
64
- "typescript": "^5.7.2"
91
+ "tw-animate-css": "^1.4.0",
92
+ "typescript": "^5.7.2",
93
+ "vite": "^7.3.1",
94
+ "vite-tsconfig-paths": "^5.1.4"
65
95
  },
66
96
  "peerDependencies": {
67
97
  "@typescript-eslint/eslint-plugin": ">=8.0.0",