Skip to content
Source
Sign inSign up
Open History to see the last commit

fix-api-jsdocs.ts

160 lines4.8 KB

1// Normalizes API-generator JSDoc in package sources: moves the generator
2// metadata tags (`@resource`, `@product`, …) to the end of the block and
3// rewrites legacy `@section` / `@example` tags into `###` / `**Example:**`.
4//
5// Usage: node scripts/fix-api-jsdocs.ts [--check]
6import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
7import * as NodeServices from "@effect/platform-node/NodeServices";
8import { Command, Flag } from "effect/cli";
9import * as Console from "effect/Console";
10import * as Data from "effect/Data";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Path from "effect/Path";
14import * as Runtime from "effect/Runtime";
15​
16/** Packages whose `src/` carries API-generator JSDoc. */
17const packages = [
18 "alchemy",
19 "better-auth",
20 "git",
21 "drizzle",
22 "acme",
23 "axiom",
24 "doppler",
25 "infisical",
26 "turso",
27 "planetscale",
28 "neon",
29 "prisma",
30 "hetzner",
31 "railway",
32 "fly",
33 "github",
34 "stripe",
35 "cloudflare",
36 "gcp",
37 "aws",
38 "domain",
39 "kubernetes",
40 "docker",
41 "redis",
42 "sql",
43];
44​
45class UnnormalizedJsdocs extends Data.TaggedError("UnnormalizedJsdocs")<{
46 readonly files: ReadonlyArray<string>;
47}> {
48 // Already reported on stderr; only set the exit code.
49 readonly [Runtime.errorReported] = false;
51​
52const pageTag = /^\s*\*\s+@(resource|binding|layer|primitive)\b/;
53const generatorTag =
54 /^\s*\*\s+@(resource|binding|layer|primitive|product|category|group|label|title|provides|peer)\b/;
55const sectionTag = /^(\s*\*\s+)@section(?:\s+(.*))?$/;
56const exampleTag = /^(\s*\*\s+)@example(?:\s+(.*))?$/;
57​
58function fixBlock(block: string): string {
59 const lines = block.split("\n");
60 const isPage = lines.some((line) => pageTag.test(line));
61 const hasSections = lines.some((line) => sectionTag.test(line));
62 if (!isPage && !hasSections) return block;
63​
64 const metadata: string[] = [];
65 const body: string[] = [];
66 let insideFence = false;
67​
68 for (const line of lines) {
69 if (
70 line
71 .trim()
72 .replace(/^\*\s?/, "")
73 .startsWith("```")
74 ) {
75 insideFence = !insideFence;
76 }
77​
78 if (isPage && !insideFence && generatorTag.test(line)) {
79 metadata.push(line);
80 continue;
81 }
82​
83 if (!insideFence) {
84 const section = line.match(sectionTag);
85 if (section) {
86 body.push(`${section[1]}### ${section[2]?.trim() || "Examples"}`);
87 continue;
88 }
89​
90 const example = line.match(exampleTag);
91 if (example) {
92 body.push(`${example[1]}**Example:** ${example[2]?.trim() || "Example"}`);
93 continue;
94 }
95 }
96​
97 body.push(line);
98 }
99​
100 const close = body.pop();
101 if (close === undefined || !close.trim().endsWith("*/")) return block;
102​
103 while (body.at(-1)?.trim() === "*") body.pop();
104 const commentPrefix = metadata[0]?.match(/^(\s*\*)/)?.[1];
105 if (body.length > 1 && commentPrefix) body.push(commentPrefix);
106 body.push(...metadata, close);
107 return body.join("\n");
109​
110const sourceFiles = Effect.fn(function* (root: string) {
111 const fs = yield* FileSystem.FileSystem;
112 const path = yield* Path.Path;
113 const entries = yield* fs.readDirectory(root, { recursive: true });
114 return entries
115 .filter((entry) => /\.[cm]?tsx?$/.test(entry) && !entry.endsWith(".d.ts"))
116 .map((entry) => path.join(root, entry));
117});
118​
119const command = Command.make(
120 "fix-api-jsdocs",
121 {
122 check: Flag.Boolean("check").pipe(
123 Flag.withDefault(false),
124 Flag.withDescription("Only report files that need fixing; exit 1 if any do"),
125 ),
126 },
127 Effect.fn(function* ({ check }) {
128 const fs = yield* FileSystem.FileSystem;
129 const path = yield* Path.Path;
130 const repoRoot = path.resolve(import.meta.dirname, "..");
131 const changedFiles: string[] = [];
132​
133 for (const pkg of packages) {
134 for (const file of yield* sourceFiles(path.join(repoRoot, "packages", pkg, "src"))) {
135 const source = yield* fs.readFileString(file);
136 const fixed = source.replace(/\/\*\*[\s\S]*?\*\//g, fixBlock);
137 if (fixed === source) continue;
138 changedFiles.push(path.relative(repoRoot, file));
139 if (!check) yield* fs.writeFileString(file, fixed);
140 }
141 }
142​
143 if (check && changedFiles.length > 0) {
144 yield* Console.error(
145 `${changedFiles.length} files contain API JSDocs that need fixing:\n${changedFiles.map((file) => `- ${file}`).join("\n")}\n\nRun \`pnpm docs:fix-jsdoc\` to fix them.`,
146 );
147 return yield* new UnnormalizedJsdocs({ files: changedFiles });
148 }
149 yield* Console.log(
150 check
151 ? "All API JSDocs are normalized."
152 : `Normalized API JSDocs in ${changedFiles.length} files.`,
153 );
154 }),
155).pipe(Command.withDescription("Normalize API-generator JSDoc blocks in package sources"));
156​
157Command.run(command, { version: "0.0.0" }).pipe(
158 Effect.provide(NodeServices.layer),
159 NodeRuntime.runMain,
160);