@appium/docutils 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +2 -5
  2. package/build/lib/build/mkdocs.d.ts +58 -0
  3. package/build/lib/build/mkdocs.d.ts.map +1 -0
  4. package/build/lib/build/mkdocs.js +80 -0
  5. package/build/lib/build/mkdocs.js.map +1 -0
  6. package/build/lib/build/typedoc.d.ts +55 -0
  7. package/build/lib/build/typedoc.d.ts.map +1 -0
  8. package/build/lib/build/typedoc.js +120 -0
  9. package/build/lib/build/typedoc.js.map +1 -0
  10. package/build/lib/build-api.d.ts +18 -0
  11. package/build/lib/build-api.d.ts.map +1 -0
  12. package/build/lib/build-api.js +75 -0
  13. package/build/lib/build-api.js.map +1 -0
  14. package/build/lib/build.d.ts +21 -0
  15. package/build/lib/build.d.ts.map +1 -0
  16. package/build/lib/build.js +71 -0
  17. package/build/lib/build.js.map +1 -0
  18. package/build/lib/builder/deploy.d.ts +89 -0
  19. package/build/lib/builder/deploy.d.ts.map +1 -0
  20. package/build/lib/builder/deploy.js +102 -0
  21. package/build/lib/builder/deploy.js.map +1 -0
  22. package/build/lib/builder/index.d.ts +5 -0
  23. package/build/lib/builder/index.d.ts.map +1 -0
  24. package/build/lib/builder/index.js +21 -0
  25. package/build/lib/builder/index.js.map +1 -0
  26. package/build/lib/builder/nav.d.ts +81 -0
  27. package/build/lib/builder/nav.d.ts.map +1 -0
  28. package/build/lib/builder/nav.js +272 -0
  29. package/build/lib/builder/nav.js.map +1 -0
  30. package/build/lib/builder/reference.d.ts +57 -0
  31. package/build/lib/builder/reference.d.ts.map +1 -0
  32. package/build/lib/builder/reference.js +129 -0
  33. package/build/lib/builder/reference.js.map +1 -0
  34. package/build/lib/builder/site.d.ts +55 -0
  35. package/build/lib/builder/site.d.ts.map +1 -0
  36. package/build/lib/builder/site.js +81 -0
  37. package/build/lib/builder/site.js.map +1 -0
  38. package/build/lib/cli/command/build.d.ts +178 -0
  39. package/build/lib/cli/command/build.d.ts.map +1 -0
  40. package/build/lib/cli/command/build.js +223 -0
  41. package/build/lib/cli/command/build.js.map +1 -0
  42. package/build/lib/cli/command/deploy.d.ts +1 -0
  43. package/build/lib/cli/command/deploy.d.ts.map +1 -0
  44. package/build/lib/cli/command/deploy.js +2 -0
  45. package/build/lib/cli/command/deploy.js.map +1 -0
  46. package/build/lib/cli/command/index.d.ts +4 -0
  47. package/build/lib/cli/command/index.d.ts.map +1 -0
  48. package/build/lib/cli/command/index.js +13 -0
  49. package/build/lib/cli/command/index.js.map +1 -0
  50. package/build/lib/cli/command/init.d.ts +143 -0
  51. package/build/lib/cli/command/init.d.ts.map +1 -0
  52. package/build/lib/cli/command/init.js +164 -0
  53. package/build/lib/cli/command/init.js.map +1 -0
  54. package/build/lib/cli/command/validate.d.ts +76 -0
  55. package/build/lib/cli/command/validate.d.ts.map +1 -0
  56. package/build/lib/cli/command/validate.js +115 -0
  57. package/build/lib/cli/command/validate.js.map +1 -0
  58. package/build/lib/cli/command-init.d.ts +143 -0
  59. package/build/lib/cli/command-init.d.ts.map +1 -0
  60. package/build/lib/cli/command-init.js +164 -0
  61. package/build/lib/cli/command-init.js.map +1 -0
  62. package/build/lib/cli/command-validate.d.ts +52 -0
  63. package/build/lib/cli/command-validate.d.ts.map +1 -0
  64. package/build/lib/cli/command-validate.js +66 -0
  65. package/build/lib/cli/command-validate.js.map +1 -0
  66. package/build/lib/cli/config.d.ts +28 -0
  67. package/build/lib/cli/config.d.ts.map +1 -0
  68. package/build/lib/cli/config.js +114 -0
  69. package/build/lib/cli/config.js.map +1 -0
  70. package/build/lib/cli/index.d.ts +13 -0
  71. package/build/lib/cli/index.d.ts.map +1 -0
  72. package/build/lib/cli/index.js +91 -0
  73. package/build/lib/cli/index.js.map +1 -0
  74. package/build/lib/cli/init.d.ts +143 -0
  75. package/build/lib/cli/init.d.ts.map +1 -0
  76. package/build/lib/cli/init.js +164 -0
  77. package/build/lib/cli/init.js.map +1 -0
  78. package/build/lib/cli/options.d.ts +1 -0
  79. package/build/lib/cli/options.d.ts.map +1 -0
  80. package/build/lib/cli/options.js +2 -0
  81. package/build/lib/cli/options.js.map +1 -0
  82. package/build/lib/cli/validate.d.ts +1 -0
  83. package/build/lib/cli/validate.d.ts.map +1 -0
  84. package/build/lib/cli/validate.js +2 -0
  85. package/build/lib/cli/validate.js.map +1 -0
  86. package/build/lib/cli.d.ts +10 -0
  87. package/build/lib/cli.d.ts.map +1 -0
  88. package/build/lib/cli.js +328 -0
  89. package/build/lib/cli.js.map +1 -0
  90. package/build/lib/constants.d.ts +125 -0
  91. package/build/lib/constants.d.ts.map +1 -0
  92. package/build/lib/constants.js +133 -0
  93. package/build/lib/constants.js.map +1 -0
  94. package/build/lib/error.d.ts +3 -0
  95. package/build/lib/error.d.ts.map +1 -0
  96. package/build/lib/error.js +7 -0
  97. package/build/lib/error.js.map +1 -0
  98. package/build/lib/fs.d.ts +142 -0
  99. package/build/lib/fs.d.ts.map +1 -0
  100. package/build/lib/fs.js +237 -0
  101. package/build/lib/fs.js.map +1 -0
  102. package/build/lib/index.d.ts +5 -2
  103. package/build/lib/index.d.ts.map +1 -1
  104. package/build/lib/index.js +4 -1
  105. package/build/lib/index.js.map +1 -1
  106. package/build/lib/init-task.d.ts +49 -0
  107. package/build/lib/init-task.d.ts.map +1 -0
  108. package/build/lib/init-task.js +95 -0
  109. package/build/lib/init-task.js.map +1 -0
  110. package/build/lib/init.d.ts +202 -0
  111. package/build/lib/init.d.ts.map +1 -0
  112. package/build/lib/init.js +225 -0
  113. package/build/lib/init.js.map +1 -0
  114. package/build/lib/io.d.ts +1 -0
  115. package/build/lib/io.d.ts.map +1 -0
  116. package/build/lib/io.js +2 -0
  117. package/build/lib/io.js.map +1 -0
  118. package/build/lib/logger.d.ts +17 -2
  119. package/build/lib/logger.d.ts.map +1 -1
  120. package/build/lib/logger.js +187 -2
  121. package/build/lib/logger.js.map +1 -1
  122. package/build/lib/mike.d.ts +3 -0
  123. package/build/lib/mike.d.ts.map +1 -1
  124. package/build/lib/mike.js +4 -0
  125. package/build/lib/mike.js.map +1 -1
  126. package/build/lib/mkdocs.d.ts +51 -12
  127. package/build/lib/mkdocs.d.ts.map +1 -1
  128. package/build/lib/mkdocs.js +64 -32
  129. package/build/lib/mkdocs.js.map +1 -1
  130. package/build/lib/model.d.ts +80 -0
  131. package/build/lib/model.d.ts.map +1 -0
  132. package/build/lib/model.js +8 -0
  133. package/build/lib/model.js.map +1 -0
  134. package/build/lib/nav.d.ts +47 -0
  135. package/build/lib/nav.d.ts.map +1 -0
  136. package/build/lib/nav.js +132 -0
  137. package/build/lib/nav.js.map +1 -0
  138. package/build/lib/scaffold.d.ts +95 -0
  139. package/build/lib/scaffold.d.ts.map +1 -0
  140. package/build/lib/scaffold.js +103 -0
  141. package/build/lib/scaffold.js.map +1 -0
  142. package/build/lib/test.d.ts +9 -0
  143. package/build/lib/test.d.ts.map +1 -0
  144. package/build/lib/test.js +2 -0
  145. package/build/lib/test.js.map +1 -0
  146. package/build/lib/typedoc.d.ts +55 -0
  147. package/build/lib/typedoc.d.ts.map +1 -0
  148. package/build/lib/typedoc.js +122 -0
  149. package/build/lib/typedoc.js.map +1 -0
  150. package/build/lib/types.d.ts +52 -0
  151. package/build/lib/types.d.ts.map +1 -0
  152. package/build/lib/types.js +7 -0
  153. package/build/lib/types.js.map +1 -0
  154. package/build/lib/util.d.ts +42 -0
  155. package/build/lib/util.d.ts.map +1 -0
  156. package/build/lib/util.js +56 -0
  157. package/build/lib/util.js.map +1 -0
  158. package/build/lib/validate.d.ts +218 -0
  159. package/build/lib/validate.d.ts.map +1 -0
  160. package/build/lib/validate.js +501 -0
  161. package/build/lib/validate.js.map +1 -0
  162. package/build/lib/validation/base-validator.d.ts +218 -0
  163. package/build/lib/validation/base-validator.d.ts.map +1 -0
  164. package/build/lib/validation/base-validator.js +453 -0
  165. package/build/lib/validation/base-validator.js.map +1 -0
  166. package/build/lib/validation/mkdocs-validator.d.ts +5 -0
  167. package/build/lib/validation/mkdocs-validator.d.ts.map +1 -0
  168. package/build/lib/validation/mkdocs-validator.js +54 -0
  169. package/build/lib/validation/mkdocs-validator.js.map +1 -0
  170. package/build/lib/validation/python-validator.d.ts +1 -0
  171. package/build/lib/validation/python-validator.d.ts.map +1 -0
  172. package/build/lib/validation/python-validator.js +2 -0
  173. package/build/lib/validation/python-validator.js.map +1 -0
  174. package/build/lib/validation/python.d.ts +1 -0
  175. package/build/lib/validation/python.d.ts.map +1 -0
  176. package/build/lib/validation/python.js +2 -0
  177. package/build/lib/validation/python.js.map +1 -0
  178. package/build/lib/validation/validate.d.ts +221 -0
  179. package/build/lib/validation/validate.d.ts.map +1 -0
  180. package/build/lib/validation/validate.js +508 -0
  181. package/build/lib/validation/validate.js.map +1 -0
  182. package/build/lib/validation/validator.d.ts +220 -0
  183. package/build/lib/validation/validator.d.ts.map +1 -0
  184. package/build/lib/validation/validator.js +470 -0
  185. package/build/lib/validation/validator.js.map +1 -0
  186. package/lib/builder/deploy.ts +217 -0
  187. package/lib/builder/index.ts +4 -0
  188. package/lib/builder/nav.ts +393 -0
  189. package/lib/builder/reference.ts +191 -0
  190. package/lib/builder/site.ts +143 -0
  191. package/lib/cli/command/build.ts +229 -0
  192. package/lib/cli/command/index.ts +3 -0
  193. package/lib/cli/command/init.ts +166 -0
  194. package/lib/cli/command/validate.ts +122 -0
  195. package/lib/cli/config.ts +89 -0
  196. package/lib/cli/index.ts +88 -0
  197. package/lib/constants.ts +150 -0
  198. package/lib/error.ts +1 -0
  199. package/lib/fs.ts +274 -0
  200. package/lib/index.ts +5 -0
  201. package/lib/init.ts +319 -0
  202. package/lib/logger.ts +198 -0
  203. package/lib/mike.js +4 -0
  204. package/lib/model.ts +92 -0
  205. package/lib/scaffold.ts +225 -0
  206. package/lib/util.ts +76 -0
  207. package/lib/validate.ts +728 -0
  208. package/package.json +38 -6
  209. package/requirements.txt +4 -0
  210. package/tsconfig.json +2 -1
  211. package/build/tsconfig.tsbuildinfo +0 -1
  212. package/lib/index.js +0 -2
  213. package/lib/logger.js +0 -3
  214. package/lib/mkdocs.js +0 -43
@@ -0,0 +1,728 @@
1
+ /**
2
+ * Validates an environment for building documentation
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {fs} from '@appium/support';
8
+ import chalk from 'chalk';
9
+ import _ from 'lodash';
10
+ import {EventEmitter} from 'node:events';
11
+ import path from 'node:path';
12
+ import pluralize from 'pluralize';
13
+ import {satisfies} from 'semver';
14
+ import {exec} from 'teen_process';
15
+ import {
16
+ DEFAULT_REL_TYPEDOC_OUT_PATH,
17
+ DOCUTILS_PKG,
18
+ NAME_BIN,
19
+ NAME_ERR_ENOENT,
20
+ NAME_MKDOCS,
21
+ NAME_MKDOCS_YML,
22
+ NAME_NPM,
23
+ NAME_PACKAGE_JSON,
24
+ NAME_PIP,
25
+ NAME_PYTHON,
26
+ NAME_REQUIREMENTS_TXT,
27
+ NAME_TSCONFIG_JSON,
28
+ NAME_TYPEDOC,
29
+ NAME_TYPEDOC_JSON,
30
+ NAME_TYPESCRIPT,
31
+ REQUIREMENTS_TXT_PATH,
32
+ } from './constants';
33
+ import {DocutilsError} from './error';
34
+ import {
35
+ findPkgDir,
36
+ findMkDocsYml,
37
+ readJson5,
38
+ readTypedocJson,
39
+ readYaml,
40
+ whichMkDocs,
41
+ whichNpm,
42
+ whichPython,
43
+ readMkDocsYml,
44
+ } from './fs';
45
+ import logger from './logger';
46
+ import {MkDocsYml, PipPackage, TypeDocJson} from './model';
47
+ import {relative} from './util';
48
+
49
+ /**
50
+ * Matches the Python version string from `python --version`
51
+ */
52
+ const PYTHON_VER_STR = 'Python 3.';
53
+
54
+ /**
55
+ * Matches the TypeScript version string from `tsc --version`
56
+ */
57
+ const TYPESCRIPT_VERSION_REGEX = /Version\s(\d+\.\d+\..+)/;
58
+
59
+ /**
60
+ * Matches the TypeDoc version string from `typedoc --version`
61
+ */
62
+ const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
63
+
64
+ /**
65
+ * Matches the MkDocs version string from `mkdocs --version`
66
+ */
67
+ const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
68
+
69
+ const log = logger.withTag('validate');
70
+
71
+ /**
72
+ * The "kinds" of validation which were requested to be performed
73
+ */
74
+ export type ValidationKind =
75
+ | typeof NAME_PYTHON
76
+ | typeof NAME_TYPESCRIPT
77
+ | typeof NAME_TYPEDOC
78
+ | typeof NAME_NPM
79
+ | typeof NAME_MKDOCS;
80
+
81
+ /**
82
+ * This class is designed to run _all_ validation checks (as requested by the user), and emit events for
83
+ * each failure encountered.
84
+ *
85
+ * Whenever a method _rejects or throws_, this is considered an "unexpected" error, and the validation
86
+ * will abort.
87
+ *
88
+ * @todo Use [`strict-event-emitter-types`](https://npm.im/strict-event-emitter-types)
89
+ */
90
+ export class DocutilsValidator extends EventEmitter {
91
+ /**
92
+ * Current working directory. Defaults to `process.cwd()`
93
+ * @todo This cannot yet be overriden by user
94
+ */
95
+ protected readonly cwd: string;
96
+
97
+ /**
98
+ * Path to `npm` executable.
99
+ */
100
+ protected readonly npmPath: string | undefined;
101
+
102
+ /**
103
+ * Path to `python` executable.
104
+ */
105
+ protected readonly pythonPath: string | undefined;
106
+
107
+ /**
108
+ * List of validations to perform
109
+ */
110
+ protected readonly validations = new Set<ValidationKind>();
111
+
112
+ /**
113
+ * Mapping of error messages to errors.
114
+ *
115
+ * Used to prevent duplicate emission of errors and track error count; if non-empty, the validation
116
+ * process should be considered to have failed.
117
+ *
118
+ * Reset after {@linkcode DocutilsValidator.validate validate} completes.
119
+ */
120
+ protected emittedErrors = new Map<string, DocutilsError>();
121
+
122
+ /**
123
+ * Path to `mkdocs.yml`. If not provided, will be lazily resolved.
124
+ */
125
+ protected mkDocsYmlPath: string | undefined;
126
+
127
+ /**
128
+ * Path to `package.json`. If not provided, will be lazily resolved.
129
+ */
130
+ protected packageJsonPath: string | undefined;
131
+
132
+ /**
133
+ * Path to the package directory. If not provided, will be lazily resolved.
134
+ */
135
+ protected pkgDir: string | undefined;
136
+
137
+ /**
138
+ * Path to `tsconfig.json`. If not provided, will be lazily resolved.
139
+ */
140
+ protected tsconfigJsonPath: string | undefined;
141
+
142
+ /**
143
+ * Path to `typedoc.json`. If not provided, will be lazily resolved.
144
+ */
145
+ protected typeDocJsonPath: string | undefined;
146
+
147
+ /**
148
+ * Emitted when validation begins with a list of validation kinds to be performed
149
+ * @event
150
+ */
151
+ public static readonly BEGIN = 'begin';
152
+
153
+ /**
154
+ * Emitted when validation ends with an error count
155
+ * @event
156
+ */
157
+ public static readonly END = 'end';
158
+
159
+ /**
160
+ * Emitted when a validation fails, with the associated {@linkcode DocutilsError}
161
+ * @event
162
+ */
163
+ public static readonly FAILURE = 'fail';
164
+
165
+ /**
166
+ * Emitted when a validation succeeds
167
+ * @event
168
+ */
169
+ public static readonly SUCCESS = 'ok';
170
+
171
+ private requirementsTxt: PipPackage[] | undefined;
172
+
173
+ /**
174
+ * Creates a listener to track errors emitted
175
+ */
176
+ constructor(opts: DocutilsValidatorOpts = {}) {
177
+ super();
178
+
179
+ this.packageJsonPath = opts.packageJson;
180
+ this.pythonPath = opts.pythonPath;
181
+ this.cwd = opts.cwd ?? process.cwd();
182
+ this.tsconfigJsonPath = opts.tsconfigJson;
183
+ this.typeDocJsonPath = opts.typedocJson;
184
+ this.npmPath = opts.npm;
185
+ this.mkDocsYmlPath = opts.mkdocsYml;
186
+
187
+ if (opts.python) {
188
+ this.validations.add(NAME_PYTHON);
189
+ }
190
+ if (opts.typescript) {
191
+ this.validations.add(NAME_TYPESCRIPT);
192
+ // npm validation is required for both typescript and typedoc validation
193
+ this.validations.add(NAME_NPM);
194
+ }
195
+ if (opts.typedoc) {
196
+ this.validations.add(NAME_TYPEDOC);
197
+ this.validations.add(NAME_NPM);
198
+ }
199
+ if (opts.mkdocs) {
200
+ this.validations.add(NAME_MKDOCS);
201
+ }
202
+
203
+ // this just tracks the emitted errors
204
+ this.on(DocutilsValidator.FAILURE, (err: DocutilsError) => {
205
+ this.emittedErrors.set(err.message, err);
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Runs the configured validations, then resets internal state upon completion or rejection.
211
+ */
212
+ public async validate() {
213
+ try {
214
+ this.emit(DocutilsValidator.BEGIN, [...this.validations]);
215
+
216
+ if (this.validations.has(NAME_PYTHON)) {
217
+ await this.validatePythonVersion();
218
+ await this.validatePythonDeps();
219
+ }
220
+
221
+ if (this.validations.has(NAME_MKDOCS)) {
222
+ await this.validateMkDocs();
223
+ await this.validateMkDocsConfig();
224
+ }
225
+
226
+ if (this.validations.has(NAME_NPM)) {
227
+ await this.validateNpmVersion();
228
+ }
229
+
230
+ if (this.validations.has(NAME_TYPESCRIPT)) {
231
+ await this.validateTypeScript();
232
+ await this.validateTypeScriptConfig();
233
+ }
234
+
235
+ if (this.validations.has(NAME_TYPEDOC)) {
236
+ await this.validateTypeDoc();
237
+ await this.validateTypeDocConfig();
238
+ }
239
+
240
+ this.emit(DocutilsValidator.END, this.emittedErrors.size);
241
+ } finally {
242
+ this.reset();
243
+ }
244
+ }
245
+
246
+ /**
247
+ * If a thing like `err` has not already been emitted, emit
248
+ * {@linkcode DocutilsValidator.FAILURE}.
249
+ * @param err A validation error
250
+ * @returns
251
+ */
252
+ protected fail(err: DocutilsError | string) {
253
+ const dErr = _.isString(err) ? new DocutilsError(err) : err;
254
+ if (!this.emittedErrors.has(dErr.message)) {
255
+ this.emit(DocutilsValidator.FAILURE, dErr);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Resolves with a the parent directory of `package.json`, if we can find it.
261
+ */
262
+ protected async findPkgDir(): Promise<string | undefined> {
263
+ return (
264
+ this.pkgDir ??
265
+ (this.pkgDir = this.packageJsonPath
266
+ ? path.dirname(this.packageJsonPath)
267
+ : await findPkgDir(this.cwd))
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Emits a {@linkcode DocutilsValidator.SUCCESS} event
273
+ * @param message Success message
274
+ */
275
+ protected ok(message: string) {
276
+ this.emit(DocutilsValidator.SUCCESS, message);
277
+ }
278
+
279
+ /**
280
+ * Parses a `requirements.txt` file and returns an array of packages
281
+ *
282
+ * Caches the result.
283
+ * @returns List of package data w/ name and version
284
+ */
285
+ protected async parseRequirementsTxt(): Promise<PipPackage[]> {
286
+ if (this.requirementsTxt) {
287
+ return this.requirementsTxt;
288
+ }
289
+
290
+ let requiredPackages: PipPackage[] = [];
291
+
292
+ try {
293
+ let requirementsTxt = await fs.readFile(REQUIREMENTS_TXT_PATH, 'utf8');
294
+ requirementsTxt = requirementsTxt.trim();
295
+ log.debug('Raw %s: %s', NAME_REQUIREMENTS_TXT, requirementsTxt);
296
+ for (const line of requirementsTxt.split(/\r?\n/)) {
297
+ const [name, version] = line.trim().split('==');
298
+ requiredPackages.push({name, version});
299
+ }
300
+ log.debug('Parsed %s: %O', NAME_REQUIREMENTS_TXT, requiredPackages);
301
+ } catch {
302
+ throw new DocutilsError(`Could not find ${REQUIREMENTS_TXT_PATH}. This is a bug`);
303
+ }
304
+
305
+ return (this.requirementsTxt = requiredPackages);
306
+ }
307
+
308
+ /**
309
+ * Resets the cache of emitted errors
310
+ */
311
+ protected reset() {
312
+ this.emittedErrors.clear();
313
+ }
314
+
315
+ protected async validateMkDocs() {
316
+ let mkDocsPath: string | undefined;
317
+ try {
318
+ mkDocsPath = await whichMkDocs();
319
+ } catch {
320
+ // _pretty sure_ the exception code is always ENOENT
321
+ return this.fail(
322
+ `Could not find ${NAME_MKDOCS} executable in PATH. If it is installed, check your PATH environment variable.`
323
+ );
324
+ }
325
+
326
+ let rawMkDocsVersion: string | undefined;
327
+ try {
328
+ ({stdout: rawMkDocsVersion} = await exec(mkDocsPath, ['--version']));
329
+ } catch (err) {
330
+ return this.fail(`${mkDocsPath} --version failed: ${err}`);
331
+ }
332
+ const match = rawMkDocsVersion.match(MKDOCS_VERSION_REGEX);
333
+ if (match) {
334
+ const version = match[1];
335
+ const reqs = await this.parseRequirementsTxt();
336
+ const mkDocsPipPkg = _.find(reqs, {name: NAME_MKDOCS});
337
+ if (!mkDocsPipPkg) {
338
+ throw new DocutilsError(
339
+ `No ${NAME_MKDOCS} package in ${REQUIREMENTS_TXT_PATH}. This is a bug.`
340
+ );
341
+ }
342
+ const {version: mkDocsReqdVersion} = mkDocsPipPkg;
343
+ if (version !== mkDocsReqdVersion) {
344
+ return this.fail(
345
+ `${NAME_MKDOCS} at ${mkDocsPath} is v${version}, but ${REQUIREMENTS_TXT_PATH} requires v${mkDocsReqdVersion}`
346
+ );
347
+ }
348
+ } else {
349
+ throw new DocutilsError(
350
+ `Could not parse version from "${mkDocsPath} --version". This is a bug. Output was ${rawMkDocsVersion}`
351
+ );
352
+ }
353
+
354
+ this.ok('MkDocs install OK');
355
+ }
356
+
357
+ /**
358
+ * Validates (sort of) an `mkdocs.yml` config file.
359
+ *
360
+ * It checks if the file exists, if it can be parsed as YAML, and if it has a `site_name` property.
361
+ */
362
+ protected async validateMkDocsConfig(mkDocsYmlPath?: string) {
363
+ mkDocsYmlPath = mkDocsYmlPath ?? this.mkDocsYmlPath ?? (await findMkDocsYml(this.cwd));
364
+ if (!mkDocsYmlPath) {
365
+ return this.fail(
366
+ `Could not find ${NAME_MKDOCS_YML} from ${this.cwd}. Run "${NAME_BIN} init" to create it`
367
+ );
368
+ }
369
+ let mkDocsYml: MkDocsYml;
370
+ try {
371
+ mkDocsYml = await readMkDocsYml(mkDocsYmlPath);
372
+ } catch (e) {
373
+ const err = e as NodeJS.ErrnoException;
374
+ if (err.code === NAME_ERR_ENOENT) {
375
+ return this.fail(
376
+ `Could not find ${NAME_MKDOCS_YML} at ${mkDocsYmlPath}. Use --mkdocs-yml to specify a different path.`
377
+ );
378
+ }
379
+ return this.fail(`Could not parse ${mkDocsYmlPath}: ${err}`);
380
+ }
381
+
382
+ if (!mkDocsYml.site_name) {
383
+ return this.fail(`Could not find required property "site_name" in ${mkDocsYmlPath}`);
384
+ }
385
+
386
+ this.ok(`MkDocs config at ${mkDocsYmlPath} OK`);
387
+ }
388
+
389
+ /**
390
+ * Validates that the version of `npm` matches what's described in this package's `engines` field.
391
+ *
392
+ * This is required because other validators need `npm exec` to work, which is only available in npm 7+.
393
+ */
394
+ protected async validateNpmVersion() {
395
+ const npmEngineRange = DOCUTILS_PKG.engines?.npm;
396
+ if (!npmEngineRange) {
397
+ throw new DocutilsError('Could not find property engines.npm in package.json. This is a bug');
398
+ }
399
+ try {
400
+ const npmPath = this.npmPath ?? (await whichNpm());
401
+ if (!npmPath) {
402
+ throw new DocutilsError(
403
+ `Could not find ${NAME_NPM} in PATH. That seems weird, doesn't it?`
404
+ );
405
+ }
406
+ const {stdout: npmVersion} = await exec(npmPath, ['-v']);
407
+ if (!satisfies(npmVersion.trim(), npmEngineRange)) {
408
+ this.fail(`${NAME_NPM} is version ${npmVersion}, but ${npmEngineRange} is required`);
409
+ return;
410
+ }
411
+ } catch {
412
+ return this.fail(`Could not find ${this.npmPath} in PATH. Is it installed?`);
413
+ }
414
+ this.ok(`${NAME_NPM} version OK`);
415
+ }
416
+
417
+ /**
418
+ * Asserts that the dependencies as listed in `requirements.txt` are installed.
419
+ *
420
+ * @privateRemarks This lists all installed packages with `pip` and then compares them to the
421
+ * contents of our `requirements.txt`. Versions _must_ match exactly.
422
+ */
423
+ protected async validatePythonDeps() {
424
+ let pipListOutput: string;
425
+ const pythonPath = this.pythonPath ?? (await whichPython());
426
+ if (!pythonPath) {
427
+ return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
428
+ }
429
+ try {
430
+ ({stdout: pipListOutput} = await exec(pythonPath, [
431
+ '-m',
432
+ NAME_PIP,
433
+ 'list',
434
+ '--format',
435
+ 'json',
436
+ ]));
437
+ } catch {
438
+ return this.fail(`Could not find ${NAME_PIP} in PATH. Is it installed?`);
439
+ }
440
+
441
+ let installedPkgs: PipPackage[];
442
+ try {
443
+ installedPkgs = JSON.parse(pipListOutput) as PipPackage[];
444
+ } catch {
445
+ throw new DocutilsError(
446
+ `Could not parse output of "${NAME_PIP} list" as JSON: ${pipListOutput}`
447
+ );
448
+ }
449
+
450
+ const pkgsByName = _.mapValues(_.keyBy(installedPkgs, 'name'), 'version');
451
+ log.debug('Installed Python packages: %O', pkgsByName);
452
+
453
+ const requiredPackages = await this.parseRequirementsTxt();
454
+ const missingPackages: PipPackage[] = [];
455
+ const invalidVersionPackages: [expected: PipPackage, actual: PipPackage][] = [];
456
+ for (const reqdPkg of requiredPackages) {
457
+ const version = pkgsByName[reqdPkg.name];
458
+ if (!version) {
459
+ missingPackages.push(reqdPkg);
460
+ }
461
+ if (version !== reqdPkg.version) {
462
+ invalidVersionPackages.push([reqdPkg, {name: reqdPkg.name, version}]);
463
+ }
464
+ }
465
+
466
+ const msgParts = [];
467
+ if (missingPackages.length) {
468
+ msgParts.push(
469
+ `The following required ${pluralize(
470
+ 'package',
471
+ missingPackages.length
472
+ )} could not be found:\n${missingPackages
473
+ .map((p) => chalk`- {yellow ${p.name}} @ {yellow ${p.version}}`)
474
+ .join('\n')}`
475
+ );
476
+ }
477
+ if (invalidVersionPackages.length) {
478
+ msgParts.push(
479
+ `The following required ${pluralize(
480
+ 'package',
481
+ invalidVersionPackages.length
482
+ )} are installed, but at the wrong version:\n${invalidVersionPackages
483
+ .map(
484
+ ([expected, actual]) =>
485
+ chalk`- {yellow ${expected.name}} @ {yellow ${expected.version}} (found {red ${actual.version}})`
486
+ )
487
+ .join('\n')}`
488
+ );
489
+ }
490
+ if (msgParts.length) {
491
+ return this.fail(`Required Python dependency validation failed:\n\n${msgParts.join('\n\n')}`);
492
+ }
493
+
494
+ this.ok('Python dependencies OK');
495
+ }
496
+
497
+ /**
498
+ * Asserts that the Python version is 3.x
499
+ */
500
+ protected async validatePythonVersion() {
501
+ const pythonPath = this.pythonPath ?? (await whichPython());
502
+ if (!pythonPath) {
503
+ return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
504
+ }
505
+
506
+ try {
507
+ const {stdout} = await exec(pythonPath, ['--version']);
508
+ if (!stdout.includes(PYTHON_VER_STR)) {
509
+ return this.fail(
510
+ `Could not find Python 3.x in PATH; found ${stdout}. Please use --python-path`
511
+ );
512
+ }
513
+ } catch {
514
+ return this.fail(`Could not find Python 3.x in PATH.`);
515
+ }
516
+ this.ok('Python version OK');
517
+ }
518
+
519
+ /**
520
+ * Asserts TypeDoc is installed, runnable, the correct version, and that the config file is readable
521
+ * and constaints required options
522
+ */
523
+ protected async validateTypeDoc() {
524
+ const pkgDir = await this.findPkgDir();
525
+ let rawTypeDocVersion: string;
526
+ let typeDocVersion: string;
527
+ try {
528
+ ({stdout: rawTypeDocVersion} = await exec('npm', ['exec', NAME_TYPEDOC, '--', '--version'], {
529
+ cwd: pkgDir,
530
+ }));
531
+ } catch {
532
+ return this.fail(`Could not find ${NAME_TYPEDOC} executable from ${pkgDir}`);
533
+ }
534
+
535
+ if (rawTypeDocVersion) {
536
+ let match = rawTypeDocVersion.match(TYPEDOC_VERSION_REGEX);
537
+ if (match) {
538
+ typeDocVersion = match[1];
539
+ } else {
540
+ throw new DocutilsError(
541
+ `Could not parse TypeDoc version from "typedoc --version"; output was:\n ${rawTypeDocVersion}`
542
+ );
543
+ }
544
+
545
+ const reqdTypeDocVersion = DOCUTILS_PKG.dependencies!.typedoc!;
546
+ if (!satisfies(typeDocVersion, reqdTypeDocVersion)) {
547
+ return this.fail(
548
+ `Found TypeDoc version ${typeDocVersion}, but ${reqdTypeDocVersion} is required`
549
+ );
550
+ }
551
+ this.ok('TypeDoc install OK');
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Validates the `typedoc.json` file
557
+ */
558
+ protected async validateTypeDocConfig() {
559
+ const pkgDir = await this.findPkgDir();
560
+ if (!pkgDir) {
561
+ return this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
562
+ }
563
+ const typeDocJsonPath = (this.typeDocJsonPath =
564
+ this.typeDocJsonPath ?? path.join(pkgDir, NAME_TYPEDOC_JSON));
565
+ const relTypeDocJsonPath = relative(this.cwd, typeDocJsonPath);
566
+ let typeDocJson: TypeDocJson;
567
+
568
+ // handle the case where the user passes a JS file as the typedoc config
569
+ // (which is allowed by TypeDoc)
570
+ if (typeDocJsonPath.endsWith('.js')) {
571
+ try {
572
+ typeDocJson = require(typeDocJsonPath);
573
+ } catch (err) {
574
+ throw new DocutilsError(
575
+ `TypeDoc config at ${relTypeDocJsonPath} threw an exception: ${err}`
576
+ );
577
+ }
578
+ } else {
579
+ try {
580
+ typeDocJson = readTypedocJson(typeDocJsonPath);
581
+ } catch (e) {
582
+ if (e instanceof SyntaxError) {
583
+ return this.fail(
584
+ new DocutilsError(`Unparseable ${NAME_TYPEDOC_JSON} at ${relTypeDocJsonPath}: ${e}`)
585
+ );
586
+ }
587
+ return this.fail(
588
+ new DocutilsError(
589
+ `Missing ${NAME_TYPEDOC_JSON} at ${relTypeDocJsonPath}; "${NAME_BIN} init" can help`
590
+ )
591
+ );
592
+ }
593
+ }
594
+
595
+ if (!typeDocJson.out) {
596
+ return this.fail(
597
+ new DocutilsError(
598
+ `Missing "out" property in ${relTypeDocJsonPath}; path "${DEFAULT_REL_TYPEDOC_OUT_PATH} is recommended`
599
+ )
600
+ );
601
+ }
602
+
603
+ this.ok('TypeDoc config OK');
604
+ }
605
+
606
+ /**
607
+ * Asserts that TypeScript is installed, runnable, the correct version, and a parseable `tsconfig.json` exists.
608
+ */
609
+ protected async validateTypeScript() {
610
+ const pkgDir = await this.findPkgDir();
611
+ if (!pkgDir) {
612
+ return this.fail(`Could not find package.json in ${this.cwd}`);
613
+ }
614
+ let typeScriptVersion: string;
615
+ let rawTypeScriptVersion: string;
616
+ try {
617
+ ({stdout: rawTypeScriptVersion} = await exec(NAME_NPM, ['exec', 'tsc', '--', '--version'], {
618
+ cwd: pkgDir,
619
+ }));
620
+ } catch {
621
+ return this.fail(`Could not find TypeScript compiler ("tsc") from ${pkgDir}`);
622
+ }
623
+
624
+ let match = rawTypeScriptVersion.match(TYPESCRIPT_VERSION_REGEX);
625
+ if (match) {
626
+ typeScriptVersion = match[1];
627
+ } else {
628
+ return this.fail(
629
+ `Could not parse TypeScript version from "tsc --version"; output was:\n ${rawTypeScriptVersion}`
630
+ );
631
+ }
632
+
633
+ const reqdTypeScriptVersion = DOCUTILS_PKG.dependencies?.typescript;
634
+
635
+ if (!reqdTypeScriptVersion) {
636
+ throw new DocutilsError(
637
+ `Could not find a dep for ${NAME_TYPESCRIPT} in ${NAME_PACKAGE_JSON}. This is a bug.`
638
+ );
639
+ }
640
+
641
+ if (!satisfies(typeScriptVersion, reqdTypeScriptVersion)) {
642
+ return this.fail(
643
+ `Found TypeScript version ${typeScriptVersion}, but ${reqdTypeScriptVersion} is required`
644
+ );
645
+ }
646
+ this.ok('TypeScript install OK');
647
+ }
648
+
649
+ /**
650
+ * Validates a `tsconfig.json` file
651
+ */
652
+ protected async validateTypeScriptConfig() {
653
+ const pkgDir = await this.findPkgDir();
654
+ if (!pkgDir) {
655
+ return this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
656
+ }
657
+ const tsconfigJsonPath = (this.tsconfigJsonPath =
658
+ this.tsconfigJsonPath ?? path.join(pkgDir, NAME_TSCONFIG_JSON));
659
+ const relTsconfigJsonPath = relative(this.cwd, tsconfigJsonPath);
660
+ try {
661
+ await readJson5(tsconfigJsonPath);
662
+ } catch (e) {
663
+ if (e instanceof SyntaxError) {
664
+ return this.fail(
665
+ new DocutilsError(`Unparseable ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}: ${e}`)
666
+ );
667
+ }
668
+ return this.fail(
669
+ new DocutilsError(
670
+ `Missing ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}; "${NAME_BIN} init" can help`
671
+ )
672
+ );
673
+ }
674
+
675
+ this.ok('TypeScript config OK');
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Options for {@linkcode DocutilsValidator} constructor
681
+ */
682
+
683
+ export interface DocutilsValidatorOpts {
684
+ /**
685
+ * Current working directory
686
+ */
687
+ cwd?: string;
688
+ /**
689
+ * Path to `mkdocs.yml`
690
+ */
691
+ mkdocsYml?: string;
692
+ /**
693
+ * Path to `npm` executable
694
+ */
695
+ npm?: string;
696
+ /**
697
+ * Path to `package.json`
698
+ */
699
+ packageJson?: string;
700
+ /**
701
+ * If `true`, run Python validation
702
+ */
703
+ python?: boolean;
704
+ /**
705
+ * Path to `python` executable
706
+ */
707
+ pythonPath?: string;
708
+ /**
709
+ * Path to `tsconfig.json`
710
+ */
711
+ tsconfigJson?: string;
712
+ /**
713
+ * If `true`, run TypeDoc validation
714
+ */
715
+ typedoc?: boolean;
716
+ /**
717
+ * Path to `typedoc.json`
718
+ */
719
+ typedocJson?: string;
720
+ /**
721
+ * If `true`, run TypeScript validation
722
+ */
723
+ typescript?: boolean;
724
+ /**
725
+ * If `true`, run MkDocs validation
726
+ */
727
+ mkdocs?: boolean;
728
+ }