UNPKG

@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
"use strict"; 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