@quasarbright/projection
Version:
A static site generator that creates a beautiful, interactive gallery to showcase your coding projects. Features search, filtering, tags, responsive design, and an admin UI.
260 lines • 9.61 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Validator = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const project_1 = require("../types/project");
const errors_1 = require("../utils/errors");
/**
* Validates project data according to requirements
*/
class Validator {
constructor(cwd = process.cwd()) {
this.cwd = cwd;
}
/**
* Validates an array of projects
* @param projects - Array of projects to validate
* @throws ProjectionError if validation fails
* @returns Array of warnings (non-fatal issues)
*/
validate(projects) {
const result = this.validateProjects(projects);
if (!result.valid) {
throw new errors_1.ProjectionError('Project data validation failed', errors_1.ErrorCodes.VALIDATION_ERROR, { errors: result.errors });
}
return result.warnings;
}
/**
* Validates projects and returns detailed results
* @param projects - Array of projects to validate
* @returns ValidationResult with all errors and warnings found
*/
validateProjects(projects) {
const errors = [];
const warnings = [];
if (!Array.isArray(projects)) {
return {
valid: false,
errors: [{
projectIndex: -1,
field: 'projects',
message: 'Projects must be an array'
}],
warnings: []
};
}
if (projects.length === 0) {
return {
valid: false,
errors: [{
projectIndex: -1,
field: 'projects',
message: 'Projects array cannot be empty'
}],
warnings: []
};
}
// Track project IDs for duplicate detection
const seenIds = new Set();
projects.forEach((project, index) => {
// Validate required fields
errors.push(...this.validateRequiredFields(project, index));
// Validate project ID format
if (project.id) {
errors.push(...this.validateProjectId(project.id, index));
// Check for duplicate IDs
if (seenIds.has(project.id)) {
errors.push({
projectId: project.id,
projectIndex: index,
field: 'id',
message: `Duplicate project ID: "${project.id}"`
});
}
else {
seenIds.add(project.id);
}
}
// Validate date format
if (project.creationDate) {
errors.push(...this.validateDateFormat(project.creationDate, project.id, index));
}
// Check for missing local asset files (warnings only)
warnings.push(...this.checkLocalAssets(project, index));
});
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Validates that all required fields are present
*/
validateRequiredFields(project, index) {
const errors = [];
const requiredFields = ['id', 'title', 'pageLink', 'creationDate'];
requiredFields.forEach(field => {
if (!project[field] || (typeof project[field] === 'string' && project[field].trim() === '')) {
errors.push({
projectId: project.id,
projectIndex: index,
field,
message: `Missing required field: "${field}"`
});
}
});
return errors;
}
/**
* Validates project ID format (URL slug pattern)
*/
validateProjectId(id, index) {
const errors = [];
if (!(0, project_1.isValidProjectId)(id)) {
errors.push({
projectId: id,
projectIndex: index,
field: 'id',
message: `Invalid project ID format: "${id}". Must be lowercase alphanumeric with hyphens, cannot start or end with hyphen`
});
}
return errors;
}
/**
* Validates date format (ISO date string YYYY-MM-DD)
*/
validateDateFormat(date, projectId, index) {
const errors = [];
// Check basic format YYYY-MM-DD
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
if (!datePattern.test(date)) {
errors.push({
projectId,
projectIndex: index,
field: 'creationDate',
message: `Invalid date format: "${date}". Expected format: YYYY-MM-DD (e.g., "2024-01-15")`
});
return errors;
}
// Validate that it's a real date by checking if the parsed date matches the input
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
errors.push({
projectId,
projectIndex: index,
field: 'creationDate',
message: `Invalid date: "${date}". Date does not exist`
});
return errors;
}
// Check if the date was normalized (e.g., 2024-02-30 becomes 2024-03-01)
const isoString = parsedDate.toISOString().split('T')[0];
if (isoString !== date) {
errors.push({
projectId,
projectIndex: index,
field: 'creationDate',
message: `Invalid date: "${date}". Date does not exist`
});
}
return errors;
}
/**
* Checks if local asset files exist (generates warnings, not errors)
*/
checkLocalAssets(project, index) {
const warnings = [];
// Check thumbnailLink if it's a local path
if (project.thumbnailLink) {
warnings.push(...this.checkLocalFile(project.thumbnailLink, 'thumbnailLink', project.id, index));
}
return warnings;
}
/**
* Checks if a file path is local and if it exists
*/
checkLocalFile(filePath, field, projectId, index) {
const warnings = [];
// Skip if it's an absolute URL
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return warnings;
}
// Skip if it's a domain-absolute path (starts with /)
if (filePath.startsWith('/')) {
return warnings;
}
// Skip if it's an admin:// prefixed path (admin-uploaded screenshots)
if (filePath.startsWith('admin://')) {
// Validate that the file exists in screenshots/ directory
const filename = filePath.substring(8); // Remove 'admin://' prefix
const screenshotsPath = path.join(this.cwd, 'screenshots', filename);
if (!fs.existsSync(screenshotsPath)) {
warnings.push({
projectId,
projectIndex: index,
field,
message: `Admin-uploaded file not found: "${filePath}" (expected at: screenshots/${filename})`
});
}
return warnings;
}
// It's a local relative path - check if it exists
let resolvedPath;
if (filePath.startsWith('./')) {
resolvedPath = path.join(this.cwd, filePath.substring(2));
}
else if (filePath.startsWith('../')) {
resolvedPath = path.join(this.cwd, filePath);
}
else {
// Path without prefix - treat as relative to cwd
resolvedPath = path.join(this.cwd, filePath);
}
if (!fs.existsSync(resolvedPath)) {
warnings.push({
projectId,
projectIndex: index,
field,
message: `Local file not found: "${filePath}" (resolved to: ${resolvedPath})`
});
}
return warnings;
}
}
exports.Validator = Validator;
//# sourceMappingURL=validator.js.map