spec-up-t-healthcheck 1.0.0 → 1.1.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.
@@ -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';
@@ -319,8 +346,12 @@ function validateFieldTypes(spec, results) {
319
346
 
320
347
  requiredSourceFields.forEach(field => {
321
348
  if (!(field in spec.source) || !spec.source[field]) {
322
- results.errors.push(`Source field "${field}" is missing or empty`);
323
- sourceValid = false;
349
+ if (field === 'branch') {
350
+ results.warnings.push(`Source field "${field}" is missing or empty`);
351
+ } else {
352
+ results.errors.push(`Source field "${field}" is missing or empty`);
353
+ sourceValid = false;
354
+ }
324
355
  }
325
356
  });
326
357
 
@@ -352,6 +383,231 @@ function validateFieldTypes(spec, results) {
352
383
  }
353
384
  }
354
385
 
386
+ /**
387
+ * Validates URL accessibility for logo, logo_link, and favicon fields
388
+ * @param {Object} spec - The spec object to validate
389
+ * @param {Object} results - Results accumulator
390
+ */
391
+ async function validateUrlAccessibility(spec, results) {
392
+ const urlFields = [
393
+ { field: 'logo', required: true },
394
+ { field: 'logo_link', required: true },
395
+ { field: 'favicon', required: false }
396
+ ];
397
+
398
+ for (const { field, required } of urlFields) {
399
+ if (spec[field]) {
400
+ try {
401
+ const accessibility = await checkUrlAccessibility(spec[field], field);
402
+ if (accessibility.isAccessible) {
403
+ results.success.push(`${field} URL is accessible (HTTP ${accessibility.statusCode})`);
404
+ } else {
405
+ if (required) {
406
+ results.errors.push(`${field} URL is not accessible: ${accessibility.message}`);
407
+ } else {
408
+ results.warnings.push(`${field} URL is not accessible: ${accessibility.message}`);
409
+ }
410
+ }
411
+ } catch (error) {
412
+ if (required) {
413
+ results.errors.push(`Failed to check ${field} URL accessibility: ${error.message}`);
414
+ } else {
415
+ results.warnings.push(`Failed to check ${field} URL accessibility: ${error.message}`);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Validates that markdown files specified in markdown_paths exist in spec_directory
424
+ * @param {Object} spec - The spec object to validate
425
+ * @param {import('../providers.js').Provider} provider - The provider instance for file operations
426
+ * @param {Object} results - Results accumulator
427
+ */
428
+ async function validateMarkdownFiles(spec, provider, results) {
429
+ if (!spec.markdown_paths || !Array.isArray(spec.markdown_paths)) {
430
+ return;
431
+ }
432
+
433
+ if (!spec.spec_directory) {
434
+ results.errors.push('Cannot validate markdown files: spec_directory is not defined');
435
+ return;
436
+ }
437
+
438
+ for (const markdownFile of spec.markdown_paths) {
439
+ if (typeof markdownFile !== 'string') {
440
+ results.errors.push(`Invalid markdown_paths entry: "${markdownFile}" is not a string`);
441
+ continue;
442
+ }
443
+
444
+ // Construct the full path to the markdown file
445
+ const filePath = `${spec.spec_directory.replace(/\/$/, '')}/${markdownFile}`;
446
+
447
+ try {
448
+ const exists = await provider.fileExists(filePath);
449
+ if (exists) {
450
+ results.success.push(`Markdown file "${markdownFile}" exists in spec_directory`);
451
+ } else {
452
+ results.errors.push(`Markdown file "${markdownFile}" not found in spec_directory "${spec.spec_directory}"`);
453
+ }
454
+ } catch (error) {
455
+ results.errors.push(`Failed to check existence of markdown file "${markdownFile}": ${error.message}`);
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Checks if a URL is accessible and returns HTTP 200
462
+ * @param {string} url - The URL to check
463
+ * @param {string} fieldName - Name of the field being checked (for error messages)
464
+ * @returns {Promise<{isAccessible: boolean, statusCode?: number, message?: string}>}
465
+ */
466
+ async function checkUrlAccessibility(url, fieldName) {
467
+ // Validate URL format first
468
+ if (!isValidUrl(url)) {
469
+ return {
470
+ isAccessible: false,
471
+ message: `Invalid URL format: ${url}`
472
+ };
473
+ }
474
+
475
+ // First, try HEAD request (more efficient)
476
+ const headResult = await attemptHeadRequest(url, fieldName);
477
+ if (headResult !== null) {
478
+ return headResult;
479
+ }
480
+
481
+ // If HEAD fails, try GET request (some servers don't support HEAD)
482
+ const getResult = await attemptGetRequest(url, fieldName);
483
+ return getResult;
484
+ }
485
+
486
+ /**
487
+ * Validates URL format
488
+ * @param {string} urlString - URL to validate
489
+ * @returns {boolean} True if URL is valid
490
+ */
491
+ function isValidUrl(urlString) {
492
+ try {
493
+ const url = new URL(urlString);
494
+ return url.protocol === 'http:' || url.protocol === 'https:';
495
+ } catch {
496
+ return false;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Detects if running in browser environment
502
+ * @returns {boolean} True if in browser environment
503
+ */
504
+ function isBrowserEnvironment() {
505
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
506
+ }
507
+
508
+ /**
509
+ * Attempts to check URL accessibility using HEAD request
510
+ * @param {string} url - The URL to check
511
+ * @param {string} fieldName - Name of the field being checked
512
+ * @returns {Promise<Object|null>} Result object or null if HEAD is not supported
513
+ */
514
+ async function attemptHeadRequest(url, fieldName) {
515
+ try {
516
+ const isBrowser = isBrowserEnvironment();
517
+
518
+ if (isBrowser) {
519
+ // In browser, try using proxy first
520
+ try {
521
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
522
+ const response = await axios.head(proxyUrl, {
523
+ timeout: HTTP_TIMEOUT,
524
+ validateStatus: (status) => status < 500
525
+ });
526
+ return createAccessibilityResult(response.status, fieldName);
527
+ } catch (proxyError) {
528
+ // Proxy not available (e.g., dev environment without PHP)
529
+ // Return null to skip this check gracefully
530
+ return null;
531
+ }
532
+ } else {
533
+ // In Node.js, make direct request
534
+ const response = await axios.head(url, {
535
+ timeout: HTTP_TIMEOUT,
536
+ maxRedirects: MAX_REDIRECTS,
537
+ validateStatus: (status) => status < 500
538
+ });
539
+ return createAccessibilityResult(response.status, fieldName);
540
+ }
541
+ } catch (error) {
542
+ // Return null to signal that GET should be attempted
543
+ return null;
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Attempts to check URL accessibility using GET request
549
+ * @param {string} url - The URL to check
550
+ * @param {string} fieldName - Name of the field being checked
551
+ * @returns {Promise<Object>} Result object
552
+ */
553
+ async function attemptGetRequest(url, fieldName) {
554
+ try {
555
+ const isBrowser = isBrowserEnvironment();
556
+
557
+ if (isBrowser) {
558
+ // In browser, try using proxy
559
+ try {
560
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
561
+ const response = await axios.get(proxyUrl, {
562
+ timeout: HTTP_TIMEOUT,
563
+ validateStatus: (status) => status < 500
564
+ });
565
+ return createAccessibilityResult(response.status, fieldName);
566
+ } catch (proxyError) {
567
+ // Proxy not available (e.g., dev environment without PHP)
568
+ return {
569
+ isAccessible: false,
570
+ message: `${fieldName} accessibility check skipped (proxy unavailable in dev environment)`
571
+ };
572
+ }
573
+ } else {
574
+ // In Node.js, make direct request
575
+ const response = await axios.get(url, {
576
+ timeout: HTTP_TIMEOUT,
577
+ maxRedirects: MAX_REDIRECTS,
578
+ validateStatus: (status) => status < 500
579
+ });
580
+ return createAccessibilityResult(response.status, fieldName);
581
+ }
582
+ } catch (error) {
583
+ return {
584
+ isAccessible: false,
585
+ message: `${fieldName} is not accessible: ${error.code || error.message}`
586
+ };
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Creates a standardized accessibility result based on HTTP status code
592
+ * @param {number} statusCode - HTTP status code
593
+ * @param {string} fieldName - Name of the field being checked
594
+ * @returns {Object} Accessibility result
595
+ */
596
+ function createAccessibilityResult(statusCode, fieldName) {
597
+ if (statusCode === 200) {
598
+ return {
599
+ isAccessible: true,
600
+ statusCode: 200
601
+ };
602
+ } else {
603
+ return {
604
+ isAccessible: false,
605
+ statusCode: statusCode,
606
+ message: `${fieldName} returned HTTP ${statusCode}`
607
+ };
608
+ }
609
+ }
610
+
355
611
  // Export as default for backward compatibility
356
612
  export default {
357
613
  CHECK_ID,