1#!/usr/bin/env node
2
3/**
4 * Command-line interface for generating Effect API documentation.
5 *
6 * @since 0.6.0
7 */
8import * as Array from "effect/Array"
9import * as CliError from "effect/cli/CliError"
10import * as Command from "effect/cli/Command"
11import * as Flag from "effect/cli/Flag"
12import * as Config from "effect/Config"
13import * as Effect from "effect/Effect"
14import * as Option from "effect/Option"
15import * as Schema from "effect/Schema"
16import PackageJson from "../package.json" with { type: "json" }
17import * as Configuration from "./Configuration.ts"
18import * as Core from "./Core.ts"
19import * as Domain from "./Domain.ts"
20
21const 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
29const 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
35const 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
40const 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
45const 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
50const 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
55const 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
60const 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
65const 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
73const 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
78const 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
83const 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
88const 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
100const compilerOptionsSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown))
101
102const 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
119const 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
124const parseCompilerOptionsInline = parseCompilerOptionsFlag(
125 "parse-compiler-options",
126 "The TypeScript compiler options to use for parsing source files"
127).pipe(Flag.optional)
128
129const 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
134const examplesCompilerOptionsInline = parseCompilerOptionsFlag(
135 "examples-compiler-options",
136 "The TypeScript compiler options to use for examples"
137).pipe(Flag.optional)
138
139const 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 */
160export const docgenCommand = Command.make("docgen", options)
161
162/** @internal */
163export 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
223const 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 */
242export const cli = Command.runWith(command, {
243 version: PackageJson["version"]
244})