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/README.md +246 -16
- package/lib/generator-current.d.ts +44 -0
- package/lib/generator-current.js +398 -0
- package/lib/generator.d.ts +4 -2
- package/lib/generator.js +163 -71
- package/lib/index.js +174 -10
- package/lib/null-handling-guide.d.ts +47 -0
- package/lib/null-handling-guide.js +290 -0
- package/lib/processor.d.ts +0 -10
- package/lib/processor.js +217 -80
- package/lib/types.d.ts +10 -0
- package/lib/utils.d.ts +141 -7
- package/lib/utils.js +429 -34
- package/package.json +2 -2
- package/src/generator.ts +206 -86
- package/src/index.ts +202 -14
- package/src/null-handling-guide.ts +321 -0
- package/src/processor.ts +303 -126
- package/src/types.ts +15 -0
- package/src/utils.ts +530 -59
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
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
156
|
-
} catch (err:
|
|
157
|
-
|
|
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
|
+
*/
|