Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
48
repos/effect/packages/tools/ai-docgen/src/Glob.ts
Normal file
48
repos/effect/packages/tools/ai-docgen/src/Glob.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Glob pattern matching service.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Context from "effect/Context"
|
||||
import * as Data from "effect/Data"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as GlobLib from "glob"
|
||||
|
||||
/**
|
||||
* Error during glob pattern matching.
|
||||
*
|
||||
* @category errors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class GlobError extends Data.TaggedError("GlobError")<{
|
||||
readonly pattern: string | ReadonlyArray<string>
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
/**
|
||||
* Context service for glob pattern matching used by AI docgen tooling.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class Glob extends Context.Service<Glob, {
|
||||
readonly glob: (
|
||||
pattern: string | ReadonlyArray<string>,
|
||||
options?: GlobLib.GlobOptions
|
||||
) => Effect.Effect<Array<string>, GlobError>
|
||||
}>()("@effect/ai-codegen/Glob") {}
|
||||
|
||||
/**
|
||||
* Layer providing the Glob service.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer: Layer.Layer<Glob> = Layer.succeed(Glob, {
|
||||
glob: (pattern, options) =>
|
||||
Effect.tryPromise({
|
||||
try: () => GlobLib.glob(pattern as string | Array<string>, options ?? {}) as Promise<Array<string>>,
|
||||
catch: (cause) => new GlobError({ pattern, cause })
|
||||
})
|
||||
})
|
||||
188
repos/effect/packages/tools/ai-docgen/src/main.ts
Executable file
188
repos/effect/packages/tools/ai-docgen/src/main.ts
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/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
|
||||
})
|
||||
Reference in New Issue
Block a user