| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * Command-line interface for generating Effect API documentation. |
| 5 | * |
| 6 | * @since 0.6.0 |
| 7 | */ |
| 8 | import * as Array from "effect/Array" |
| 9 | import * as CliError from "effect/cli/CliError" |
| 10 | import * as Command from "effect/cli/Command" |
| 11 | import * as Flag from "effect/cli/Flag" |
| 12 | import * as Config from "effect/Config" |
| 13 | import * as Effect from "effect/Effect" |
| 14 | import * as Option from "effect/Option" |
| 15 | import * as Schema from "effect/Schema" |
| 16 | import PackageJson from "../package.json" with { type: "json" } |
| 17 | import * as Configuration from "./Configuration.ts" |
| 18 | import * as Core from "./Core.ts" |
| 19 | import * as Domain from "./Domain.ts" |
| 20 | |
| 21 | const projectHomepage = Flag.String("homepage").pipe( |
| 22 | Flag.withFallbackConfig(Config.String("projectHomepage")), |
| 23 | Flag.withDescription( |
| 24 | "The link to the project homepage (will be shown in the Auxiliary Links of the generated documentation)" |
| 25 | ), |
| 26 | Flag.optional |
| 27 | ) |
| 28 | |
| 29 | const srcLink = Flag.String("srcLink").pipe( |
| 30 | Flag.withFallbackConfig(Config.String("srcLink")), |
| 31 | Flag.withDescription("The link to the project source code"), |
| 32 | Flag.optional |
| 33 | ) |
| 34 | |
| 35 | const srcDir = Flag.Directory("src", { mustExist: true }).pipe( |
| 36 | Flag.withFallbackConfig(Config.String("src").pipe(Config.withDefault("src"))), |
| 37 | Flag.withDescription("The directory in which docgen will search for TypeScript files to parse") |
| 38 | ) |
| 39 | |
| 40 | const outDir = Flag.Directory("out").pipe( |
| 41 | Flag.withFallbackConfig(Config.String("out").pipe(Config.withDefault("docs"))), |
| 42 | Flag.withDescription("The directory to which docgen will generate its output markdown documents") |
| 43 | ) |
| 44 | |
| 45 | const theme = Flag.String("theme").pipe( |
| 46 | Flag.withFallbackConfig(Config.String("theme").pipe(Config.withDefault(Configuration.DEFAULT_THEME))), |
| 47 | Flag.withDescription("The Jekyll theme that should be used for the generated documentation") |
| 48 | ) |
| 49 | |
| 50 | const disableSearch = Flag.Boolean("disable-search").pipe( |
| 51 | Flag.withDescription("Whether or not search should be enabled in the generated documentation"), |
| 52 | Flag.optional |
| 53 | ) |
| 54 | |
| 55 | const enableSearchAlias = Flag.Boolean("enable-search").pipe( |
| 56 | Flag.withDescription("Whether or not search should be enabled in the generated documentation"), |
| 57 | Flag.optional |
| 58 | ) |
| 59 | |
| 60 | const enforceDescriptions = Flag.Boolean("enforce-descriptions").pipe( |
| 61 | Flag.withDescription("Whether or not a description for each module export should be required"), |
| 62 | Flag.optional |
| 63 | ) |
| 64 | |
| 65 | const enforceExamples = Flag.Boolean("enforce-examples").pipe( |
| 66 | Flag.withDescription( |
| 67 | "Whether or not @example tags for each module export should be required " + |
| 68 | "(Note: examples will not be enforced in module documentation)" |
| 69 | ), |
| 70 | Flag.optional |
| 71 | ) |
| 72 | |
| 73 | const noEnforceVersion = Flag.Boolean("no-enforce-version").pipe( |
| 74 | Flag.withDescription("Whether or not @since tags for each module export should be required"), |
| 75 | Flag.optional |
| 76 | ) |
| 77 | |
| 78 | const enforceVersionAlias = Flag.Boolean("enforce-version").pipe( |
| 79 | Flag.withDescription("Whether or not @since tags for each module export should be required"), |
| 80 | Flag.optional |
| 81 | ) |
| 82 | |
| 83 | const runExamples = Flag.Boolean("run-examples").pipe( |
| 84 | Flag.withDescription("Whether or not to execute examples discovered in the TypeScript source files"), |
| 85 | Flag.optional |
| 86 | ) |
| 87 | |
| 88 | const exclude = Flag.String("exclude").pipe( |
| 89 | Flag.between(0, Infinity), |
| 90 | Flag.withFallbackConfig( |
| 91 | Config.Array(Schema.String, "exclude").pipe( |
| 92 | Config.withDefault(Array.empty<string>()) |
| 93 | ) |
| 94 | ), |
| 95 | Flag.withDescription( |
| 96 | "An array of glob patterns specifying files that should be excluded from the generated documentation" |
| 97 | ) |
| 98 | ) |
| 99 | |
| 100 | const compilerOptionsSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)) |
| 101 | |
| 102 | const parseCompilerOptionsFlag = (name: string, description: string) => |
| 103 | Flag.String(name).pipe( |
| 104 | Flag.withDescription(description), |
| 105 | Flag.mapEffect((value) => |
| 106 | Schema.decodeUnknownEffect(compilerOptionsSchema)(value).pipe( |
| 107 | Effect.mapError((error) => |
| 108 | new CliError.InvalidValue({ |
| 109 | option: name, |
| 110 | value, |
| 111 | expected: `a JSON record (${globalThis.String(error)})`, |
| 112 | kind: "flag" |
| 113 | }) |
| 114 | ) |
| 115 | ) |
| 116 | ) |
| 117 | ) |
| 118 | |
| 119 | const parseCompilerOptionsFile = Flag.File("parse-tsconfig-file", { mustExist: true }).pipe( |
| 120 | Flag.withDescription("The TypeScript TSConfig file to use for parsing source files"), |
| 121 | Flag.optional |
| 122 | ) |
| 123 | |
| 124 | const parseCompilerOptionsInline = parseCompilerOptionsFlag( |
| 125 | "parse-compiler-options", |
| 126 | "The TypeScript compiler options to use for parsing source files" |
| 127 | ).pipe(Flag.optional) |
| 128 | |
| 129 | const examplesCompilerOptionsFile = Flag.File("examples-tsconfig-file", { mustExist: true }).pipe( |
| 130 | Flag.withDescription("The TypeScript TSConfig file to use for examples"), |
| 131 | Flag.optional |
| 132 | ) |
| 133 | |
| 134 | const examplesCompilerOptionsInline = parseCompilerOptionsFlag( |
| 135 | "examples-compiler-options", |
| 136 | "The TypeScript compiler options to use for examples" |
| 137 | ).pipe(Flag.optional) |
| 138 | |
| 139 | const options = { |
| 140 | projectHomepage, |
| 141 | srcLink, |
| 142 | srcDir, |
| 143 | outDir, |
| 144 | theme, |
| 145 | disableSearch, |
| 146 | enableSearchAlias, |
| 147 | enforceDescriptions, |
| 148 | enforceExamples, |
| 149 | noEnforceVersion, |
| 150 | enforceVersionAlias, |
| 151 | runExamples, |
| 152 | exclude, |
| 153 | parseCompilerOptionsFile, |
| 154 | parseCompilerOptionsInline, |
| 155 | examplesCompilerOptionsFile, |
| 156 | examplesCompilerOptionsInline |
| 157 | } |
| 158 | |
| 159 | /** @internal */ |
| 160 | export const docgenCommand = Command.make("docgen", options) |
| 161 | |
| 162 | /** @internal */ |
| 163 | export const loadConfiguration = Effect.fnUntraced(function*( |
| 164 | args: Command.Command.Config.Infer<typeof options> |
| 165 | ) { |
| 166 | const { |
| 167 | enableSearchAlias, |
| 168 | disableSearch, |
| 169 | enforceDescriptions, |
| 170 | enforceExamples, |
| 171 | enforceVersionAlias, |
| 172 | noEnforceVersion, |
| 173 | runExamples, |
| 174 | examplesCompilerOptionsFile, |
| 175 | examplesCompilerOptionsInline, |
| 176 | parseCompilerOptionsFile, |
| 177 | parseCompilerOptionsInline, |
| 178 | ...config |
| 179 | } = args |
| 180 | if (Option.isSome(parseCompilerOptionsFile) && Option.isSome(parseCompilerOptionsInline)) { |
| 181 | return yield* new CliError.InvalidValue({ |
| 182 | option: "parse-compiler-options", |
| 183 | value: JSON.stringify(parseCompilerOptionsInline.value), |
| 184 | expected: "only one of --parse-tsconfig-file or --parse-compiler-options", |
| 185 | kind: "flag" |
| 186 | }) |
| 187 | } |
| 188 | if (Option.isSome(examplesCompilerOptionsFile) && Option.isSome(examplesCompilerOptionsInline)) { |
| 189 | return yield* new CliError.InvalidValue({ |
| 190 | option: "examples-compiler-options", |
| 191 | value: JSON.stringify(examplesCompilerOptionsInline.value), |
| 192 | expected: "only one of --examples-tsconfig-file or --examples-compiler-options", |
| 193 | kind: "flag" |
| 194 | }) |
| 195 | } |
| 196 | const configuredEnableSearch = yield* Config.Boolean("enableSearch").pipe(Effect.orElseSucceed(() => true)) |
| 197 | const configuredEnforceDescriptions = yield* Config.Boolean("enforceDescriptions").pipe( |
| 198 | Effect.orElseSucceed(() => false) |
| 199 | ) |
| 200 | const configuredEnforceExamples = yield* Config.Boolean("enforceExamples").pipe( |
| 201 | Effect.orElseSucceed(() => false) |
| 202 | ) |
| 203 | const configuredEnforceVersion = yield* Config.Boolean("enforceVersion").pipe(Effect.orElseSucceed(() => true)) |
| 204 | const configuredRunExamples = yield* Config.Boolean("runExamples").pipe(Effect.orElseSucceed(() => false)) |
| 205 | return yield* Configuration.load({ |
| 206 | ...config, |
| 207 | enableSearch: Option.match(disableSearch, { |
| 208 | onNone: () => Option.getOrElse(enableSearchAlias, () => configuredEnableSearch), |
| 209 | onSome: (disabled) => !disabled |
| 210 | }), |
| 211 | enforceDescriptions: Option.getOrElse(enforceDescriptions, () => configuredEnforceDescriptions), |
| 212 | enforceExamples: Option.getOrElse(enforceExamples, () => configuredEnforceExamples), |
| 213 | enforceVersion: Option.match(noEnforceVersion, { |
| 214 | onNone: () => Option.getOrElse(enforceVersionAlias, () => configuredEnforceVersion), |
| 215 | onSome: (disabled) => !disabled |
| 216 | }), |
| 217 | runExamples: Option.getOrElse(runExamples, () => configuredRunExamples), |
| 218 | parseCompilerOptions: Option.orElse(parseCompilerOptionsFile, () => parseCompilerOptionsInline), |
| 219 | examplesCompilerOptions: Option.orElse(examplesCompilerOptionsFile, () => examplesCompilerOptionsInline) |
| 220 | }) |
| 221 | }) |
| 222 | |
| 223 | const command = docgenCommand.pipe( |
| 224 | Command.withHandler(() => |
| 225 | Effect.scoped(Core.program).pipe( |
| 226 | Effect.catchTag("DocgenError", (error) => |
| 227 | Effect.gen(function*() { |
| 228 | const config = yield* Configuration.Configuration |
| 229 | return yield* new Domain.DocgenError({ message: `[${config.projectName}] ${error.message}` }) |
| 230 | })) |
| 231 | ) |
| 232 | ), |
| 233 | Command.provideEffect(Configuration.Configuration, loadConfiguration) |
| 234 | ) |
| 235 | |
| 236 | /** |
| 237 | * Runs the docgen command-line program. |
| 238 | * |
| 239 | * @category running |
| 240 | * @since 0.6.0 |
| 241 | */ |
| 242 | export const cli = Command.runWith(command, { |
| 243 | version: PackageJson["version"] |
| 244 | }) |