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.
- package/README.md +5 -0
- package/lib/checks/console-messages.js +211 -0
- package/lib/checks/external-specs-urls.js +93 -16
- package/lib/checks/gitignore.js +1 -1
- package/lib/checks/link-checker.js +361 -0
- package/lib/checks/package-json.js +1 -1
- package/lib/checks/spec-directory-and-files.js +356 -0
- package/lib/checks/specsjson.js +261 -5
- package/lib/formatters/result-details-formatter.js +505 -0
- package/lib/health-check-registry.js +57 -3
- package/lib/health-checker.js +13 -3
- package/lib/html-formatter.js +139 -168
- package/lib/index.js +1 -1
- package/lib/providers-browser.js +73 -0
- package/lib/providers.js +24 -0
- package/lib/web.js +10 -6
- package/package.json +5 -17
|
@@ -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
|
+
}
|
package/lib/checks/specsjson.js
CHANGED
|
@@ -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 = '
|
|
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
|
-
|
|
323
|
-
|
|
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,
|