| 1 | /** |
| 2 | * Coordinates source parsing, validation, example checking, and Markdown generation. |
| 3 | * |
| 4 | * @since 0.6.0 |
| 5 | */ |
| 6 | |
| 7 | import * as NodePath from "@effect/platform-node/NodePath" |
| 8 | import chalk from "chalk" |
| 9 | import * as Array from "effect/Array" |
| 10 | import * as Effect from "effect/Effect" |
| 11 | import * as Fiber from "effect/Fiber" |
| 12 | import * as FileSystem from "effect/FileSystem" |
| 13 | import { pipe } from "effect/Function" |
| 14 | import * as Path from "effect/Path" |
| 15 | import * as ChildProcess from "effect/process/ChildProcess" |
| 16 | import * as Stream from "effect/Stream" |
| 17 | import * as String from "effect/String" |
| 18 | import * as Glob from "glob" |
| 19 | import * as Checker from "./Checker.ts" |
| 20 | import * as Configuration from "./Configuration.ts" |
| 21 | import * as Domain from "./Domain.ts" |
| 22 | import * as Parser from "./Parser.ts" |
| 23 | import * as Printer from "./Printer.ts" |
| 24 | /** |
| 25 | * Find all files matching the specified `glob` pattern, optionally excluding |
| 26 | * files matching the provided `exclude` patterns. |
| 27 | */ |
| 28 | const glob = (pattern: string, exclude: ReadonlyArray<string> = []) => |
| 29 | Effect.tryPromise(() => |
| 30 | Glob.glob(pattern, { |
| 31 | ignore: exclude.slice(), |
| 32 | withFileTypes: false |
| 33 | }) |
| 34 | ).pipe( |
| 35 | Effect.mapError(() => |
| 36 | new Domain.DocgenError({ |
| 37 | message: `[Core.glob] Unable to execute glob pattern '${pattern}' ` + |
| 38 | `excluding files matching '${exclude}'` |
| 39 | }) |
| 40 | ), |
| 41 | Effect.orDie |
| 42 | ) |
| 43 | |
| 44 | /** @internal */ |
| 45 | export const runCommand = Effect.fnUntraced(function*( |
| 46 | executable: string, |
| 47 | args: ReadonlyArray<string>, |
| 48 | shell: boolean |
| 49 | ) { |
| 50 | const handle = yield* ChildProcess.make(executable, args, { shell }) |
| 51 | const [stdout, stderr, exitCode] = yield* Effect.all([ |
| 52 | Stream.mkString(Stream.decodeText(handle.stdout)), |
| 53 | Stream.mkString(Stream.decodeText(handle.stderr)), |
| 54 | handle.exitCode |
| 55 | ], { concurrency: "unbounded" }) |
| 56 | return { stdout, stderr, exitCode } as const |
| 57 | }) |
| 58 | |
| 59 | /** |
| 60 | * Reads all TypeScript files in the source directory and returns an array of file objects. |
| 61 | * Each file object contains the file path and its content. |
| 62 | */ |
| 63 | const readSourceFiles = Effect.gen(function*() { |
| 64 | const config = yield* Configuration.Configuration |
| 65 | const fs = yield* FileSystem.FileSystem |
| 66 | const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)) |
| 67 | const pattern = path.normalize(path.join(config.srcDir, "**", "*.ts")) |
| 68 | const paths = yield* glob(pattern, config.exclude) |
| 69 | yield* Effect.logInfo(chalk.bold(`${paths.length} module(s) found`)) |
| 70 | return yield* Effect.forEach(paths, (path) => |
| 71 | Effect.map( |
| 72 | fs.readFileString(path), |
| 73 | (content) => new Domain.File(path, content, false) |
| 74 | ), { concurrency: "unbounded" }) |
| 75 | }) |
| 76 | |
| 77 | /** |
| 78 | * Writes a file to the `config.outDir` directory, taking into account the configuration and existing files. |
| 79 | */ |
| 80 | const writeFileToOutDir = (file: Domain.File) => |
| 81 | Effect.gen(function*() { |
| 82 | const config = yield* Configuration.Configuration |
| 83 | const fs = yield* FileSystem.FileSystem |
| 84 | const path = yield* Path.Path |
| 85 | const process = yield* Domain.Process |
| 86 | const cwd = yield* process.cwd |
| 87 | const fileName = path.relative(path.join(cwd, config.outDir), file.path) |
| 88 | |
| 89 | const exists = yield* fs.exists(file.path) |
| 90 | if (exists) { |
| 91 | if (file.isOverwriteable) { |
| 92 | yield* Effect.logDebug(`Overwriting file ${chalk.black(fileName)}...`) |
| 93 | yield* fs.makeDirectory(path.dirname(file.path), { recursive: true }) |
| 94 | yield* fs.writeFileString(file.path, file.content) |
| 95 | } else { |
| 96 | yield* Effect.logDebug( |
| 97 | `File ${chalk.black(fileName)} already exists, skipping creation.` |
| 98 | ) |
| 99 | } |
| 100 | } else { |
| 101 | yield* fs.makeDirectory(path.dirname(file.path), { recursive: true }) |
| 102 | yield* fs.writeFileString(file.path, file.content) |
| 103 | } |
| 104 | }) |
| 105 | |
| 106 | const writeFilesToOutDir = ( |
| 107 | files: ReadonlyArray<Domain.File> |
| 108 | ) => Effect.forEach(files, writeFileToOutDir, { discard: true }) |
| 109 | |
| 110 | const parseModules = (files: ReadonlyArray<Domain.File>) => |
| 111 | Parser.parseFiles(files).pipe( |
| 112 | Effect.mapError((errors) => |
| 113 | new Domain.DocgenError({ |
| 114 | message: "[Core.parseModules] The following error(s) occurred while " + |
| 115 | `parsing the TypeScript source files:\n${errors.map((errors) => errors.join("\n")).join("\n")}` |
| 116 | }) |
| 117 | ) |
| 118 | ) |
| 119 | |
| 120 | /** |
| 121 | * Runs the example files for the given modules, type-checking them before execution. |
| 122 | */ |
| 123 | const typeCheckAndRunExamples = (modules: ReadonlyArray<Domain.Module>) => |
| 124 | Effect.gen(function*() { |
| 125 | const config = yield* Configuration.Configuration |
| 126 | yield* cleanupExamples |
| 127 | const files = yield* getExampleFiles(modules) |
| 128 | const len = files.length |
| 129 | if (len > 0) { |
| 130 | yield* Effect.logInfo(`${len} example(s) found`) |
| 131 | yield* writeExamplesToOutDir(files) |
| 132 | yield* createExamplesTsConfigJson |
| 133 | yield* Effect.logInfo("Typechecking examples...") |
| 134 | yield* runTscOnExamples |
| 135 | if (config.runExamples) { |
| 136 | yield* Effect.logInfo("Running examples...") |
| 137 | yield* runTsxOnExamples |
| 138 | } else { |
| 139 | yield* Effect.logInfo(chalk.gray("Skipping running examples")) |
| 140 | } |
| 141 | } else { |
| 142 | yield* Effect.logInfo("No examples found.") |
| 143 | } |
| 144 | yield* cleanupExamples |
| 145 | }) |
| 146 | |
| 147 | /** |
| 148 | * Joins an array of strings with a "-" after dropping all empty strings. |
| 149 | */ |
| 150 | const filterJoin = (self: Array<string>) => |
| 151 | pipe( |
| 152 | self, |
| 153 | Array.filter(String.isNonEmpty), |
| 154 | Array.join("-") |
| 155 | ) |
| 156 | |
| 157 | /** |
| 158 | * Extracts deeply nested namespaces with their corresponding namespace prefix |
| 159 | * from a given namespace. |
| 160 | */ |
| 161 | const extractPrefixedNestedNamespaces = ( |
| 162 | doc: Domain.Namespace, |
| 163 | prefix: string |
| 164 | ): ReadonlyArray<[string, Domain.Namespace]> => { |
| 165 | const newPrefix = String.isEmpty(prefix) ? doc.name : `${prefix}-${doc.name}` |
| 166 | const namespaces = Array.flatMap( |
| 167 | doc.namespaces, |
| 168 | (namespace) => extractPrefixedNestedNamespaces(namespace, newPrefix) |
| 169 | ) |
| 170 | return Array.prepend(namespaces, [prefix, doc]) |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Fence metadata that excludes an example from docgen type checking. |
| 175 | * |
| 176 | * @category constants |
| 177 | * @since 0.6.0 |
| 178 | */ |
| 179 | export const SKIP_TYPE_CHECKING_FENCE_METADATA = "skip-type-checking" |
| 180 | |
| 181 | /** |
| 182 | * Extracts all fenced code blocks from markdown content. |
| 183 | * Handles both ``` and ~~~ fences, including any metadata like language, title, and other attributes. |
| 184 | * |
| 185 | * @internal |
| 186 | */ |
| 187 | export const extractFencedCode = (content: string): [examples: Array<string>, warnings: Array<string>] => { |
| 188 | // The regex now captures the closing fence (group 3) if present. |
| 189 | // If there's no closing fence, group 3 will be undefined. |
| 190 | const fenceRegex = /(?:```|~~~)(.*?)\n([\s\S]*?)(?:(```|~~~)|$)/g |
| 191 | const matches = Array.fromIterable(content.matchAll(fenceRegex)) |
| 192 | |
| 193 | const warnings: Array<string> = [] |
| 194 | |
| 195 | // Log a warning if a code fence is not properly closed. |
| 196 | for (const match of matches) { |
| 197 | if (match[3] === undefined) { |
| 198 | warnings.push(`Code block does not have a matching closing fence:\n${content}`) |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | return [ |
| 203 | matches |
| 204 | .filter((match) => { |
| 205 | const meta = match[1].toLocaleLowerCase() |
| 206 | const isTypeScript = meta.startsWith("ts") || meta.startsWith("typescript") |
| 207 | const isSkipTypeChecking = meta.includes(SKIP_TYPE_CHECKING_FENCE_METADATA) |
| 208 | return isTypeScript && !isSkipTypeChecking |
| 209 | }) |
| 210 | .map((match) => match[2].trim()), |
| 211 | warnings |
| 212 | ] |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Generates example files for the given modules. |
| 217 | */ |
| 218 | const getExampleFiles = (modules: ReadonlyArray<Domain.Module>) => |
| 219 | Effect.gen(function*() { |
| 220 | const config = yield* Configuration.Configuration |
| 221 | const path = yield* Path.Path |
| 222 | let warnings: Array<string> = [] |
| 223 | // Flattened module paths can collide, so give every emitted example a unique prefix. |
| 224 | let fileIndex = 0 |
| 225 | const files = Array.flatMap(modules, (module) => { |
| 226 | const prefix = module.path.join("-") |
| 227 | |
| 228 | const getFiles = |
| 229 | (exampleId: string) => |
| 230 | (namedDoc: { readonly name: string; readonly doc: Domain.Doc }): ReadonlyArray<Domain.File> => { |
| 231 | let descriptionExamples: Array<string> = [] |
| 232 | if (namedDoc.doc.description !== undefined) { |
| 233 | const [es, ws] = extractFencedCode(namedDoc.doc.description) |
| 234 | warnings = warnings.concat(ws) |
| 235 | descriptionExamples = es |
| 236 | } |
| 237 | let exampleTagExamples: Array<string> = [] |
| 238 | for (const example of namedDoc.doc.examples) { |
| 239 | const [es, ws] = extractFencedCode(example) |
| 240 | warnings = warnings.concat(ws) |
| 241 | exampleTagExamples = exampleTagExamples.concat(es) |
| 242 | } |
| 243 | const examples = descriptionExamples.concat(exampleTagExamples) |
| 244 | return Array.map( |
| 245 | examples, |
| 246 | (example, i) => { |
| 247 | return new Domain.File( |
| 248 | path.join( |
| 249 | config.outDir, |
| 250 | "examples", |
| 251 | `${fileIndex++}-${prefix}-${exampleId}-${namedDoc.name}-${i}.ts` |
| 252 | ), |
| 253 | example, |
| 254 | true // make the file overwritable |
| 255 | ) |
| 256 | } |
| 257 | ) |
| 258 | } |
| 259 | |
| 260 | const allPrefixedNamespaces = Array.flatMap(module.namespaces, (namespace) => |
| 261 | extractPrefixedNestedNamespaces(namespace, "")) |
| 262 | |
| 263 | const moduleExamples = getFiles("module")(module) |
| 264 | const classExamples = Array.flatMap(module.classes, (c) => |
| 265 | Array.flatten([ |
| 266 | getFiles("class")(c), |
| 267 | Array.flatMap(c.properties, getFiles(`${c.name}-property`)), |
| 268 | Array.flatMap( |
| 269 | c.methods, |
| 270 | getFiles(`${c.name}-method`) |
| 271 | ), |
| 272 | Array.flatMap( |
| 273 | c.staticMethods, |
| 274 | getFiles(`${c.name}-staticmethod`) |
| 275 | ) |
| 276 | ])) |
| 277 | const allPrefixedInterfaces = [ |
| 278 | ...module.interfaces.map((iface) => |
| 279 | ["" as string, iface] as const |
| 280 | ), |
| 281 | ...Array.flatMap(allPrefixedNamespaces, ([prefix, namespace]) => |
| 282 | namespace.interfaces.map((iface) => |
| 283 | [filterJoin([prefix, namespace.name]), iface] as const |
| 284 | )) |
| 285 | ] |
| 286 | const interfacesExamples = Array.flatMap( |
| 287 | allPrefixedInterfaces, |
| 288 | ([ns, doc]) => getFiles(filterJoin(["interface", ns]))(doc) |
| 289 | ) |
| 290 | const allPrefixedTypeAliases = [ |
| 291 | ...module.typeAliases.map((typeAlias) => ["" as string, typeAlias] as const), |
| 292 | ...Array.flatMap(allPrefixedNamespaces, ([prefix, namespace]) => |
| 293 | namespace.typeAliases.map((typeAlias) => |
| 294 | [filterJoin([prefix, namespace.name]), typeAlias] as const |
| 295 | )) |
| 296 | ] |
| 297 | const typeAliasesExamples = Array.flatMap( |
| 298 | allPrefixedTypeAliases, |
| 299 | ([ns, doc]) => |
| 300 | getFiles(filterJoin(["typealias", ns]))(doc) |
| 301 | ) |
| 302 | const constantsExamples = Array.flatMap( |
| 303 | module.constants, |
| 304 | getFiles("constant") |
| 305 | ) |
| 306 | const functionsExamples = Array.flatMap( |
| 307 | module.functions, |
| 308 | getFiles("function") |
| 309 | ) |
| 310 | const exportsExamples = Array.flatMap( |
| 311 | module.exports, |
| 312 | getFiles("export") |
| 313 | ) |
| 314 | const namespacesExamples = Array.flatMap( |
| 315 | allPrefixedNamespaces, |
| 316 | ([ns, doc]) => getFiles(filterJoin(["namespace", ns]))(doc) |
| 317 | ) |
| 318 | |
| 319 | return Array.flatten([ |
| 320 | moduleExamples, |
| 321 | classExamples, |
| 322 | interfacesExamples, |
| 323 | typeAliasesExamples, |
| 324 | constantsExamples, |
| 325 | functionsExamples, |
| 326 | namespacesExamples, |
| 327 | exportsExamples |
| 328 | ]) |
| 329 | }) |
| 330 | |
| 331 | if (warnings.length > 0) { |
| 332 | yield* Effect.logWarning(warnings.join("\n")) |
| 333 | } |
| 334 | |
| 335 | return files |
| 336 | }) |
| 337 | |
| 338 | /** |
| 339 | * Generates an entry point file for the given examples. |
| 340 | */ |
| 341 | const getExamplesEntryPoint = (examples: ReadonlyArray<Domain.File>) => |
| 342 | Effect.gen(function*() { |
| 343 | const config = yield* Configuration.Configuration |
| 344 | const path = yield* Path.Path |
| 345 | const content = examples.map((example) => `import './${path.basename(example.path, ".ts")}'`) |
| 346 | .join("\n") |
| 347 | return new Domain.File( |
| 348 | path.normalize(path.join(config.outDir, "examples", "index.ts")), |
| 349 | `${content}\n`, |
| 350 | true // make the file overwritable |
| 351 | ) |
| 352 | }) |
| 353 | |
| 354 | /** |
| 355 | * Removes the "examples" directory from the output directory specified in the configuration. |
| 356 | */ |
| 357 | const cleanupExamples = Effect.gen(function*() { |
| 358 | const fs = yield* FileSystem.FileSystem |
| 359 | const config = yield* Configuration.Configuration |
| 360 | const path = yield* Path.Path |
| 361 | const examplesDir = path.join(config.outDir, "examples") |
| 362 | const exists = yield* Effect.orDie(fs.exists(examplesDir)) |
| 363 | if (exists) { |
| 364 | yield* fs.remove(examplesDir, { recursive: true }) |
| 365 | } |
| 366 | }) |
| 367 | |
| 368 | /** |
| 369 | * Runs tsc on the examples directory. |
| 370 | */ |
| 371 | const runTscOnExamples = Effect.gen(function*() { |
| 372 | const config = yield* Configuration.Configuration |
| 373 | const process = yield* Domain.Process |
| 374 | const cwd = yield* process.cwd |
| 375 | const path = yield* Path.Path |
| 376 | const platform = yield* process.platform |
| 377 | |
| 378 | const tsconfig = path.normalize(path.join(cwd, config.outDir, "examples", "tsconfig.json")) |
| 379 | const options = ["--noEmit", "--project", tsconfig] |
| 380 | yield* Effect.logDebug("Running tsc on examples...") |
| 381 | const result = yield* runCommand( |
| 382 | platform === "win32" ? `${config.tscExecutable}.cmd` : config.tscExecutable, |
| 383 | options, |
| 384 | platform === "win32" |
| 385 | ).pipe(Effect.mapError((error) => |
| 386 | new Domain.DocgenError({ |
| 387 | message: `Something went wrong while running tsc on examples:\n\n${globalThis.String(error)}` |
| 388 | }) |
| 389 | )) |
| 390 | if (result.exitCode !== 0) { |
| 391 | return yield* new Domain.DocgenError({ |
| 392 | message: `Something went wrong while running tsc on examples:\n\n${result.stdout}` |
| 393 | }) |
| 394 | } |
| 395 | }) |
| 396 | |
| 397 | /** |
| 398 | * Runs tsc on the examples directory. |
| 399 | */ |
| 400 | const runTsxOnExamples = Effect.gen(function*() { |
| 401 | const config = yield* Configuration.Configuration |
| 402 | const path = yield* Path.Path |
| 403 | const process = yield* Domain.Process |
| 404 | const cwd = yield* process.cwd |
| 405 | const platform = yield* process.platform |
| 406 | |
| 407 | const examples = path.normalize(path.join(cwd, config.outDir, "examples")) |
| 408 | const tsconfig = path.join(examples, "tsconfig.json") |
| 409 | const index = path.join(examples, "index.ts") |
| 410 | const options = ["--tsconfig", tsconfig, index] |
| 411 | yield* Effect.logDebug("Running tsx on examples...") |
| 412 | const result = yield* runCommand( |
| 413 | platform === "win32" ? "tsx.cmd" : "tsx", |
| 414 | options, |
| 415 | platform === "win32" |
| 416 | ).pipe(Effect.mapError((error) => |
| 417 | new Domain.DocgenError({ |
| 418 | message: `Something went wrong while running tsx on examples:\n\n${globalThis.String(error)}` |
| 419 | }) |
| 420 | )) |
| 421 | if (result.exitCode !== 0) { |
| 422 | return yield* new Domain.DocgenError({ |
| 423 | message: `Something went wrong while running tsx on examples:\n\n${result.stderr}` |
| 424 | }) |
| 425 | } |
| 426 | }) |
| 427 | |
| 428 | const writeExamplesToOutDir = (examples: ReadonlyArray<Domain.File>) => |
| 429 | Effect.gen(function*() { |
| 430 | yield* Effect.logDebug("Writing examples...") |
| 431 | const entryPoint = yield* getExamplesEntryPoint(examples) |
| 432 | const files = [entryPoint, ...examples] |
| 433 | yield* writeFilesToOutDir(files) |
| 434 | }) |
| 435 | |
| 436 | const createExamplesTsConfigJson = Effect.gen(function*() { |
| 437 | yield* Effect.logDebug("Writing examples tsconfig...") |
| 438 | const config = yield* Configuration.Configuration |
| 439 | const process = yield* Domain.Process |
| 440 | const cwd = yield* process.cwd |
| 441 | const path = yield* Path.Path |
| 442 | yield* writeFileToOutDir( |
| 443 | new Domain.File( |
| 444 | path.join(cwd, config.outDir, "examples", "tsconfig.json"), |
| 445 | JSON.stringify({ compilerOptions: config.examplesCompilerOptions }, null, 2), |
| 446 | true // make the file overwritable |
| 447 | ) |
| 448 | ) |
| 449 | }) |
| 450 | |
| 451 | const getMarkdown = (modules: ReadonlyArray<Domain.Module>) => |
| 452 | Effect.gen(function*() { |
| 453 | const homepage = yield* getMarkdownHomepage |
| 454 | const index = yield* getMarkdownIndex |
| 455 | const yml = yield* getMarkdownConfigYML |
| 456 | const moduleFiles = yield* getModuleMarkdownFiles(modules) |
| 457 | return [homepage, index, yml, ...moduleFiles] |
| 458 | }) |
| 459 | |
| 460 | const getMarkdownHomepage = Effect.gen(function*() { |
| 461 | const config = yield* Configuration.Configuration |
| 462 | const process = yield* Domain.Process |
| 463 | const cwd = yield* process.cwd |
| 464 | const path = yield* Path.Path |
| 465 | return new Domain.File( |
| 466 | path.join(cwd, config.outDir, "index.md"), |
| 467 | String.stripMargin( |
| 468 | `|--- |
| 469 | |title: Home |
| 470 | |nav_order: 1 |
| 471 | |--- |
| 472 | |` |
| 473 | ), |
| 474 | false |
| 475 | ) |
| 476 | }) |
| 477 | |
| 478 | const getMarkdownIndex = Effect.gen(function*() { |
| 479 | const config = yield* Configuration.Configuration |
| 480 | const process = yield* Domain.Process |
| 481 | const cwd = yield* process.cwd |
| 482 | const path = yield* Path.Path |
| 483 | return new Domain.File( |
| 484 | path.join(cwd, config.outDir, "modules", "index.md"), |
| 485 | String.stripMargin( |
| 486 | `|--- |
| 487 | |title: Modules |
| 488 | |has_children: true |
| 489 | |permalink: /docs/modules |
| 490 | |nav_order: 2 |
| 491 | |--- |
| 492 | |` |
| 493 | ), |
| 494 | false |
| 495 | ) |
| 496 | }) |
| 497 | |
| 498 | const resolveConfigYML = (content: string) => |
| 499 | Effect.gen(function*() { |
| 500 | const config = yield* Configuration.Configuration |
| 501 | return content |
| 502 | .replace(/^remote_theme:.*$/m, `remote_theme: ${config.theme}`) |
| 503 | .replace( |
| 504 | /^search_enabled:.*$/m, |
| 505 | `search_enabled: ${config.enableSearch}` |
| 506 | ).replace( |
| 507 | /^ {2}'\S* on GitHub':\n {4}- '.*'/m, |
| 508 | ` '${config.projectName} on GitHub':\n - '${config.projectHomepage}'` |
| 509 | ) |
| 510 | }) |
| 511 | |
| 512 | const getHomepageNavigationHeader = (config: Configuration.ConfigurationShape): string => { |
| 513 | const isGitHub = config.projectHomepage.toLowerCase().includes("github") |
| 514 | return isGitHub ? config.projectName + " on GitHub" : "Homepage" |
| 515 | } |
| 516 | |
| 517 | const getMarkdownConfigYML = Effect.gen(function*() { |
| 518 | const config = yield* Configuration.Configuration |
| 519 | const process = yield* Domain.Process |
| 520 | const fs = yield* FileSystem.FileSystem |
| 521 | const cwd = yield* process.cwd |
| 522 | const path = yield* Path.Path |
| 523 | const configPath = path.join(cwd, config.outDir, "_config.yml") |
| 524 | const exists = yield* fs.exists(configPath) |
| 525 | if (exists) { |
| 526 | const content = yield* fs.readFileString(configPath) |
| 527 | const resolved = yield* resolveConfigYML(content) |
| 528 | return new Domain.File(configPath, resolved, true) |
| 529 | } else { |
| 530 | return new Domain.File( |
| 531 | configPath, |
| 532 | String.stripMargin( |
| 533 | `|remote_theme: ${config.theme} |
| 534 | | |
| 535 | |# Enable or disable the site search |
| 536 | |search_enabled: ${config.enableSearch} |
| 537 | | |
| 538 | |# Aux links for the upper right navigation |
| 539 | |aux_links: |
| 540 | |'${getHomepageNavigationHeader(config)}': |
| 541 | | - '${config.projectHomepage}'` |
| 542 | ), |
| 543 | false |
| 544 | ) |
| 545 | } |
| 546 | }) |
| 547 | |
| 548 | const getModuleMarkdownOutputPath = (module: Domain.Module) => { |
| 549 | return Effect.gen(function*() { |
| 550 | const config = yield* Configuration.Configuration |
| 551 | const path = yield* Path.Path |
| 552 | return path.normalize(path.join( |
| 553 | config.outDir, |
| 554 | "modules", |
| 555 | `${path.relative(config.srcDir, module.path.join(path.sep))}.md` |
| 556 | )) |
| 557 | }) |
| 558 | } |
| 559 | |
| 560 | /** @internal */ |
| 561 | export const getModuleMarkdownFiles = (modules: ReadonlyArray<Domain.Module>) => |
| 562 | Effect.forEach(modules, (module, i) => |
| 563 | Effect.gen(function*() { |
| 564 | const outputPath = yield* getModuleMarkdownOutputPath(module) |
| 565 | const moduleContent = yield* Printer.printModule(module) |
| 566 | const tocgen = yield* Effect.promise(() => import("@effect/markdown-toc").then((module) => module.default)).pipe( |
| 567 | Effect.orDie |
| 568 | ) |
| 569 | const toc = tocgen(moduleContent, { bullets: "-" }).content |
| 570 | const frontMatter = Printer.printFrontMatter(module, i + 1) |
| 571 | const content = (frontMatter + "\n\n" + moduleContent).replace( |
| 572 | "<!-- toc -->", |
| 573 | `--- |
| 574 | ## Exports Grouped by Category |
| 575 | ${toc} |
| 576 | ---` |
| 577 | ) |
| 578 | |
| 579 | const prettified = yield* Printer.prettify(content) |
| 580 | return new Domain.File(outputPath, prettified, true) |
| 581 | })) |
| 582 | |
| 583 | const writeMarkdown = (files: ReadonlyArray<Domain.File>) => |
| 584 | Effect.gen(function*() { |
| 585 | const config = yield* Configuration.Configuration |
| 586 | const fileSystem = yield* FileSystem.FileSystem |
| 587 | const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)) |
| 588 | const pattern = path.normalize(path.join(config.outDir, "**/*.ts.md")) |
| 589 | yield* Effect.logDebug(`Deleting ${chalk.black(pattern)}...`) |
| 590 | const paths = yield* glob(pattern) |
| 591 | yield* Effect.forEach(paths, (path) => fileSystem.remove(path, { recursive: true }), { |
| 592 | concurrency: "unbounded" |
| 593 | }) |
| 594 | return yield* writeFilesToOutDir(files) |
| 595 | }) |
| 596 | |
| 597 | /** @internal */ |
| 598 | export const program = Effect.gen(function*() { |
| 599 | yield* Effect.logInfo("Reading modules...") |
| 600 | const sourceFiles = yield* readSourceFiles |
| 601 | yield* Effect.logInfo("Parsing modules...") |
| 602 | const modules = yield* parseModules(sourceFiles) |
| 603 | |
| 604 | const checkFiber = yield* Effect.gen(function*() { |
| 605 | yield* Effect.logInfo("Checking modules...") |
| 606 | const errors = yield* Checker.checkModules(modules) |
| 607 | if (errors.length > 0) { |
| 608 | return yield* Effect.fail( |
| 609 | new Domain.DocgenError({ |
| 610 | message: `The following errors occurred while checking the modules:\n\n${errors.join("\n\n")}` |
| 611 | }) |
| 612 | ) |
| 613 | } |
| 614 | yield* typeCheckAndRunExamples(modules) |
| 615 | }).pipe(Effect.forkChild) |
| 616 | |
| 617 | const markdownFiber = yield* Effect.gen(function*() { |
| 618 | yield* Effect.logInfo("Creating markdown files...") |
| 619 | const outputFiles = yield* getMarkdown(modules) |
| 620 | yield* Effect.logInfo("Writing markdown files...") |
| 621 | yield* writeMarkdown(outputFiles) |
| 622 | }).pipe(Effect.forkChild) |
| 623 | |
| 624 | yield* Fiber.joinAll([checkFiber, markdownFiber]) |
| 625 | |
| 626 | yield* Effect.logInfo(chalk.bold.green("✓ Docs generation succeeded!")) |
| 627 | }) |