@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.
257 lines • 10.9 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.Generator = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const yaml = __importStar(require("js-yaml"));
const config_1 = require("./config");
const validator_1 = require("./validator");
const html_builder_1 = require("./html-builder");
const asset_copier_1 = require("./asset-copier");
const errors_1 = require("../utils/errors");
const logger_1 = require("../utils/logger");
const project_file_finder_1 = require("../utils/project-file-finder");
/**
* Main generator orchestrator that coordinates the build process
*/
class Generator {
constructor(config, cwd = process.cwd(), adminMode = false) {
this.config = config;
this.cwd = cwd;
this.adminMode = adminMode;
this.outputDir = path.isAbsolute(config.output || 'dist')
? config.output || 'dist'
: path.join(cwd, config.output || 'dist');
this.configLoader = new config_1.ConfigLoader(cwd);
this.validator = new validator_1.Validator(cwd);
this.htmlBuilder = new html_builder_1.HTMLBuilder(config, { adminMode: this.adminMode });
this.assetCopier = new asset_copier_1.AssetCopier(cwd, this.outputDir);
}
/**
* Create a Generator instance by loading configuration
*/
static async create(options = {}) {
const cwd = options.cwd || process.cwd();
const configLoader = new config_1.ConfigLoader(cwd);
// Load configuration
const config = await configLoader.load({
configPath: options.configPath
});
// Override output directory if specified in options
if (options.outputDir) {
config.output = options.outputDir;
}
// Override baseUrl if specified in options (useful for dev server)
if (options.baseUrl !== undefined) {
config.baseUrl = options.baseUrl;
}
// Pass adminMode to constructor (defaults to false for production builds)
const adminMode = options.adminMode || false;
return new Generator(config, cwd, adminMode);
}
/**
* Main generation method that orchestrates the entire build process
*/
async generate() {
try {
logger_1.Logger.header('🚀 Starting build process');
// 1. Load project data
logger_1.Logger.step('Loading project data...');
const projectsData = await this.loadProjectData();
logger_1.Logger.success(`Loaded ${projectsData.projects.length} project${projectsData.projects.length !== 1 ? 's' : ''}`);
// 2. Validate projects
logger_1.Logger.step('Validating project data...');
const warnings = this.validator.validate(projectsData.projects);
logger_1.Logger.success('Validation passed');
// Display warnings if any
if (warnings.length > 0) {
logger_1.Logger.newline();
logger_1.Logger.warn(`Found ${warnings.length} warning${warnings.length !== 1 ? 's' : ''}:`);
warnings.forEach(warning => {
const projectInfo = warning.projectId ? `[${warning.projectId}]` : `[Project ${warning.projectIndex}]`;
logger_1.Logger.dim(` ${projectInfo} ${warning.field}: ${warning.message}`);
});
logger_1.Logger.newline();
}
// 3. Clean output directory if requested
if (this.outputDir && fs.existsSync(this.outputDir)) {
logger_1.Logger.step('Cleaning output directory...');
this.cleanOutputDirectory();
logger_1.Logger.success('Output directory cleaned');
}
// 4. Generate HTML
logger_1.Logger.step('Generating HTML...');
const html = this.htmlBuilder.generateHTML(projectsData);
logger_1.Logger.success('HTML generated');
// 5. Write output
logger_1.Logger.step('Writing output files...');
await this.writeOutput(html);
logger_1.Logger.success('Output written');
// 6. Copy assets
logger_1.Logger.step('Copying assets...');
const thumbnails = projectsData.projects
.map(p => p.thumbnailLink)
.filter((t) => !!t);
await this.assetCopier.copyAssets(this.config, thumbnails);
logger_1.Logger.success('Assets copied');
logger_1.Logger.newline();
logger_1.Logger.icon('✨', `Build complete! Output: ${this.outputDir}`, '\x1b[32m');
}
catch (error) {
if (error instanceof errors_1.ProjectionError) {
throw error;
}
throw new errors_1.ProjectionError('Build process failed', errors_1.ErrorCodes.RUNTIME_ERROR, { originalError: error.message });
}
}
/**
* Load project data from projects.yaml or projects.json
*/
async loadProjectData() {
// Use shared utility to find projects file
const projectFileResult = project_file_finder_1.ProjectFileFinder.find(this.cwd);
if (!projectFileResult) {
throw new errors_1.ProjectionError('Projects file not found', errors_1.ErrorCodes.FILE_NOT_FOUND, {
message: `Could not find ${project_file_finder_1.ProjectFileFinder.getSupportedFileNames().join(', ')} in the current directory`,
searchedPaths: project_file_finder_1.ProjectFileFinder.getPossiblePaths(this.cwd),
cwd: this.cwd
});
}
const projectsPath = projectFileResult.path;
try {
const content = fs.readFileSync(projectsPath, 'utf-8');
const ext = path.extname(projectsPath);
let data;
if (ext === '.yaml' || ext === '.yml') {
data = yaml.load(content);
}
else if (ext === '.json') {
data = JSON.parse(content);
}
else {
throw new errors_1.ProjectionError(`Unsupported file format: ${ext}`, errors_1.ErrorCodes.PARSE_ERROR, { path: projectsPath });
}
// Validate structure
if (!data || typeof data !== 'object') {
throw new errors_1.ProjectionError('Invalid projects file: must contain an object', errors_1.ErrorCodes.PARSE_ERROR, { path: projectsPath });
}
if (!data.projects || !Array.isArray(data.projects)) {
throw new errors_1.ProjectionError('Invalid projects file: must contain a "projects" array', errors_1.ErrorCodes.PARSE_ERROR, { path: projectsPath });
}
return {
config: data.config,
projects: data.projects
};
}
catch (error) {
if (error instanceof errors_1.ProjectionError) {
throw error;
}
throw new errors_1.ProjectionError(`Failed to parse projects file: ${projectsPath}`, errors_1.ErrorCodes.PARSE_ERROR, {
path: projectsPath,
originalError: error.message
});
}
}
/**
* Write generated HTML to output file
*/
async writeOutput(html) {
// Ensure output directory exists
if (!fs.existsSync(this.outputDir)) {
try {
fs.mkdirSync(this.outputDir, { recursive: true });
}
catch (error) {
throw new errors_1.ProjectionError(`Failed to create output directory: ${this.outputDir}`, errors_1.ErrorCodes.FILE_WRITE_ERROR, {
path: this.outputDir,
originalError: error.message
});
}
}
const outputPath = path.join(this.outputDir, 'index.html');
try {
fs.writeFileSync(outputPath, html, 'utf-8');
}
catch (error) {
throw new errors_1.ProjectionError(`Failed to write output file: ${outputPath}`, errors_1.ErrorCodes.FILE_WRITE_ERROR, {
path: outputPath,
originalError: error.message
});
}
}
/**
* Clean the output directory
*/
cleanOutputDirectory() {
if (!fs.existsSync(this.outputDir)) {
return;
}
try {
// Remove all files and subdirectories
const entries = fs.readdirSync(this.outputDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(this.outputDir, entry.name);
if (entry.isDirectory()) {
fs.rmSync(fullPath, { recursive: true, force: true });
}
else {
fs.unlinkSync(fullPath);
}
}
}
catch (error) {
throw new errors_1.ProjectionError(`Failed to clean output directory: ${this.outputDir}`, errors_1.ErrorCodes.FILE_WRITE_ERROR, {
path: this.outputDir,
originalError: error.message
});
}
}
/**
* Get the output directory path
*/
getOutputDir() {
return this.outputDir;
}
/**
* Get the current configuration
*/
getConfig() {
return this.config;
}
}
exports.Generator = Generator;
//# sourceMappingURL=index.js.map