docusaurus-plugin-llms 0.2.2 → 0.3.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.
package/src/index.ts CHANGED
@@ -10,13 +10,190 @@
10
10
 
11
11
  import * as path from 'path';
12
12
  import type { LoadContext, Plugin, Props, RouteConfig } from '@docusaurus/types';
13
- import { PluginOptions, PluginContext } from './types';
13
+ import { PluginOptions, PluginContext, CustomLLMFile } from './types';
14
14
  import { collectDocFiles, generateStandardLLMFiles, generateCustomLLMFiles } from './generator';
15
+ import { setLogLevel, LogLevel, logger, getErrorMessage, isDefined, isNonEmptyString, isNonEmptyArray } from './utils';
16
+
17
+ /**
18
+ * Validates plugin options to ensure they conform to expected types and constraints
19
+ * @param options - Plugin options to validate
20
+ * @throws Error if any option is invalid
21
+ */
22
+ function validatePluginOptions(options: PluginOptions): void {
23
+ // Validate includeOrder
24
+ if (options.includeOrder !== undefined) {
25
+ if (!Array.isArray(options.includeOrder)) {
26
+ throw new Error('includeOrder must be an array');
27
+ }
28
+ if (!options.includeOrder.every(item => typeof item === 'string')) {
29
+ throw new Error('includeOrder must contain only strings');
30
+ }
31
+ }
32
+
33
+ // Validate ignoreFiles
34
+ if (options.ignoreFiles !== undefined) {
35
+ if (!Array.isArray(options.ignoreFiles)) {
36
+ throw new Error('ignoreFiles must be an array');
37
+ }
38
+ if (!options.ignoreFiles.every(item => typeof item === 'string')) {
39
+ throw new Error('ignoreFiles must contain only strings');
40
+ }
41
+ }
42
+
43
+ // Validate pathTransformation
44
+ if (isDefined(options.pathTransformation)) {
45
+ if (typeof options.pathTransformation !== 'object') {
46
+ throw new Error('pathTransformation must be an object');
47
+ }
48
+
49
+ const { ignorePaths, addPaths } = options.pathTransformation;
50
+
51
+ if (ignorePaths !== undefined) {
52
+ if (!Array.isArray(ignorePaths)) {
53
+ throw new Error('pathTransformation.ignorePaths must be an array');
54
+ }
55
+ if (!ignorePaths.every(item => typeof item === 'string')) {
56
+ throw new Error('pathTransformation.ignorePaths must contain only strings');
57
+ }
58
+ }
59
+
60
+ if (addPaths !== undefined) {
61
+ if (!Array.isArray(addPaths)) {
62
+ throw new Error('pathTransformation.addPaths must be an array');
63
+ }
64
+ if (!addPaths.every(item => typeof item === 'string')) {
65
+ throw new Error('pathTransformation.addPaths must contain only strings');
66
+ }
67
+ }
68
+ }
69
+
70
+ // Validate boolean options
71
+ const booleanOptions = [
72
+ 'generateLLMsTxt',
73
+ 'generateLLMsFullTxt',
74
+ 'includeBlog',
75
+ 'includeUnmatchedLast',
76
+ 'excludeImports',
77
+ 'removeDuplicateHeadings',
78
+ 'generateMarkdownFiles',
79
+ 'preserveDirectoryStructure'
80
+ ] as const;
81
+
82
+ for (const option of booleanOptions) {
83
+ if (options[option] !== undefined && typeof options[option] !== 'boolean') {
84
+ throw new Error(`${option} must be a boolean`);
85
+ }
86
+ }
87
+
88
+ // Validate string options
89
+ const stringOptions = [
90
+ 'docsDir',
91
+ 'title',
92
+ 'description',
93
+ 'llmsTxtFilename',
94
+ 'llmsFullTxtFilename',
95
+ 'version',
96
+ 'rootContent',
97
+ 'fullRootContent'
98
+ ] as const;
99
+
100
+ for (const option of stringOptions) {
101
+ if (options[option] !== undefined && typeof options[option] !== 'string') {
102
+ throw new Error(`${option} must be a string`);
103
+ }
104
+ }
105
+
106
+ // Validate keepFrontMatter
107
+ if (options.keepFrontMatter !== undefined) {
108
+ if (!Array.isArray(options.keepFrontMatter)) {
109
+ throw new Error('keepFrontMatter must be an array');
110
+ }
111
+ if (!options.keepFrontMatter.every(item => typeof item === 'string')) {
112
+ throw new Error('keepFrontMatter must contain only strings');
113
+ }
114
+ }
115
+
116
+ // Validate logLevel
117
+ if (options.logLevel !== undefined) {
118
+ const validLogLevels = ['quiet', 'normal', 'verbose'];
119
+ if (!validLogLevels.includes(options.logLevel)) {
120
+ throw new Error(`logLevel must be one of: ${validLogLevels.join(', ')}`);
121
+ }
122
+ }
123
+
124
+ // Validate customLLMFiles
125
+ if (options.customLLMFiles !== undefined) {
126
+ if (!Array.isArray(options.customLLMFiles)) {
127
+ throw new Error('customLLMFiles must be an array');
128
+ }
129
+
130
+ options.customLLMFiles.forEach((file, index) => {
131
+ if (!isDefined(file) || typeof file !== 'object') {
132
+ throw new Error(`customLLMFiles[${index}] must be an object`);
133
+ }
134
+
135
+ // Required fields
136
+ if (!isNonEmptyString(file.filename)) {
137
+ throw new Error(`customLLMFiles[${index}].filename must be a non-empty string`);
138
+ }
139
+
140
+ if (!isNonEmptyArray(file.includePatterns)) {
141
+ throw new Error(`customLLMFiles[${index}].includePatterns must be a non-empty array`);
142
+ }
143
+ if (!file.includePatterns.every(item => typeof item === 'string')) {
144
+ throw new Error(`customLLMFiles[${index}].includePatterns must contain only strings`);
145
+ }
146
+
147
+ if (typeof file.fullContent !== 'boolean') {
148
+ throw new Error(`customLLMFiles[${index}].fullContent must be a boolean`);
149
+ }
150
+
151
+ // Optional fields
152
+ if (isDefined(file.title) && !isNonEmptyString(file.title)) {
153
+ throw new Error(`customLLMFiles[${index}].title must be a non-empty string`);
154
+ }
155
+
156
+ if (isDefined(file.description) && !isNonEmptyString(file.description)) {
157
+ throw new Error(`customLLMFiles[${index}].description must be a non-empty string`);
158
+ }
159
+
160
+ if (file.ignorePatterns !== undefined) {
161
+ if (!Array.isArray(file.ignorePatterns)) {
162
+ throw new Error(`customLLMFiles[${index}].ignorePatterns must be an array`);
163
+ }
164
+ if (!file.ignorePatterns.every(item => typeof item === 'string')) {
165
+ throw new Error(`customLLMFiles[${index}].ignorePatterns must contain only strings`);
166
+ }
167
+ }
168
+
169
+ if (file.orderPatterns !== undefined) {
170
+ if (!Array.isArray(file.orderPatterns)) {
171
+ throw new Error(`customLLMFiles[${index}].orderPatterns must be an array`);
172
+ }
173
+ if (!file.orderPatterns.every(item => typeof item === 'string')) {
174
+ throw new Error(`customLLMFiles[${index}].orderPatterns must contain only strings`);
175
+ }
176
+ }
177
+
178
+ if (file.includeUnmatchedLast !== undefined && typeof file.includeUnmatchedLast !== 'boolean') {
179
+ throw new Error(`customLLMFiles[${index}].includeUnmatchedLast must be a boolean`);
180
+ }
181
+
182
+ if (isDefined(file.version) && !isNonEmptyString(file.version)) {
183
+ throw new Error(`customLLMFiles[${index}].version must be a non-empty string`);
184
+ }
185
+
186
+ if (isDefined(file.rootContent) && !isNonEmptyString(file.rootContent)) {
187
+ throw new Error(`customLLMFiles[${index}].rootContent must be a non-empty string`);
188
+ }
189
+ });
190
+ }
191
+ }
15
192
 
16
193
  /**
17
194
  * A Docusaurus plugin to generate LLM-friendly documentation following
18
195
  * the llmstxt.org standard
19
- *
196
+ *
20
197
  * @param context - Docusaurus context
21
198
  * @param options - Plugin options
22
199
  * @returns Plugin object
@@ -25,6 +202,8 @@ export default function docusaurusPluginLLMs(
25
202
  context: LoadContext,
26
203
  options: PluginOptions = {}
27
204
  ): Plugin<void> {
205
+ // Validate options before processing
206
+ validatePluginOptions(options);
28
207
  // Set default options
29
208
  const {
30
209
  generateLLMsTxt = true,
@@ -46,20 +225,29 @@ export default function docusaurusPluginLLMs(
46
225
  keepFrontMatter = [],
47
226
  rootContent,
48
227
  fullRootContent,
228
+ logLevel = 'normal',
49
229
  } = options;
50
230
 
231
+ // Initialize logging level
232
+ const logLevelMap = {
233
+ quiet: LogLevel.QUIET,
234
+ normal: LogLevel.NORMAL,
235
+ verbose: LogLevel.VERBOSE,
236
+ };
237
+ setLogLevel(logLevelMap[logLevel] || LogLevel.NORMAL);
238
+
51
239
  const {
52
240
  siteDir,
53
241
  siteConfig,
54
242
  outDir,
55
243
  } = context;
56
244
 
57
- // Build the site URL with proper trailing slash
58
- const siteUrl = siteConfig.url + (
59
- siteConfig.baseUrl.endsWith('/')
60
- ? siteConfig.baseUrl.slice(0, -1)
61
- : siteConfig.baseUrl || ''
62
- );
245
+ // Normalize baseUrl: remove trailing slash unless it's root '/'
246
+ let normalizedBaseUrl = siteConfig.baseUrl || '/';
247
+ if (normalizedBaseUrl !== '/' && normalizedBaseUrl.endsWith('/')) {
248
+ normalizedBaseUrl = normalizedBaseUrl.slice(0, -1);
249
+ }
250
+ const siteUrl = siteConfig.url + normalizedBaseUrl;
63
251
 
64
252
  // Create a plugin context object with processed options
65
253
  const pluginContext: PluginContext = {
@@ -99,7 +287,7 @@ export default function docusaurusPluginLLMs(
99
287
  * Generates LLM-friendly documentation files after the build is complete
100
288
  */
101
289
  async postBuild(props?: Props & { content: unknown }): Promise<void> {
102
- console.log('Generating LLM-friendly documentation...');
290
+ logger.info('Generating LLM-friendly documentation...');
103
291
 
104
292
  try {
105
293
  let enhancedContext = pluginContext;
@@ -140,8 +328,8 @@ export default function docusaurusPluginLLMs(
140
328
  const allDocFiles = await collectDocFiles(enhancedContext);
141
329
 
142
330
  // Skip further processing if no documents were found
143
- if (allDocFiles.length === 0) {
144
- console.warn('No documents found to process.');
331
+ if (!isNonEmptyArray(allDocFiles)) {
332
+ logger.warn('No documents found to process. Skipping.');
145
333
  return;
146
334
  }
147
335
 
@@ -152,9 +340,9 @@ export default function docusaurusPluginLLMs(
152
340
  await generateCustomLLMFiles(enhancedContext, allDocFiles);
153
341
 
154
342
  // Output overall statistics
155
- console.log(`Stats: ${allDocFiles.length} total available documents processed`);
156
- } catch (err: any) {
157
- console.error('Error generating LLM documentation:', err);
343
+ logger.info(`Stats: ${allDocFiles.length} total available documents processed`);
344
+ } catch (err: unknown) {
345
+ logger.error(`Error generating LLM documentation: ${getErrorMessage(err)}`);
158
346
  }
159
347
  },
160
348
  };
@@ -0,0 +1,321 @@
1
+ /**
2
+ * NULL AND UNDEFINED HANDLING GUIDE
3
+ *
4
+ * This file documents the standardized patterns for null/undefined handling
5
+ * across the docusaurus-plugin-llms codebase.
6
+ *
7
+ * PRINCIPLES:
8
+ * 1. Be explicit about null/undefined checks - avoid loose truthy checks
9
+ * 2. Use optional chaining for safe property access on optional values
10
+ * 3. Validate required parameters early with explicit checks
11
+ * 4. Distinguish between "missing" (undefined/null) and "falsy" (0, '', false)
12
+ *
13
+ * PATTERNS:
14
+ */
15
+
16
+ // ============================================================================
17
+ // PATTERN 1: Checking for null OR undefined (most common)
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Use when you need to check if a value exists (not null and not undefined).
22
+ * This is the most common check for optional parameters or properties.
23
+ */
24
+ function checkIfDefined_GOOD(value: string | undefined | null): void {
25
+ if (value !== undefined && value !== null) {
26
+ // Value is defined and not null
27
+ console.log(value.toUpperCase());
28
+ }
29
+ }
30
+
31
+ /**
32
+ * AVOID: Loose truthy check - this catches 0, '', false, NaN
33
+ */
34
+ function checkIfDefined_BAD(value: string | undefined | null): void {
35
+ if (value) {
36
+ // PROBLEM: This rejects empty strings, 0, false, etc.
37
+ console.log(value.toUpperCase());
38
+ }
39
+ }
40
+
41
+ // ============================================================================
42
+ // PATTERN 2: Optional chaining for safe property access
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Use optional chaining when accessing properties on values that might be
47
+ * null or undefined. This avoids TypeError exceptions.
48
+ */
49
+ function safePropertyAccess_GOOD(obj: { prop?: string } | undefined): void {
50
+ const value = obj?.prop;
51
+ if (value !== undefined && value !== null) {
52
+ console.log(value);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * AVOID: Manual null checks before property access (verbose)
58
+ */
59
+ function safePropertyAccess_BAD(obj: { prop?: string } | undefined): void {
60
+ if (obj && obj.prop) {
61
+ // Verbose and misses the case where prop is explicitly false/0
62
+ console.log(obj.prop);
63
+ }
64
+ }
65
+
66
+ // ============================================================================
67
+ // PATTERN 3: Type-specific validation with explicit null checks
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Use when validating optional parameters that must be a specific type if provided.
72
+ * Check both that the value is defined AND that it has the correct type.
73
+ */
74
+ function validateOptionalString_GOOD(value: unknown): void {
75
+ if (value !== undefined && value !== null) {
76
+ if (typeof value !== 'string') {
77
+ throw new Error('Value must be a string if provided');
78
+ }
79
+ // Now we know value is a string
80
+ console.log(value.trim());
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Can be combined into a single check when appropriate
86
+ */
87
+ function validateOptionalString_GOOD_COMPACT(value: unknown): void {
88
+ if (value !== undefined && typeof value !== 'string') {
89
+ throw new Error('Value must be a string if provided');
90
+ }
91
+ }
92
+
93
+ // ============================================================================
94
+ // PATTERN 4: Non-empty string validation
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Use when you need a string that is not only defined but also not empty.
99
+ * This is common for required fields that have been parsed from user input.
100
+ */
101
+ function validateNonEmptyString_GOOD(value: string | undefined | null): void {
102
+ if (typeof value === 'string' && value.trim() !== '') {
103
+ // Value is a non-empty string
104
+ console.log(value);
105
+ } else {
106
+ throw new Error('Value must be a non-empty string');
107
+ }
108
+ }
109
+
110
+ /**
111
+ * AVOID: Loose check that doesn't validate the type
112
+ */
113
+ function validateNonEmptyString_BAD(value: string | undefined | null): void {
114
+ if (value && value.trim()) {
115
+ // PROBLEM: Assumes value has .trim() method
116
+ console.log(value);
117
+ }
118
+ }
119
+
120
+ // ============================================================================
121
+ // PATTERN 5: Array validation
122
+ // ============================================================================
123
+
124
+ /**
125
+ * Use explicit checks for arrays, checking both existence and array type.
126
+ */
127
+ function validateOptionalArray_GOOD(value: unknown): void {
128
+ if (value !== undefined && value !== null) {
129
+ if (!Array.isArray(value)) {
130
+ throw new Error('Value must be an array if provided');
131
+ }
132
+ // Now we know value is an array
133
+ console.log(value.length);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check for non-empty arrays using optional chaining and length check
139
+ */
140
+ function checkNonEmptyArray_GOOD(value: string[] | undefined): void {
141
+ if (value?.length) {
142
+ // Array exists and has at least one element
143
+ console.log(value[0]);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * AVOID: Loose truthy check on arrays - empty arrays are truthy!
149
+ */
150
+ function checkNonEmptyArray_BAD(value: string[] | undefined): void {
151
+ if (value && value.length > 0) {
152
+ // Works but is verbose compared to optional chaining
153
+ console.log(value[0]);
154
+ }
155
+ }
156
+
157
+ // ============================================================================
158
+ // PATTERN 6: Object validation
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Use explicit null check for objects since typeof null === 'object'
163
+ */
164
+ function validateOptionalObject_GOOD(value: unknown): void {
165
+ if (value !== undefined && value !== null) {
166
+ if (typeof value !== 'object') {
167
+ throw new Error('Value must be an object if provided');
168
+ }
169
+ // Now we know value is an object (not null)
170
+ }
171
+ }
172
+
173
+ /**
174
+ * CRITICAL: Always check for null when using typeof to validate objects
175
+ */
176
+ function validateOptionalObject_BAD(value: unknown): void {
177
+ if (value !== undefined) {
178
+ if (typeof value !== 'object') {
179
+ // PROBLEM: typeof null === 'object', so null passes this check!
180
+ throw new Error('Value must be an object if provided');
181
+ }
182
+ }
183
+ }
184
+
185
+ // ============================================================================
186
+ // PATTERN 7: Boolean validation
187
+ // ============================================================================
188
+
189
+ /**
190
+ * Use explicit type check for booleans, not truthiness.
191
+ * This distinguishes between missing (undefined) and false.
192
+ */
193
+ function validateOptionalBoolean_GOOD(value: unknown): void {
194
+ if (value !== undefined && value !== null && typeof value !== 'boolean') {
195
+ throw new Error('Value must be a boolean if provided');
196
+ }
197
+ // Can safely use value as boolean | undefined | null
198
+ }
199
+
200
+ /**
201
+ * When you need to treat undefined as a specific boolean value
202
+ */
203
+ function withDefaultBoolean_GOOD(value: boolean | undefined): boolean {
204
+ // Explicit: undefined becomes true, false stays false
205
+ return value !== false;
206
+ }
207
+
208
+ /**
209
+ * AVOID: Coercing to boolean implicitly
210
+ */
211
+ function withDefaultBoolean_BAD(value: boolean | undefined): boolean {
212
+ // PROBLEM: value = 0 or '' would also become false
213
+ return !!value;
214
+ }
215
+
216
+ // ============================================================================
217
+ // PATTERN 8: Default values with nullish coalescing
218
+ // ============================================================================
219
+
220
+ /**
221
+ * Use nullish coalescing (??) for default values.
222
+ * This only replaces null/undefined, not other falsy values.
223
+ */
224
+ function withDefault_GOOD(value: string | undefined | null): string {
225
+ return value ?? 'default';
226
+ }
227
+
228
+ /**
229
+ * AVOID: Logical OR for defaults - it replaces ALL falsy values
230
+ */
231
+ function withDefault_BAD(value: string | undefined | null): string {
232
+ // PROBLEM: value = '' or 0 would also use the default
233
+ return value || 'default';
234
+ }
235
+
236
+ // ============================================================================
237
+ // PATTERN 9: Early validation for required parameters
238
+ // ============================================================================
239
+
240
+ /**
241
+ * Validate required parameters at function entry with explicit checks.
242
+ */
243
+ function requireParameter_GOOD(value: string | undefined | null): void {
244
+ if (value === undefined || value === null) {
245
+ throw new Error('Parameter is required');
246
+ }
247
+ // Now TypeScript knows value is string
248
+ console.log(value.toUpperCase());
249
+ }
250
+
251
+ /**
252
+ * AVOID: Loose validation that doesn't distinguish null/undefined from falsy
253
+ */
254
+ function requireParameter_BAD(value: string | undefined | null): void {
255
+ if (!value) {
256
+ // PROBLEM: Also throws for empty string, 0, false
257
+ throw new Error('Parameter is required');
258
+ }
259
+ console.log(value.toUpperCase());
260
+ }
261
+
262
+ // ============================================================================
263
+ // PATTERN 10: Converting truthy checks to explicit checks
264
+ // ============================================================================
265
+
266
+ /**
267
+ * When checking if a value should be used (but preserving falsy values)
268
+ */
269
+ function explicitCheck_GOOD(value: string | number | boolean | undefined | null): void {
270
+ if (value !== undefined && value !== null) {
271
+ // Now can use value = 0, '', false safely
272
+ console.log(value);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * AVOID: Truthy check when falsy values are valid
278
+ */
279
+ function explicitCheck_BAD(value: string | number | boolean | undefined | null): void {
280
+ if (value) {
281
+ // PROBLEM: Rejects value = 0, '', false
282
+ console.log(value);
283
+ }
284
+ }
285
+
286
+ // ============================================================================
287
+ // SUMMARY
288
+ // ============================================================================
289
+
290
+ /**
291
+ * QUICK REFERENCE:
292
+ *
293
+ * 1. Optional parameter check:
294
+ * if (value !== undefined && value !== null) { ... }
295
+ *
296
+ * 2. Safe property access:
297
+ * const prop = obj?.property
298
+ *
299
+ * 3. Type validation with null check:
300
+ * if (typeof value !== 'object' || value === null) { throw ... }
301
+ *
302
+ * 4. Non-empty string:
303
+ * if (typeof value === 'string' && value.trim() !== '') { ... }
304
+ *
305
+ * 5. Non-empty array:
306
+ * if (value?.length) { ... }
307
+ *
308
+ * 6. Default values:
309
+ * const result = value ?? defaultValue
310
+ *
311
+ * 7. Boolean with default:
312
+ * const enabled = value !== false // undefined and null become true
313
+ *
314
+ * 8. Required parameter:
315
+ * if (value === undefined || value === null) { throw ... }
316
+ *
317
+ * AVOID:
318
+ * - if (value) { ... } // Too loose, catches falsy values
319
+ * - value || defaultValue // Replaces ALL falsy values, not just null/undefined
320
+ * - !value // Too loose for validation
321
+ */