diff --git a/.changeset/silly-insects-draw.md b/.changeset/silly-insects-draw.md new file mode 100644 index 0000000..48255e3 --- /dev/null +++ b/.changeset/silly-insects-draw.md @@ -0,0 +1,9 @@ +--- +"@effect/docgen": minor +--- + +- **Extracts examples from descriptions** – This can be disabled using `skip-type-checking` metadata on fenced code blocks. `@example` tags still work, but we may remove them in the future. +- **Supports the `@throws` JSDoc tag** – Properly documents possible errors. +- **Basic support for the `@see` JSDoc tag** – Displays only the API name and description. +- **Adds GitHub source links** – Provides direct access to the corresponding source code. +- **Repositions signatures** – Now moved further down, just before the source link and `@since` tag, for better readability. diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 28592a3..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable no-undef */ -module.exports = { - ignorePatterns: ["dist", "*.mjs", "docs", "*.md"], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 2018, - sourceType: "module" - }, - settings: { - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] - }, - "import/resolver": { - typescript: { - alwaysTryTypes: true - } - } - }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@effect/recommended" - ], - plugins: ["deprecation", "import", "sort-destructure-keys", "simple-import-sort", "codegen"], - rules: { - "codegen/codegen": "error", - "no-fallthrough": "off", - "no-irregular-whitespace": "off", - "object-shorthand": "error", - "prefer-destructuring": "off", - "sort-imports": "off", - "no-unused-vars": "off", - "prefer-rest-params": "off", - "prefer-spread": "off", - "import/first": "error", - "import/no-cycle": "error", - "import/newline-after-import": "error", - "import/no-duplicates": "error", - "import/no-unresolved": "off", - "import/order": "off", - "simple-import-sort/imports": "off", - "sort-destructure-keys/sort-destructure-keys": "error", - "deprecation/deprecation": "off", - "@typescript-eslint/array-type": ["warn", { "default": "generic", "readonly": "generic" }], - "@typescript-eslint/member-delimiter-style": 0, - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-empty-object-type": "off", - "@typescript-eslint/consistent-type-imports": "warn", - "@typescript-eslint/no-unused-vars": ["error", { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_" - }], - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/camelcase": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/no-array-constructor": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/no-namespace": "off", - "@effect/dprint": [ - "error", - { - config: { - "indentWidth": 2, - "lineWidth": 100, - "semiColons": "asi", - "quoteStyle": "alwaysDouble", - "trailingCommas": "never", - "operatorPosition": "maintain", - "arrowFunction.useParentheses": "force" - } - } - ] - } -} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..ef63606 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,23 @@ +name: Snapshot +on: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: {} + +jobs: + snapshot: + name: Snapshot + if: github.repository_owner == 'Effect-Ts' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Build package + run: pnpm build + - name: Create snapshot + id: snapshot + run: pnpx pkg-pr-new@0.0.28 publish --pnpm --comment=off diff --git a/docgen.json b/docgen.json index 1aa61a3..559df04 100644 --- a/docgen.json +++ b/docgen.json @@ -8,9 +8,18 @@ "exactOptionalPropertyTypes": true, "moduleResolution": "Bundler", "target": "ES2022", - "lib": [ - "ES2022", - "DOM" - ] + "lib": ["ES2022", "DOM"] + }, + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "exactOptionalPropertyTypes": true, + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "paths": { + "@effect/docgen": ["../../src/index.js"] + } } } diff --git a/docs/modules/CLI.ts.md b/docs/modules/CLI.ts.md index cd1a397..72eef92 100644 --- a/docs/modules/CLI.ts.md +++ b/docs/modules/CLI.ts.md @@ -1,15 +1,38 @@ --- title: CLI.ts -nav_order: 1 +nav_order: 2 parent: Modules --- -## CLI overview +## CLI.ts overview -Added in v1.0.0 +Since v0.6.0 --- -
['"])${projectName}(?:/lib)?(?:/(?.*))?\\k `, - "g" - ) - - const out = source.replace(importRegex(config.projectName), (...args) => { - const groups: { path?: string } = args[args.length - 1] - return `from '../../src${groups.path ? `/${groups.path}` : ""}'` - }) + if (warnings.length > 0) { + yield* Effect.logWarning(warnings.join("\n")) + } - return out + return files }) -const handleImports = (files: ReadonlyArray) => - Effect.forEach(files, (file) => - Effect.gen(function*(_) { - const source = yield* _(replaceProjectName(file.content)) - const content = addAssertImport(source) - return File.createFile(file.path, content, file.isOverwriteable) - })) - /** * Generates an entry point file for the given examples. */ -const getExamplesEntryPoint = (examples: ReadonlyArray ) => - Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const path = yield* _(Path.Path) +const getExamplesEntryPoint = (examples: ReadonlyArray ) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path const content = examples.map((example) => `import './${path.basename(example.path, ".ts")}'`) .join("\n") - return File.createFile( + return new Domain.File( path.normalize(path.join(config.outDir, "examples", "index.ts")), `${content}\n`, true // make the file overwritable @@ -306,27 +336,27 @@ const getExamplesEntryPoint = (examples: ReadonlyArray ) => /** * Removes the "examples" directory from the output directory specified in the configuration. */ -const cleanupExamples = Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const config = yield* _(Configuration.Configuration) - const path = yield* _(Path.Path) +const cleanupExamples = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const config = yield* Configuration.Configuration + const path = yield* Path.Path const examplesDir = path.join(config.outDir, "examples") - const exists = yield* _(Effect.orDie(fs.exists(examplesDir))) + const exists = yield* Effect.orDie(fs.exists(examplesDir)) if (exists) { - yield* _(fs.remove(examplesDir, { recursive: true })) + yield* fs.remove(examplesDir, { recursive: true }) } }) /** * Runs tsc on the examples directory. */ -const runTscOnExamples = Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const process = yield* _(Process.Process) - const executor = yield* _(CommandExecutor.CommandExecutor) - const cwd = yield* _(process.cwd) - const path = yield* _(Path.Path) - const platform = yield* _(process.platform) +const runTscOnExamples = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const executor = yield* CommandExecutor.CommandExecutor + const cwd = yield* process.cwd + const path = yield* Path.Path + const platform = yield* process.platform const tsconfig = path.normalize(path.join(cwd, config.outDir, "examples", "tsconfig.json")) const options = ["--noEmit", "--project", tsconfig] @@ -334,9 +364,9 @@ const runTscOnExamples = Effect.gen(function*(_) { ? Command.runInShell(Command.make("tsc.cmd", ...options), true) : Command.make("tsc", ...options) - yield* _(Effect.logDebug("Running tsc on examples...")) + yield* Effect.logDebug("Running tsc on examples...") - const [stdout, exitCode] = yield* _( + const [stdout, exitCode] = yield* pipe( executor.start(command), Effect.flatMap((process) => Effect.all([ @@ -352,24 +382,22 @@ const runTscOnExamples = Effect.gen(function*(_) { ) if (exitCode !== 0) { - yield* _( - new DocgenError({ - message: `Something went wrong while running tsc on examples:\n\n${stdout.join("\n")}` - }) - ) + yield* new Domain.DocgenError({ + message: `Something went wrong while running tsc on examples:\n\n${stdout.join("\n")}` + }) } }) /** * Runs tsc on the examples directory. */ -const runTsxOnExamples = Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const path = yield* _(Path.Path) - const process = yield* _(Process.Process) - const executor = yield* _(CommandExecutor.CommandExecutor) - const cwd = yield* _(process.cwd) - const platform = yield* _(process.platform) +const runTsxOnExamples = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + const process = yield* Domain.Process + const executor = yield* CommandExecutor.CommandExecutor + const cwd = yield* process.cwd + const platform = yield* process.platform const examples = path.normalize(path.join(cwd, config.outDir, "examples")) const tsconfig = path.join(examples, "tsconfig.json") @@ -379,9 +407,9 @@ const runTsxOnExamples = Effect.gen(function*(_) { ? Command.runInShell(Command.make("tsx.cmd", ...options), true) : Command.make("tsx", ...options) - yield* _(Effect.logDebug("Running tsx on examples...")) + yield* Effect.logDebug("Running tsx on examples...") - const [stdout, exitCode] = yield* _( + const [stdout, exitCode] = yield* pipe( executor.start(command), Effect.flatMap((process) => Effect.all([ @@ -397,54 +425,54 @@ const runTsxOnExamples = Effect.gen(function*(_) { ) if (exitCode !== 0) { - yield* _( - Effect.fail( - new DocgenError({ - message: `Something went wrong while running tsx on examples:\n\n${stdout.join("\n")}` - }) - ) + yield* Effect.fail( + new Domain.DocgenError({ + message: `Something went wrong while running tsx on examples:\n\n${stdout.join("\n")}` + }) ) } }) -const writeExamplesToOutDir = (examples: ReadonlyArray ) => - Effect.gen(function*(_) { - yield* _(Effect.logDebug("Writing examples...")) - const entryPoint = yield* _(getExamplesEntryPoint(examples)) +const writeExamplesToOutDir = (examples: ReadonlyArray ) => + Effect.gen(function*() { + yield* Effect.logDebug("Writing examples...") + const entryPoint = yield* getExamplesEntryPoint(examples) const files = [entryPoint, ...examples] - yield* _(writeFilesToOutDir(files)) + yield* writeFilesToOutDir(files) }) -const createExamplesTsConfigJson = Effect.gen(function*(_) { - yield* _(Effect.logDebug("Writing examples tsconfig...")) - const config = yield* _(Configuration.Configuration) - const process = yield* _(Process.Process) - const cwd = yield* _(process.cwd) - const path = yield* _(Path.Path) - yield* _(writeFileToOutDir( - File.createFile( +const createExamplesTsConfigJson = Effect.gen(function*() { + yield* Effect.logDebug("Writing examples tsconfig...") + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + yield* writeFileToOutDir( + new Domain.File( path.join(cwd, config.outDir, "examples", "tsconfig.json"), JSON.stringify({ compilerOptions: config.examplesCompilerOptions }, null, 2), true // make the file overwritable ) - )) + ) }) const getMarkdown = (modules: ReadonlyArray ) => - Effect.gen(function*(_) { - const homepage = yield* _(getMarkdownHomepage) - const index = yield* _(getMarkdownIndex) - const yml = yield* _(getMarkdownConfigYML) - const moduleFiles = yield* _(getModuleMarkdownFiles(modules)) - return [homepage, index, yml, ...moduleFiles] + Effect.gen(function*() { + const homepage = yield* getMarkdownHomepage + const index = yield* getMarkdownIndex + const yml = yield* getMarkdownConfigYML + const moduleFiles = yield* getModuleMarkdownFiles(modules) + const aiFiles = yield* maybeGetAIMarkdownFiles(modules) + const jsonFiles = yield* maybeGetJsonFiles(modules) + return [homepage, index, yml, ...moduleFiles, ...aiFiles, ...jsonFiles] }) -const getMarkdownHomepage = Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const process = yield* _(Process.Process) - const cwd = yield* _(process.cwd) - const path = yield* _(Path.Path) - return File.createFile( +const getMarkdownHomepage = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + return new Domain.File( path.join(cwd, config.outDir, "index.md"), String.stripMargin( `|--- @@ -457,12 +485,12 @@ const getMarkdownHomepage = Effect.gen(function*(_) { ) }) -const getMarkdownIndex = Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const process = yield* _(Process.Process) - const cwd = yield* _(process.cwd) - const path = yield* _(Path.Path) - return File.createFile( +const getMarkdownIndex = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + return new Domain.File( path.join(cwd, config.outDir, "modules", "index.md"), String.stripMargin( `|--- @@ -478,8 +506,8 @@ const getMarkdownIndex = Effect.gen(function*(_) { }) const resolveConfigYML = (content: string) => - Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) + Effect.gen(function*() { + const config = yield* Configuration.Configuration return content .replace(/^remote_theme:.*$/m, `remote_theme: ${config.theme}`) .replace( @@ -496,20 +524,20 @@ const getHomepageNavigationHeader = (config: Configuration.ConfigurationShape): return isGitHub ? config.projectName + " on GitHub" : "Homepage" } -const getMarkdownConfigYML = Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const process = yield* _(Process.Process) - const fs = yield* _(FileSystem.FileSystem) - const cwd = yield* _(process.cwd) - const path = yield* _(Path.Path) +const getMarkdownConfigYML = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const fs = yield* FileSystem.FileSystem + const cwd = yield* process.cwd + const path = yield* Path.Path const configPath = path.join(cwd, config.outDir, "_config.yml") - const exists = yield* _(fs.exists(configPath)) + const exists = yield* fs.exists(configPath) if (exists) { - const content = yield* _(fs.readFileString(configPath)) - const resolved = yield* _(resolveConfigYML(content)) - return File.createFile(configPath, resolved, true) + const content = yield* fs.readFileString(configPath) + const resolved = yield* resolveConfigYML(content) + return new Domain.File(configPath, resolved, true) } else { - return File.createFile( + return new Domain.File( configPath, String.stripMargin( `|remote_theme: ${config.theme} @@ -527,49 +555,172 @@ const getMarkdownConfigYML = Effect.gen(function*(_) { } }) -const getModuleMarkdownOutputPath = (module: Domain.Module) => - Effect.map( - Effect.all([Configuration.Configuration, Path.Path]), - ([config, path]) => - path.normalize(path.join( - config.outDir, - "modules", - `${module.path.slice(1).join(path.sep)}.md` - )) +const getModuleMarkdownOutputPath = (module: Domain.Module) => { + return Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + return path.normalize(path.join( + config.outDir, + "modules", + `${module.path.slice(1).join(path.sep)}.md` + )) + }) +} + +const getAIMarkdownOutputPath = Effect.fnUntraced(function*( + module: Domain.Module, + printable: Printer.Printable +) { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + return path.join( + config.outDir, + "ai", + `${module.path.slice(1).join("-").replace(/\.ts$/, "")}-${printable.name}.md` ) +}) const getModuleMarkdownFiles = (modules: ReadonlyArray ) => - Effect.forEach(modules, (module, order) => - Effect.gen(function*(_) { - const outputPath = yield* _(getModuleMarkdownOutputPath(module)) - const content = yield* _(printModule(module, order + 1)) - return File.createFile(outputPath, content, true) + Effect.forEach(modules, (module, i) => + Effect.gen(function*() { + const outputPath = yield* getModuleMarkdownOutputPath(module) + const moduleContent = yield* Printer.printModule(module) + const tocgen = yield* Effect.promise(async () => { + // @ts-expect-error + return await import("@effect/markdown-toc").then((m) => m.default) + }).pipe(Effect.orDie) + const toc = tocgen(moduleContent, { bullets: "-" }).content + const frontMatter = Printer.printFrontMatter(module, i + 1) + const content = (frontMatter + "\n\n" + moduleContent).replace( + "", + `--- +## Exports Grouped by Category +${toc} +---` + ) + + const prettified = yield* Printer.prettify(content) + return new Domain.File(outputPath, prettified, true) })) -const writeMarkdown = (files: ReadonlyArray ) => - Effect.gen(function*(_) { - const config = yield* _(Configuration.Configuration) - const path = yield* _(Path.Path, Effect.provide(NodePath.layerPosix)) - const fileSystem = yield* _(FileSystem.FileSystem) +const getModulePrintables = (module: Domain.Module) => + [ + module.classes, + module.constants, + module.functions, + module.interfaces, + module.typeAliases, + module.exports, + module.namespaces.flatMap((ns) => + [ns.interfaces, ns.typeAliases].flat().map((doc) => doc.modifyName(`${ns.name}.${doc.name}`)) + ) + ].flat() + +const getAIMarkdownFiles = Effect.fnUntraced(function*(projectName: string, modules: ReadonlyArray ) { + const aiModules = pipe( + modules, + Array.flatMap((module) => + pipe( + getModulePrintables(module), + Array.map((printable) => ({ module, printable })) + ) + ), + Array.filter(({ printable }) => printable.doc.description !== undefined) + ) + + const files = Array.empty () + for (const { module, printable } of aiModules) { + const outputPath = yield* getAIMarkdownOutputPath(module, printable) + const content = yield* Printer.printForAI(projectName, module, printable) + files.push(new Domain.File(outputPath, content, true)) + } + return files +}) + +const getJsonFiles = Effect.fnUntraced(function*( + projectName: string, + modules: ReadonlyArray +) { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + const printables = pipe( + modules, + Array.bindTo("module"), + Array.bind("printable", ({ module }) => getModulePrintables(module)), + Array.map(({ module, printable }) => { + return { + _tag: printable._tag, + module: { + name: module.name, + path: module.path.join("/") + }, + project: projectName, + name: printable.name, + description: printable.doc.description, + deprecated: pipe(printable.doc.deprecated, Array.head, Option.isSome), + examples: printable.doc.examples.map((example) => example), + since: pipe(printable.doc.since, Array.head, Option.getOrNull), + category: pipe(printable.doc.category, Array.head, Option.getOrNull), + signature: "signature" in printable ? printable.signature : null, + sourceUrl: config.srcLink + module.source.sourceFile.getBaseName() + + "#L" + + printable.position.line + } + }) + ) + + return [ + new Domain.File( + path.join(config.outDir, `${projectName.replace("/", "-")}.json`), + JSON.stringify(printables, null, 2), + true + ) + ] +}) + +const maybeGetAIMarkdownFiles = Effect.fnUntraced(function*(modules: ReadonlyArray ) { + const config = yield* Configuration.Configuration + return config.enableAI ? yield* getAIMarkdownFiles(config.projectName, modules) : [] +}) + +const maybeGetJsonFiles = Effect.fnUntraced(function*(modules: ReadonlyArray ) { + const config = yield* Configuration.Configuration + return config.enableJson ? yield* getJsonFiles(config.projectName, modules) : [] +}) + +const writeMarkdown = (files: ReadonlyArray ) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Effect.provide(Path.Path, NodePath.layerPosix) + const fileSystem = yield* FileSystem.FileSystem const pattern = path.normalize(path.join(config.outDir, "**/*.ts.md")) - yield* _(Effect.logDebug(`Deleting ${chalk.black(pattern)}...`)) - const paths = yield* _(glob(pattern)) - yield* _(Effect.forEach(paths, (path) => fileSystem.remove(path, { recursive: true }), { + yield* Effect.logDebug(`Deleting ${chalk.black(pattern)}...`) + const paths = yield* glob(pattern) + yield* Effect.forEach(paths, (path) => fileSystem.remove(path, { recursive: true }), { concurrency: "unbounded" - })) - return yield* _(writeFilesToOutDir(files)) + }) + return yield* writeFilesToOutDir(files) }) /** @internal */ -export const program = Effect.gen(function*(_) { - yield* _(Effect.logInfo("Reading modules...")) - const sourceFiles = yield* _(readSourceFiles) - yield* _(Effect.logInfo("Parsing modules...")) - const modules = yield* _(parseModules(sourceFiles)) - yield* _(typeCheckAndRunExamples(modules)) - yield* _(Effect.logInfo("Creating markdown files...")) - const outputFiles = yield* _(getMarkdown(modules)) - yield* _(Effect.logInfo("Writing markdown files...")) - yield* _(writeMarkdown(outputFiles)) - yield* _(Effect.logInfo(chalk.bold.green("Docs generation succeeded!"))) +export const program = Effect.gen(function*() { + yield* Effect.logInfo("Reading modules...") + const sourceFiles = yield* readSourceFiles + yield* Effect.logInfo("Parsing modules...") + const modules = yield* parseModules(sourceFiles) + yield* Effect.logInfo("Checking modules...") + const errors = yield* Checker.checkModules(modules) + if (errors.length > 0) { + yield* Effect.fail( + new Domain.DocgenError({ + message: `The following errors occurred while checking the modules:\n\n${errors.join("\n\n")}` + }) + ) + } + yield* typeCheckAndRunExamples(modules) + yield* Effect.logInfo("Creating markdown files...") + const outputFiles = yield* getMarkdown(modules) + yield* Effect.logInfo("Writing markdown files...") + yield* writeMarkdown(outputFiles) + yield* Effect.logInfo(chalk.bold.green("✓ Docs generation succeeded!")) }) diff --git a/src/Domain.ts b/src/Domain.ts index 9c93e93..93870c8 100644 --- a/src/Domain.ts +++ b/src/Domain.ts @@ -1,126 +1,192 @@ /** - * @since 1.0.0 + * @since 0.6.0 */ -import type * as Option from "effect/Option" +import type * as Array from "effect/Array" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" import * as Order from "effect/Order" import * as String from "effect/String" +import type * as Parser from "./Parser.js" /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Module extends NamedDoc { - readonly path: ReadonlyArray - readonly classes: ReadonlyArray - readonly interfaces: ReadonlyArray - readonly functions: ReadonlyArray - readonly typeAliases: ReadonlyArray - readonly constants: ReadonlyArray - readonly exports: ReadonlyArray - readonly namespaces: ReadonlyArray -} +export class DocEntry { + constructor( + readonly name: string, + readonly doc: Doc, + readonly signature: string, + readonly position: Position + ) {} -/** - * @category model - * @since 1.0.0 - */ -export type Example = { - body: string - fences?: { - start: string - end: string + modifyName(name: string): this { + const obj = { ...this, name } + Object.setPrototypeOf(obj, Object.getPrototypeOf(this)) + return obj } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Doc { - readonly description: Option.Option - readonly since: Option.Option - readonly deprecated: boolean - readonly examples: ReadonlyArray - readonly category: Option.Option +export declare namespace DocEntry { + /** + * @category model + * @since 0.6.0 + * @deprecated + */ + export type Name = A["name"] } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface NamedDoc extends Doc { - readonly name: string +export class Doc { + constructor( + readonly description: string | undefined, + readonly since: ReadonlyArray , + readonly deprecated: ReadonlyArray , + readonly examples: ReadonlyArray , + readonly category: ReadonlyArray , + readonly throws: ReadonlyArray , + readonly sees: ReadonlyArray , + readonly tags: Record | undefined> + ) {} + + modifyDescription(description: string | undefined): Doc { + return new Doc( + description, + this.since, + this.deprecated, + this.examples, + this.category, + this.throws, + this.sees, + this.tags + ) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Class extends NamedDoc { - readonly _tag: "Class" - readonly signature: string - readonly methods: ReadonlyArray - readonly staticMethods: ReadonlyArray - readonly properties: ReadonlyArray +export class Module { + constructor( + readonly source: Parser.SourceShape, + readonly name: string, + readonly doc: Doc, + readonly path: Array.NonEmptyReadonlyArray , + readonly classes: ReadonlyArray , + readonly interfaces: ReadonlyArray , + readonly functions: ReadonlyArray , + readonly typeAliases: ReadonlyArray , + readonly constants: ReadonlyArray , + readonly exports: ReadonlyArray , + readonly namespaces: ReadonlyArray + ) {} } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Method extends NamedDoc { - readonly signatures: ReadonlyArray +export class Class extends DocEntry { + readonly _tag = "Class" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position, + readonly methods: ReadonlyArray , + readonly staticMethods: ReadonlyArray , + readonly properties: ReadonlyArray + ) { + super(name, doc, signature, position) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Property extends NamedDoc { - readonly signature: string +export class Interface extends DocEntry { + readonly _tag = "Interface" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position + ) { + super(name, doc, signature, position) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Interface extends NamedDoc { - readonly _tag: "Interface" - readonly signature: string +export interface Position { + readonly line: number + readonly column: number } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Function extends NamedDoc { - readonly _tag: "Function" - readonly signatures: ReadonlyArray +export class Function extends DocEntry { + readonly _tag = "Function" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position + ) { + super(name, doc, signature, position) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface TypeAlias extends NamedDoc { - readonly _tag: "TypeAlias" - readonly signature: string +export class TypeAlias extends DocEntry { + readonly _tag = "TypeAlias" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position + ) { + super(name, doc, signature, position) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Constant extends NamedDoc { - readonly _tag: "Constant" - readonly signature: string +export class Constant extends DocEntry { + readonly _tag = "Constant" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position + ) { + super(name, doc, signature, position) + } } /** * These are manual exports, like: * - * ```ts + * ```ts skip-type-checking * const _null = ... * * export { @@ -129,178 +195,93 @@ export interface Constant extends NamedDoc { * ``` * * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Export extends NamedDoc { - readonly _tag: "Export" - readonly signature: string +export class Export extends DocEntry { + readonly _tag = "Export" + constructor( + name: string, + doc: Doc, + signature: string, + position: Position, + readonly isNamespaceExport: boolean + ) { + super(name, doc, signature, position) + } } /** * @category model - * @since 1.0.0 + * @since 0.6.0 */ -export interface Namespace extends NamedDoc { - readonly _tag: "Namespace" - readonly interfaces: ReadonlyArray - readonly typeAliases: ReadonlyArray - readonly namespaces: ReadonlyArray +export class Namespace { + readonly _tag = "Namespace" + constructor( + readonly name: string, + readonly doc: Doc, + readonly position: Position, + readonly interfaces: ReadonlyArray , + readonly typeAliases: ReadonlyArray , + readonly namespaces: ReadonlyArray + ) {} } -// ------------------------------------------------------------------------------------- -// constructors -// ------------------------------------------------------------------------------------- - -/** - * @category constructors - * @since 1.0.0 - */ -export const createDoc = ( - description: Option.Option , - since: Option.Option , - deprecated: boolean, - examples: ReadonlyArray , - category: Option.Option -): Doc => ({ description, since, deprecated, examples, category }) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createNamedDoc = ( - name: string, - description: Option.Option , - since: Option.Option , - deprecated: boolean, - examples: ReadonlyArray , - category: Option.Option -): NamedDoc => ({ name, description, since, deprecated, examples, category }) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createModule = ( - doc: NamedDoc, - path: ReadonlyArray , - classes: ReadonlyArray , - interfaces: ReadonlyArray , - functions: ReadonlyArray , - typeAliases: ReadonlyArray , - constants: ReadonlyArray , - exports: ReadonlyArray , - namespaces: ReadonlyArray -): Module => ({ - ...doc, - path, - classes, - interfaces, - functions, - typeAliases, - constants, - exports, - namespaces -}) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createClass = ( - doc: NamedDoc, - signature: string, - methods: ReadonlyArray , - staticMethods: ReadonlyArray , - properties: ReadonlyArray -): Class => ({ _tag: "Class", ...doc, signature, methods, staticMethods, properties }) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createConstant = (doc: NamedDoc, signature: string): Constant => ({ - _tag: "Constant", - ...doc, - signature -}) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createMethod = (doc: NamedDoc, signatures: ReadonlyArray ): Method => ({ - ...doc, - signatures -}) - -/** - * @category constructors - * @since 1.0.0 - */ -export const createProperty = (doc: NamedDoc, signature: string): Property => ({ - ...doc, - signature -}) - /** - * @category constructors - * @since 1.0.0 + * A comparator function for sorting `Module` objects by their file path, represented as a string. + * The file path is converted to lowercase before comparison. + * + * @category sorting + * @since 0.6.0 */ -export const createInterface = (doc: NamedDoc, signature: string): Interface => ({ - _tag: "Interface", - ...doc, - signature -}) +export const ByPath: Order.Order = Order.mapInput( + String.Order, + (module: Module) => module.path.join("/").toLowerCase() +) /** - * @category constructors - * @since 1.0.0 + * Represents a file which can be optionally overwriteable. + * + * @category model + * @since 0.6.0 */ -export const createFunction = (doc: NamedDoc, signatures: ReadonlyArray ): Function => ({ - _tag: "Function", - ...doc, - signatures -}) +export class File { + constructor( + readonly path: string, + readonly content: string, + readonly isOverwriteable: boolean = false + ) {} +} /** - * @category constructors - * @since 1.0.0 + * @category symbol + * @since 0.6.0 */ -export const createTypeAlias = (doc: NamedDoc, signature: string): TypeAlias => ({ - _tag: "TypeAlias", - ...doc, - signature -}) +export const DocgenErrorTypeId = Symbol.for("@effect/docgen/DocgenError") /** - * @category constructors - * @since 1.0.0 + * @category symbol + * @since 0.6.0 */ -export const createExport = (doc: NamedDoc, signature: string): Export => ({ - _tag: "Export", - ...doc, - signature -}) +export type DocgenErrorTypeId = typeof DocgenErrorTypeId /** - * @category constructors - * @since 1.0.0 + * @category model + * @since 0.6.0 */ -export const createNamespace = ( - doc: NamedDoc, - interfaces: ReadonlyArray