spec-up-t-healthcheck 1.0.0 → 1.1.1

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.
@@ -0,0 +1,356 @@
1
+ /**
2
+ * @fileoverview Spec Directory and Files health check module
3
+ *
4
+ * This module validates the directory structure and required files specified
5
+ * in specs.json. It ensures that the spec_directory and spec_terms_directory
6
+ * exist, and validates the presence of required markdown files.
7
+ *
8
+ * @author spec-up-t-healthcheck
9
+ */
10
+
11
+ import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
12
+
13
+ /**
14
+ * Simple cross-platform path joining that works in both Node.js and browser.
15
+ * Normalizes paths by:
16
+ * - Removing leading './' from paths
17
+ * - Ensuring single '/' separators between segments
18
+ * - Handling empty segments
19
+ *
20
+ * @param {...string} segments - Path segments to join
21
+ * @returns {string} Joined path
22
+ * @private
23
+ */
24
+ function joinPath(...segments) {
25
+ return segments
26
+ .filter(segment => segment && segment !== '') // Remove empty segments
27
+ .map(segment => segment.replace(/^\.\//, '')) // Remove leading './'
28
+ .map(segment => segment.replace(/\/$/, '')) // Remove trailing '/'
29
+ .join('/')
30
+ .replace(/\/+/g, '/'); // Replace multiple '/' with single '/'
31
+ }
32
+
33
+ /**
34
+ * The identifier for this health check, used in reports and registries.
35
+ * @type {string}
36
+ */
37
+ export const CHECK_ID = 'spec-directory-and-files';
38
+
39
+ /**
40
+ * Human-readable name for this health check.
41
+ * @type {string}
42
+ */
43
+ export const CHECK_NAME = 'Specification Directory and Files';
44
+
45
+ /**
46
+ * Description of what this health check validates.
47
+ * @type {string}
48
+ */
49
+ export const CHECK_DESCRIPTION = 'Validates spec directories and required markdown files';
50
+
51
+ /**
52
+ * Required markdown files that must exist in spec_directory.
53
+ * @type {readonly string[]}
54
+ */
55
+ const REQUIRED_SPEC_FILES = Object.freeze(['terms-and-definitions-intro.md']);
56
+
57
+ /**
58
+ * Recommended markdown files that should exist in spec_directory.
59
+ * @type {readonly string[]}
60
+ */
61
+ const RECOMMENDED_SPEC_FILES = Object.freeze(['spec-head.md', 'spec-body.md']);
62
+
63
+ /**
64
+ * Validates the existence of spec directories and required files specified in specs.json.
65
+ *
66
+ * This health check performs the following validations:
67
+ * - Checks if specs.json exists and can be parsed
68
+ * - Validates that spec_directory exists (triggers error if missing)
69
+ * - Validates that spec_terms_directory exists (triggers error if missing)
70
+ * - Checks for mandatory terms-and-definitions-intro.md file (triggers error if missing)
71
+ * - Checks for recommended spec-head.md and spec-body.md files (triggers warning if missing)
72
+ * - Checks if spec_terms_directory contains any markdown files (triggers warning if empty)
73
+ *
74
+ * @param {import('../providers.js').Provider} provider - The provider instance for file operations
75
+ * @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} The health check result with validation details
76
+ *
77
+ * @example
78
+ * ```javascript
79
+ * const provider = createLocalProvider('/path/to/repo');
80
+ * const result = await checkSpecDirectoryAndFiles(provider);
81
+ * console.log(result.status); // 'pass', 'fail', or 'warn'
82
+ * ```
83
+ */
84
+ export async function checkSpecDirectoryAndFiles(provider) {
85
+ try {
86
+ // First, check if specs.json exists
87
+ const specsJsonExists = await provider.fileExists('specs.json');
88
+ if (!specsJsonExists) {
89
+ return createHealthCheckResult(
90
+ CHECK_NAME,
91
+ 'fail',
92
+ 'specs.json not found - cannot validate spec directories',
93
+ {
94
+ errors: ['specs.json file is required to determine spec directory locations']
95
+ }
96
+ );
97
+ }
98
+
99
+ // Read and parse specs.json
100
+ const specsJsonContent = await provider.readFile('specs.json');
101
+ let specsData;
102
+
103
+ try {
104
+ specsData = JSON.parse(specsJsonContent);
105
+ } catch (parseError) {
106
+ return createHealthCheckResult(
107
+ CHECK_NAME,
108
+ 'fail',
109
+ 'specs.json contains invalid JSON',
110
+ {
111
+ errors: [`Failed to parse specs.json: ${parseError.message}`]
112
+ }
113
+ );
114
+ }
115
+
116
+ // Validate basic structure
117
+ if (!specsData.specs || !Array.isArray(specsData.specs) || specsData.specs.length === 0) {
118
+ return createHealthCheckResult(
119
+ CHECK_NAME,
120
+ 'fail',
121
+ 'specs.json has invalid structure',
122
+ {
123
+ errors: ['specs.json must contain a "specs" array with at least one entry']
124
+ }
125
+ );
126
+ }
127
+
128
+ const spec = specsData.specs[0];
129
+ const validationResults = {
130
+ errors: [],
131
+ warnings: [],
132
+ success: []
133
+ };
134
+
135
+ // Extract directory paths
136
+ const specDirectory = spec.spec_directory;
137
+ const specTermsDirectory = spec.spec_terms_directory;
138
+
139
+ // Resolve spec_terms_directory path
140
+ // If it's a relative path (doesn't start with ./ or /), join it with spec_directory
141
+ let fullSpecTermsDirectory = specTermsDirectory;
142
+ if (specDirectory && specTermsDirectory) {
143
+ if (!specTermsDirectory.startsWith('./') && !specTermsDirectory.startsWith('/')) {
144
+ // Relative path - join with spec_directory using our cross-platform joinPath
145
+ fullSpecTermsDirectory = joinPath(specDirectory, specTermsDirectory);
146
+ }
147
+ }
148
+
149
+ // Validate spec_directory exists
150
+ await validateSpecDirectory(provider, specDirectory, validationResults);
151
+
152
+ // Validate spec_terms_directory exists
153
+ await validateSpecTermsDirectory(provider, fullSpecTermsDirectory, specTermsDirectory, validationResults);
154
+
155
+ // Validate required files in spec_directory
156
+ if (specDirectory) {
157
+ await validateRequiredFiles(provider, specDirectory, validationResults);
158
+ await validateRecommendedFiles(provider, specDirectory, validationResults);
159
+ }
160
+
161
+ // Validate spec_terms_directory contains markdown files
162
+ if (fullSpecTermsDirectory) {
163
+ await validateTermsDirectoryFiles(provider, fullSpecTermsDirectory, specTermsDirectory, validationResults);
164
+ }
165
+
166
+ // Determine overall status
167
+ let status = 'pass';
168
+ let message = 'All spec directories and required files are present';
169
+
170
+ if (validationResults.errors.length > 0) {
171
+ status = 'fail';
172
+ message = `Found ${validationResults.errors.length} critical issue(s) with spec directories or files`;
173
+ } else if (validationResults.warnings.length > 0) {
174
+ status = 'warn';
175
+ message = `Spec directories are valid but ${validationResults.warnings.length} recommended file(s) are missing`;
176
+ }
177
+
178
+ return createHealthCheckResult(
179
+ CHECK_NAME,
180
+ status,
181
+ message,
182
+ {
183
+ errors: validationResults.errors,
184
+ warnings: validationResults.warnings,
185
+ success: validationResults.success,
186
+ specDirectory,
187
+ specTermsDirectory,
188
+ fullSpecTermsDirectory
189
+ }
190
+ );
191
+
192
+ } catch (error) {
193
+ return createErrorResult(CHECK_NAME, error);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Validates that the spec_directory exists.
199
+ *
200
+ * @param {import('../providers.js').Provider} provider - The provider instance
201
+ * @param {string} specDirectory - The spec_directory path from specs.json
202
+ * @param {Object} validationResults - Results object to populate
203
+ * @private
204
+ */
205
+ async function validateSpecDirectory(provider, specDirectory, validationResults) {
206
+ // Check if spec_directory field exists
207
+ if (!specDirectory) {
208
+ validationResults.errors.push('spec_directory is not defined in specs.json');
209
+ return;
210
+ }
211
+
212
+ try {
213
+ // Check if the directory exists
214
+ const dirExists = await provider.directoryExists(specDirectory);
215
+
216
+ if (dirExists) {
217
+ validationResults.success.push(`spec_directory exists: ${specDirectory}`);
218
+ } else {
219
+ validationResults.errors.push(`spec_directory does not exist: ${specDirectory}`);
220
+ }
221
+ } catch (error) {
222
+ validationResults.errors.push(`Error checking spec_directory: ${error.message}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Validates that the spec_terms_directory exists.
228
+ *
229
+ * @param {import('../providers.js').Provider} provider - The provider instance
230
+ * @param {string} fullSpecTermsDirectory - The full path to the spec_terms_directory
231
+ * @param {string} originalSpecTermsDirectory - The original spec_terms_directory value from specs.json
232
+ * @param {Object} validationResults - Results object to populate
233
+ * @private
234
+ */
235
+ async function validateSpecTermsDirectory(provider, fullSpecTermsDirectory, originalSpecTermsDirectory, validationResults) {
236
+ // Check if spec_terms_directory field exists
237
+ if (!originalSpecTermsDirectory) {
238
+ validationResults.errors.push('spec_terms_directory is not defined in specs.json');
239
+ return;
240
+ }
241
+
242
+ try {
243
+ // Check if the directory exists
244
+ const dirExists = await provider.directoryExists(fullSpecTermsDirectory);
245
+
246
+ if (dirExists) {
247
+ validationResults.success.push(`spec_terms_directory exists: ${originalSpecTermsDirectory}`);
248
+ } else {
249
+ validationResults.errors.push(`spec_terms_directory does not exist: ${originalSpecTermsDirectory}`);
250
+ }
251
+ } catch (error) {
252
+ validationResults.errors.push(`Error checking spec_terms_directory: ${error.message}`);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Validates the presence of required markdown files in spec_directory.
258
+ *
259
+ * @param {import('../providers.js').Provider} provider - The provider instance
260
+ * @param {string} specDirectory - The spec_directory path
261
+ * @param {Object} validationResults - Results object to populate
262
+ * @private
263
+ */
264
+ async function validateRequiredFiles(provider, specDirectory, validationResults) {
265
+ for (const filename of REQUIRED_SPEC_FILES) {
266
+ const filePath = joinPath(specDirectory, filename);
267
+
268
+ try {
269
+ const fileExists = await provider.fileExists(filePath);
270
+
271
+ if (fileExists) {
272
+ validationResults.success.push(`Required file exists: ${filePath}`);
273
+ } else {
274
+ validationResults.errors.push(`Required file missing: ${filePath}`);
275
+ }
276
+ } catch (error) {
277
+ validationResults.errors.push(`Error checking required file ${filePath}: ${error.message}`);
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Validates the presence of recommended markdown files in spec_directory.
284
+ *
285
+ * @param {import('../providers.js').Provider} provider - The provider instance
286
+ * @param {string} specDirectory - The spec_directory path
287
+ * @param {Object} validationResults - Results object to populate
288
+ * @private
289
+ */
290
+ async function validateRecommendedFiles(provider, specDirectory, validationResults) {
291
+ const foundFiles = [];
292
+ const missingFiles = [];
293
+
294
+ for (const filename of RECOMMENDED_SPEC_FILES) {
295
+ const filePath = joinPath(specDirectory, filename);
296
+
297
+ try {
298
+ const fileExists = await provider.fileExists(filePath);
299
+
300
+ if (fileExists) {
301
+ foundFiles.push(filename);
302
+ } else {
303
+ missingFiles.push(filename);
304
+ }
305
+ } catch (error) {
306
+ // Treat errors as missing files for recommended files
307
+ missingFiles.push(filename);
308
+ }
309
+ }
310
+
311
+ // Report results
312
+ if (foundFiles.length > 0) {
313
+ validationResults.success.push(`Found ${foundFiles.length} of ${RECOMMENDED_SPEC_FILES.length} recommended files: ${foundFiles.join(', ')}`);
314
+ }
315
+
316
+ if (missingFiles.length > 0) {
317
+ validationResults.warnings.push(`Missing ${missingFiles.length} recommended markdown file(s) in ${specDirectory}: ${missingFiles.join(', ')}`);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Validates that the spec_terms_directory contains markdown files.
323
+ *
324
+ * @param {import('../providers.js').Provider} provider - The provider instance
325
+ * @param {string} fullSpecTermsDirectory - The full path to the spec_terms_directory
326
+ * @param {string} originalSpecTermsDirectory - The original spec_terms_directory value from specs.json
327
+ * @param {Object} validationResults - Results object to populate
328
+ * @private
329
+ */
330
+ async function validateTermsDirectoryFiles(provider, fullSpecTermsDirectory, originalSpecTermsDirectory, validationResults) {
331
+ try {
332
+ // Check if directory exists first
333
+ const dirExists = await provider.directoryExists(fullSpecTermsDirectory);
334
+
335
+ if (!dirExists) {
336
+ // Already reported as an error in validateSpecTermsDirectory
337
+ return;
338
+ }
339
+
340
+ // List files in the directory
341
+ const fileEntries = await provider.listFiles(fullSpecTermsDirectory);
342
+
343
+ // Filter for markdown files (only actual files, not directories)
344
+ const markdownFiles = fileEntries.filter(entry =>
345
+ entry.isFile && (entry.name.endsWith('.md') || entry.name.endsWith('.markdown'))
346
+ );
347
+
348
+ if (markdownFiles.length > 0) {
349
+ validationResults.success.push(`spec_terms_directory contains ${markdownFiles.length} markdown file(s)`);
350
+ } else {
351
+ validationResults.warnings.push(`spec_terms_directory exists but contains no markdown files: ${originalSpecTermsDirectory}`);
352
+ }
353
+ } catch (error) {
354
+ validationResults.warnings.push(`Unable to list files in spec_terms_directory: ${error.message}`);
355
+ }
356
+ }
@@ -3,11 +3,12 @@
3
3
  *
4
4
  * This module validates the existence and structure of specs.json files in
5
5
  * specification repositories. It ensures that essential spec metadata is
6
- * present and properly formatted.
6
+ * present and properly formatted, including URL accessibility and file existence.
7
7
  *
8
8
  * @author spec-up-t-healthcheck
9
9
  */
10
10
 
11
+ import axios from 'axios';
11
12
  import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
12
13
 
13
14
  /**
@@ -20,13 +21,13 @@ export const CHECK_ID = 'specs-json';
20
21
  * Human-readable name for this health check.
21
22
  * @type {string}
22
23
  */
23
- export const CHECK_NAME = 'Specs.json Validation';
24
+ export const CHECK_NAME = 'specs.json';
24
25
 
25
26
  /**
26
27
  * Description of what this health check validates.
27
28
  * @type {string}
28
29
  */
29
- export const CHECK_DESCRIPTION = 'Validates the existence and structure of specs.json file';
30
+ export const CHECK_DESCRIPTION = 'Validates the existence and structure of specs.json file, including URL accessibility and file existence';
30
31
 
31
32
  /**
32
33
  * Required fields that must be present in a valid specs.json file.
@@ -58,6 +59,24 @@ const WARNING_FIELDS = Object.freeze(['favicon']);
58
59
  */
59
60
  const OPTIONAL_FIELDS = Object.freeze(['anchor_symbol', 'katex']);
60
61
 
62
+ /**
63
+ * Timeout for HTTP requests in milliseconds
64
+ * @type {number}
65
+ */
66
+ const HTTP_TIMEOUT = 10000;
67
+
68
+ /**
69
+ * Maximum number of redirects to follow
70
+ * @type {number}
71
+ */
72
+ const MAX_REDIRECTS = 5;
73
+
74
+ /**
75
+ * Proxy URL for browser environments (to bypass CORS)
76
+ * @type {string}
77
+ */
78
+ const PROXY_URL = './proxy.php';
79
+
61
80
  /**
62
81
  * Validates the existence and structure of specs.json in a repository.
63
82
  *
@@ -74,6 +93,8 @@ const OPTIONAL_FIELDS = Object.freeze(['anchor_symbol', 'katex']);
74
93
  * - Warning fields are checked (favicon)
75
94
  * - Optional fields are noted if missing
76
95
  * - Source object has required subfields
96
+ * - URL accessibility for logo, logo_link, and favicon (HTTP 200 OK)
97
+ * - Markdown files specified in markdown_paths exist in spec_directory
77
98
  *
78
99
  * @param {import('../providers.js').Provider} provider - The provider instance for file operations
79
100
  * @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} The health check result with validation details
@@ -152,6 +173,12 @@ export async function checkSpecsJson(provider) {
152
173
 
153
174
  // Validate field types and structure
154
175
  validateFieldTypes(spec, validationResults);
176
+
177
+ // Validate URL accessibility
178
+ await validateUrlAccessibility(spec, validationResults);
179
+
180
+ // Validate markdown file existence
181
+ await validateMarkdownFiles(spec, provider, validationResults);
155
182
 
156
183
  // Determine overall status
157
184
  let status = 'pass';
@@ -314,7 +341,8 @@ function validateFieldTypes(spec, results) {
314
341
 
315
342
  // Validate source object structure
316
343
  if (spec.source && typeof spec.source === 'object') {
317
- const requiredSourceFields = ['host', 'account', 'repo', 'branch'];
344
+ // Note: 'branch' field is no longer required as it's obtained from the spec-up-t:github-repo-info meta tag
345
+ const requiredSourceFields = ['host', 'account', 'repo'];
318
346
  let sourceValid = true;
319
347
 
320
348
  requiredSourceFields.forEach(field => {
@@ -333,7 +361,7 @@ function validateFieldTypes(spec, results) {
333
361
  if (spec.external_specs) {
334
362
  if (Array.isArray(spec.external_specs)) {
335
363
  spec.external_specs.forEach((extSpec, index) => {
336
- const requiredExtFields = ['external_spec', 'gh_page', 'url', 'terms_dir'];
364
+ const requiredExtFields = ['external_spec', 'gh_page', 'url'];
337
365
  requiredExtFields.forEach(field => {
338
366
  if (!(field in extSpec) || !extSpec[field]) {
339
367
  results.errors.push(`External spec ${index} missing "${field}"`);
@@ -352,6 +380,231 @@ function validateFieldTypes(spec, results) {
352
380
  }
353
381
  }
354
382
 
383
+ /**
384
+ * Validates URL accessibility for logo, logo_link, and favicon fields
385
+ * @param {Object} spec - The spec object to validate
386
+ * @param {Object} results - Results accumulator
387
+ */
388
+ async function validateUrlAccessibility(spec, results) {
389
+ const urlFields = [
390
+ { field: 'logo', required: true },
391
+ { field: 'logo_link', required: true },
392
+ { field: 'favicon', required: false }
393
+ ];
394
+
395
+ for (const { field, required } of urlFields) {
396
+ if (spec[field]) {
397
+ try {
398
+ const accessibility = await checkUrlAccessibility(spec[field], field);
399
+ if (accessibility.isAccessible) {
400
+ results.success.push(`${field} URL is accessible (HTTP ${accessibility.statusCode})`);
401
+ } else {
402
+ if (required) {
403
+ results.errors.push(`${field} URL is not accessible: ${accessibility.message}`);
404
+ } else {
405
+ results.warnings.push(`${field} URL is not accessible: ${accessibility.message}`);
406
+ }
407
+ }
408
+ } catch (error) {
409
+ if (required) {
410
+ results.errors.push(`Failed to check ${field} URL accessibility: ${error.message}`);
411
+ } else {
412
+ results.warnings.push(`Failed to check ${field} URL accessibility: ${error.message}`);
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Validates that markdown files specified in markdown_paths exist in spec_directory
421
+ * @param {Object} spec - The spec object to validate
422
+ * @param {import('../providers.js').Provider} provider - The provider instance for file operations
423
+ * @param {Object} results - Results accumulator
424
+ */
425
+ async function validateMarkdownFiles(spec, provider, results) {
426
+ if (!spec.markdown_paths || !Array.isArray(spec.markdown_paths)) {
427
+ return;
428
+ }
429
+
430
+ if (!spec.spec_directory) {
431
+ results.errors.push('Cannot validate markdown files: spec_directory is not defined');
432
+ return;
433
+ }
434
+
435
+ for (const markdownFile of spec.markdown_paths) {
436
+ if (typeof markdownFile !== 'string') {
437
+ results.errors.push(`Invalid markdown_paths entry: "${markdownFile}" is not a string`);
438
+ continue;
439
+ }
440
+
441
+ // Construct the full path to the markdown file
442
+ const filePath = `${spec.spec_directory.replace(/\/$/, '')}/${markdownFile}`;
443
+
444
+ try {
445
+ const exists = await provider.fileExists(filePath);
446
+ if (exists) {
447
+ results.success.push(`Markdown file "${markdownFile}" exists in spec_directory`);
448
+ } else {
449
+ results.errors.push(`Markdown file "${markdownFile}" not found in spec_directory "${spec.spec_directory}"`);
450
+ }
451
+ } catch (error) {
452
+ results.errors.push(`Failed to check existence of markdown file "${markdownFile}": ${error.message}`);
453
+ }
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Checks if a URL is accessible and returns HTTP 200
459
+ * @param {string} url - The URL to check
460
+ * @param {string} fieldName - Name of the field being checked (for error messages)
461
+ * @returns {Promise<{isAccessible: boolean, statusCode?: number, message?: string}>}
462
+ */
463
+ async function checkUrlAccessibility(url, fieldName) {
464
+ // Validate URL format first
465
+ if (!isValidUrl(url)) {
466
+ return {
467
+ isAccessible: false,
468
+ message: `Invalid URL format: ${url}`
469
+ };
470
+ }
471
+
472
+ // First, try HEAD request (more efficient)
473
+ const headResult = await attemptHeadRequest(url, fieldName);
474
+ if (headResult !== null) {
475
+ return headResult;
476
+ }
477
+
478
+ // If HEAD fails, try GET request (some servers don't support HEAD)
479
+ const getResult = await attemptGetRequest(url, fieldName);
480
+ return getResult;
481
+ }
482
+
483
+ /**
484
+ * Validates URL format
485
+ * @param {string} urlString - URL to validate
486
+ * @returns {boolean} True if URL is valid
487
+ */
488
+ function isValidUrl(urlString) {
489
+ try {
490
+ const url = new URL(urlString);
491
+ return url.protocol === 'http:' || url.protocol === 'https:';
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Detects if running in browser environment
499
+ * @returns {boolean} True if in browser environment
500
+ */
501
+ function isBrowserEnvironment() {
502
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
503
+ }
504
+
505
+ /**
506
+ * Attempts to check URL accessibility using HEAD request
507
+ * @param {string} url - The URL to check
508
+ * @param {string} fieldName - Name of the field being checked
509
+ * @returns {Promise<Object|null>} Result object or null if HEAD is not supported
510
+ */
511
+ async function attemptHeadRequest(url, fieldName) {
512
+ try {
513
+ const isBrowser = isBrowserEnvironment();
514
+
515
+ if (isBrowser) {
516
+ // In browser, try using proxy first
517
+ try {
518
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
519
+ const response = await axios.head(proxyUrl, {
520
+ timeout: HTTP_TIMEOUT,
521
+ validateStatus: (status) => status < 500
522
+ });
523
+ return createAccessibilityResult(response.status, fieldName);
524
+ } catch (proxyError) {
525
+ // Proxy not available (e.g., dev environment without PHP)
526
+ // Return null to skip this check gracefully
527
+ return null;
528
+ }
529
+ } else {
530
+ // In Node.js, make direct request
531
+ const response = await axios.head(url, {
532
+ timeout: HTTP_TIMEOUT,
533
+ maxRedirects: MAX_REDIRECTS,
534
+ validateStatus: (status) => status < 500
535
+ });
536
+ return createAccessibilityResult(response.status, fieldName);
537
+ }
538
+ } catch (error) {
539
+ // Return null to signal that GET should be attempted
540
+ return null;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Attempts to check URL accessibility using GET request
546
+ * @param {string} url - The URL to check
547
+ * @param {string} fieldName - Name of the field being checked
548
+ * @returns {Promise<Object>} Result object
549
+ */
550
+ async function attemptGetRequest(url, fieldName) {
551
+ try {
552
+ const isBrowser = isBrowserEnvironment();
553
+
554
+ if (isBrowser) {
555
+ // In browser, try using proxy
556
+ try {
557
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
558
+ const response = await axios.get(proxyUrl, {
559
+ timeout: HTTP_TIMEOUT,
560
+ validateStatus: (status) => status < 500
561
+ });
562
+ return createAccessibilityResult(response.status, fieldName);
563
+ } catch (proxyError) {
564
+ // Proxy not available (e.g., dev environment without PHP)
565
+ return {
566
+ isAccessible: false,
567
+ message: `${fieldName} accessibility check skipped (proxy unavailable in dev environment)`
568
+ };
569
+ }
570
+ } else {
571
+ // In Node.js, make direct request
572
+ const response = await axios.get(url, {
573
+ timeout: HTTP_TIMEOUT,
574
+ maxRedirects: MAX_REDIRECTS,
575
+ validateStatus: (status) => status < 500
576
+ });
577
+ return createAccessibilityResult(response.status, fieldName);
578
+ }
579
+ } catch (error) {
580
+ return {
581
+ isAccessible: false,
582
+ message: `${fieldName} is not accessible: ${error.code || error.message}`
583
+ };
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Creates a standardized accessibility result based on HTTP status code
589
+ * @param {number} statusCode - HTTP status code
590
+ * @param {string} fieldName - Name of the field being checked
591
+ * @returns {Object} Accessibility result
592
+ */
593
+ function createAccessibilityResult(statusCode, fieldName) {
594
+ if (statusCode === 200) {
595
+ return {
596
+ isAccessible: true,
597
+ statusCode: 200
598
+ };
599
+ } else {
600
+ return {
601
+ isAccessible: false,
602
+ statusCode: statusCode,
603
+ message: `${fieldName} returned HTTP ${statusCode}`
604
+ };
605
+ }
606
+ }
607
+
355
608
  // Export as default for backward compatibility
356
609
  export default {
357
610
  CHECK_ID,