189 lines
5.6 KiB
JavaScript
Executable File
189 lines
5.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* @since 4.0.0
|
|
*/
|
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
|
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
import * as Array from "effect/Array"
|
|
import * as Effect from "effect/Effect"
|
|
import * as FileSystem from "effect/FileSystem"
|
|
import { pipe } from "effect/Function"
|
|
import * as Path from "effect/Path"
|
|
import type * as PlatformError from "effect/PlatformError"
|
|
import * as Stream from "effect/Stream"
|
|
import * as String from "effect/String"
|
|
import * as Argument from "effect/unstable/cli/Argument"
|
|
import * as Command from "effect/unstable/cli/Command"
|
|
import * as Flag from "effect/unstable/cli/Flag"
|
|
|
|
const directory = Argument.directory("directory", { mustExist: true })
|
|
|
|
const output = Flag.path("output").pipe(
|
|
Flag.withAlias("o"),
|
|
Flag.withDescription("Output file path")
|
|
)
|
|
|
|
const watch = Flag.boolean("watch").pipe(
|
|
Flag.withAlias("w"),
|
|
Flag.withDescription("Watch for file changes and regenerate documentation")
|
|
)
|
|
|
|
Command.make("effect-ai-docgen", { directory, output, watch }).pipe(
|
|
Command.withHandler(Effect.fnUntraced(function*({ directory, output, watch }) {
|
|
const fs = yield* FileSystem.FileSystem
|
|
const markdown = yield* directoryToMarkdown(directory)
|
|
yield* fs.writeFileString(output, markdown)
|
|
|
|
if (!watch) return
|
|
|
|
yield* Effect.logInfo("Watching for changes...")
|
|
|
|
yield* fs.watch(directory).pipe(
|
|
Stream.debounce(1000),
|
|
Stream.tap(() => Effect.logInfo("Changes detected, regenerating documentation...")),
|
|
Stream.switchMap(() =>
|
|
directoryToMarkdown(directory).pipe(
|
|
Stream.fromEffect
|
|
)
|
|
),
|
|
Stream.runForEach(Effect.fn(function*(markdown) {
|
|
yield* fs.writeFileString(output, markdown)
|
|
yield* Effect.logInfo("Documentation updated.")
|
|
}))
|
|
)
|
|
})),
|
|
Command.run({
|
|
version: "0.0.0"
|
|
}),
|
|
Effect.provide(NodeServices.layer),
|
|
NodeRuntime.runMain
|
|
)
|
|
|
|
const directoryToMarkdown = Effect.fn("directoryToMarkdown")(
|
|
function*(
|
|
directory: string
|
|
): Effect.fn.Return<string, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> {
|
|
const pathService = yield* Path.Path
|
|
const fs = yield* FileSystem.FileSystem
|
|
|
|
const indexMd = yield* fs.readFileString(pathService.join(directory, "index.md")).pipe(
|
|
Effect.map(String.trim),
|
|
Effect.orElseSucceed(() => null)
|
|
)
|
|
|
|
const allFiles = yield* fs.readDirectory(directory)
|
|
const hasInlineFiles = allFiles.some((file) => pathService.basename(file).startsWith("0") && /\.tsx?$/.test(file))
|
|
|
|
const tsFileContent = yield* Effect.forEach(
|
|
allFiles,
|
|
Effect.fn(function*(file) {
|
|
const filePath = pathService.join(directory, file)
|
|
const stat = yield* fs.stat(filePath)
|
|
const basename = pathService.basename(filePath)
|
|
|
|
if (stat.type === "Directory") {
|
|
if (basename === "fixtures") return null
|
|
return `${yield* directoryToMarkdown(filePath)}\n`
|
|
} else if (/\.tsx?$/.test(file)) {
|
|
const metadata = yield* tsFileMetadata(filePath)
|
|
|
|
if (metadata.fileNameWithoutExt.startsWith("0")) {
|
|
return `### ${metadata.title}
|
|
|
|
${metadata.description ?? ""}
|
|
|
|
\`\`\`ts
|
|
${metadata.content}
|
|
\`\`\`
|
|
`
|
|
}
|
|
|
|
const relativePath = pathService.relative(process.cwd(), filePath)
|
|
const link = `[${metadata.title}](./${relativePath})`
|
|
let content = ""
|
|
|
|
if (hasInlineFiles && metadata.fileNameWithoutExt.startsWith("10")) {
|
|
content += `### More examples\n\n`
|
|
}
|
|
|
|
content += `- **${link}**`
|
|
|
|
if (metadata.description) {
|
|
if (metadata.description.includes("\n")) {
|
|
const indentedDescription = metadata.description
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n")
|
|
content += `:\n${indentedDescription}`
|
|
} else {
|
|
content += `: ${metadata.description}`
|
|
}
|
|
}
|
|
return content
|
|
}
|
|
|
|
return null
|
|
}),
|
|
{ concurrency: 5 }
|
|
).pipe(
|
|
Effect.map(Array.filter((s): s is string => s !== null && s.trim() !== "")),
|
|
Effect.map(Array.join("\n")),
|
|
Effect.map(String.trim)
|
|
)
|
|
|
|
return indexMd ? `${indexMd}\n\n${tsFileContent}` : tsFileContent
|
|
}
|
|
)
|
|
|
|
const tsFileMetadata = Effect.fn("tsFileMetadata")(function*(filePath: string) {
|
|
const pathService = yield* Path.Path
|
|
const fs = yield* FileSystem.FileSystem
|
|
|
|
let content = yield* fs.readFileString(filePath)
|
|
const fileNameWithoutExt = pathService.basename(filePath, pathService.extname(filePath))
|
|
|
|
let title: string = pipe(
|
|
fileNameWithoutExt,
|
|
String.replaceAll(/^\d+/g, ""),
|
|
String.replaceAll(/[-_]/g, " "),
|
|
String.trim,
|
|
String.capitalize
|
|
)
|
|
|
|
const firstDocString = content.indexOf("/**")
|
|
if (firstDocString === -1) {
|
|
return {
|
|
title,
|
|
description: undefined,
|
|
content,
|
|
fileNameWithoutExt
|
|
} as const
|
|
}
|
|
|
|
const linesWithComment = content.slice(firstDocString).split("\n")
|
|
|
|
let description = ""
|
|
|
|
for (let i = 1; i < linesWithComment.length; i++) {
|
|
const line = linesWithComment[i]
|
|
if (!line.startsWith(" *")) break
|
|
if (line.endsWith(" */")) {
|
|
content = linesWithComment.slice(i + 1).join("\n").trim()
|
|
break
|
|
}
|
|
const lineContent = line.replace(/^ \*\s?/, "")
|
|
if (lineContent.startsWith("@title")) {
|
|
title = lineContent.replace("@title", "").trim()
|
|
continue
|
|
}
|
|
description += lineContent + "\n"
|
|
}
|
|
|
|
return {
|
|
title,
|
|
description: String.trim(description) || undefined,
|
|
content,
|
|
fileNameWithoutExt
|
|
} as const
|
|
})
|