@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.
464 lines (412 loc) • 21.4 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.showDeployHelp = showDeployHelp;
exports.deploy = deploy;
const git_helper_1 = require("../utils/git-helper");
const deployment_config_1 = require("../utils/deployment-config");
const project_file_finder_1 = require("../utils/project-file-finder");
const build_helper_1 = require("../utils/build-helper");
const logger_1 = require("../utils/logger");
const errors_1 = require("../utils/errors");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const ghpages = __importStar(require("gh-pages"));
/**
* Display comprehensive help documentation for the deploy command
*/
function showDeployHelp() {
console.log(`
${'\x1b[1m'}Projection Deploy - GitHub Pages Deployment${'\x1b[0m'}
${'\x1b[1m'}DESCRIPTION:${'\x1b[0m'}
Deploy your portfolio site to GitHub Pages with a single command.
Automatically builds your site and pushes it to the gh-pages branch.
${'\x1b[1m'}USAGE:${'\x1b[0m'}
projection deploy [options]
${'\x1b[1m'}OPTIONS:${'\x1b[0m'}
-b, --branch <branch> Target branch for deployment
Default: gh-pages
-m, --message <message> Custom commit message for deployment
Default: "Deploy to GitHub Pages - <timestamp>"
-r, --remote <remote> Git remote to push to
Default: origin
-d, --dir <dir> Build directory to deploy
Default: dist
--no-build Skip the build step and deploy existing files
Use this if you've already built your site
--dry-run Simulate deployment without actually pushing
Useful for testing your configuration
--force Force push to remote (overwrites remote history)
${'\x1b[33m'}⚠️ Use with caution!${'\x1b[0m'}
--help Show this help message
${'\x1b[1m'}EXAMPLES:${'\x1b[0m'}
${'\x1b[36m'}# Basic deployment (most common)${'\x1b[0m'}
projection deploy
${'\x1b[36m'}# Deploy to a custom branch${'\x1b[0m'}
projection deploy --branch main
${'\x1b[36m'}# Deploy with a custom commit message${'\x1b[0m'}
projection deploy --message "Update portfolio with new projects"
${'\x1b[36m'}# Deploy without rebuilding (use existing dist folder)${'\x1b[0m'}
projection deploy --no-build
${'\x1b[36m'}# Test deployment without actually pushing${'\x1b[0m'}
projection deploy --dry-run
${'\x1b[36m'}# Deploy to a different remote${'\x1b[0m'}
projection deploy --remote upstream
${'\x1b[36m'}# Force push (overwrites remote history)${'\x1b[0m'}
projection deploy --force
${'\x1b[36m'}# Deploy from a custom build directory${'\x1b[0m'}
projection deploy --dir build
${'\x1b[36m'}# Combine multiple options${'\x1b[0m'}
projection deploy --branch gh-pages --message "v2.0 release" --no-build
${'\x1b[1m'}CONFIGURATION:${'\x1b[0m'}
You can configure deployment settings in your projection.config.json:
${'\x1b[2m'}{
title: "My Portfolio",
baseUrl: "/my-repo/",
homepage: "portfolio.example.com",
deployBranch: "gh-pages"
}${'\x1b[0m'}
${'\x1b[1m'}baseUrl:${'\x1b[0m'} The URL path where your site will be hosted
For project sites: /repository-name/
For user sites: /
${'\x1b[1m'}homepage:${'\x1b[0m'} Custom domain for your site (creates CNAME file)
Example: "portfolio.example.com"
${'\x1b[1m'}deployBranch:${'\x1b[0m'} Default branch for deployment (default: gh-pages)
${'\x1b[1m'}REQUIREMENTS:${'\x1b[0m'}
Before deploying, ensure you have:
${'\x1b[32m'}✓${'\x1b[0m'} Git installed and configured
${'\x1b[32m'}✓${'\x1b[0m'} A Git repository initialized (git init)
${'\x1b[32m'}✓${'\x1b[0m'} A remote configured (git remote add origin <url>)
${'\x1b[32m'}✓${'\x1b[0m'} A projects file (projects.yaml, projects.yml, or projects.json)
${'\x1b[32m'}✓${'\x1b[0m'} Git credentials configured for pushing
${'\x1b[1m'}FIRST-TIME SETUP:${'\x1b[0m'}
1. Initialize Git repository:
${'\x1b[2m'}git init${'\x1b[0m'}
2. Add your GitHub repository as remote:
${'\x1b[2m'}git remote add origin https://github.com/username/repo.git${'\x1b[0m'}
3. Configure baseUrl in projection.config.json:
${'\x1b[2m'}baseUrl: "/repo/"${'\x1b[0m'}
4. Deploy your site:
${'\x1b[2m'}projection deploy${'\x1b[0m'}
5. Enable GitHub Pages in repository settings:
- Go to Settings → Pages
- Set source to "gh-pages" branch
- Save and wait for deployment
${'\x1b[1m'}TROUBLESHOOTING:${'\x1b[0m'}
${'\x1b[1m'}Problem:${'\x1b[0m'} Git is not installed
${'\x1b[1m'}Solution:${'\x1b[0m'} Install Git from https://git-scm.com/
${'\x1b[1m'}Problem:${'\x1b[0m'} Not a git repository
${'\x1b[1m'}Solution:${'\x1b[0m'} Run 'git init' to initialize a repository
${'\x1b[1m'}Problem:${'\x1b[0m'} No git remote found
${'\x1b[1m'}Solution:${'\x1b[0m'} Run 'git remote add origin <url>' to add a remote
${'\x1b[1m'}Problem:${'\x1b[0m'} Authentication failed
${'\x1b[1m'}Solution:${'\x1b[0m'} Configure Git credentials:
- Use SSH keys: https://docs.github.com/en/authentication
- Or configure credential helper: git config credential.helper store
- Or use personal access token
${'\x1b[1m'}Problem:${'\x1b[0m'} Push rejected due to conflicts
${'\x1b[1m'}Solution:${'\x1b[0m'} Use --force flag to force push (overwrites remote)
${'\x1b[2m'}projection deploy --force${'\x1b[0m'}
${'\x1b[33m'}⚠️ Warning: This will overwrite the remote branch history${'\x1b[0m'}
${'\x1b[1m'}Problem:${'\x1b[0m'} Build fails during deployment
${'\x1b[1m'}Solution:${'\x1b[0m'} Fix build errors first, or use --no-build to deploy existing files
${'\x1b[2m'}projection build${'\x1b[0m'} (to test build separately)
${'\x1b[2m'}projection deploy --no-build${'\x1b[0m'} (to deploy without building)
${'\x1b[1m'}Problem:${'\x1b[0m'} Site shows 404 or broken links
${'\x1b[1m'}Solution:${'\x1b[0m'} Check that baseUrl in config matches your repository name
For https://username.github.io/repo/, use baseUrl: "/repo/"
${'\x1b[1m'}Problem:${'\x1b[0m'} Site not updating after deployment
${'\x1b[1m'}Solution:${'\x1b[0m'} GitHub Pages can take a few minutes to update
- Check repository Settings → Pages for deployment status
- Clear browser cache and try again
- Verify gh-pages branch has the latest changes
${'\x1b[1m'}Problem:${'\x1b[0m'} Custom domain not working
${'\x1b[1m'}Solution:${'\x1b[0m'} Ensure homepage is set in config and DNS is configured:
1. Add homepage: "yourdomain.com" to projection.config.json
2. Configure DNS records with your domain provider
3. Enable HTTPS in GitHub Pages settings
${'\x1b[1m'}WORKFLOW:${'\x1b[0m'}
The deploy command performs these steps:
1. ${'\x1b[2m'}Validates Git installation and repository setup${'\x1b[0m'}
2. ${'\x1b[2m'}Checks for projects file${'\x1b[0m'}
3. ${'\x1b[2m'}Loads deployment configuration${'\x1b[0m'}
4. ${'\x1b[2m'}Builds the site (unless --no-build)${'\x1b[0m'}
5. ${'\x1b[2m'}Adds .nojekyll file to disable Jekyll${'\x1b[0m'}
6. ${'\x1b[2m'}Adds CNAME file if custom domain configured${'\x1b[0m'}
7. ${'\x1b[2m'}Pushes to gh-pages branch${'\x1b[0m'}
8. ${'\x1b[2m'}Displays deployment URL and instructions${'\x1b[0m'}
${'\x1b[1m'}MORE INFORMATION:${'\x1b[0m'}
GitHub Pages Documentation:
https://docs.github.com/en/pages
Projection Documentation:
https://github.com/quasarbright/projection
`);
}
/**
* Deploy command - builds and deploys the portfolio site to GitHub Pages
*
* @param options - Deployment options from command-line
* @throws ProjectionError if validation fails or deployment cannot proceed
*/
async function deploy(options = {}) {
// Check if help flag is set
if (options.help) {
showDeployHelp();
return;
}
const cwd = process.cwd();
try {
logger_1.Logger.header('🚀 Deploying to GitHub Pages');
logger_1.Logger.newline();
// Step 1: Validate Git installation
logger_1.Logger.step('Checking Git installation...');
const gitInstalled = await git_helper_1.GitHelper.isGitInstalled();
if (!gitInstalled) {
throw new errors_1.ProjectionError('Git is not installed or not in PATH', errors_1.ErrorCodes.VALIDATION_ERROR, {
solution: 'Install Git from https://git-scm.com/',
required: 'Git is required for deployment to GitHub Pages'
});
}
logger_1.Logger.success('Git is installed');
// Step 2: Validate Git repository and remote configuration
logger_1.Logger.step('Validating Git repository...');
const remote = options.remote || 'origin';
const validation = await git_helper_1.GitHelper.validateRepository(cwd, remote);
if (!validation.isGitRepo) {
throw new errors_1.ProjectionError('Not a git repository', errors_1.ErrorCodes.VALIDATION_ERROR, {
solution: "Run 'git init' to initialize a repository",
required: 'A Git repository is required for deployment'
});
}
if (!validation.hasRemote) {
throw new errors_1.ProjectionError(`No git remote '${remote}' found`, errors_1.ErrorCodes.VALIDATION_ERROR, {
solution: `Run 'git remote add ${remote} <url>' to add a remote`,
required: 'A Git remote is required to push the deployment'
});
}
logger_1.Logger.success(`Git repository validated (remote: ${remote})`);
// Step 3: Verify projects data file exists
logger_1.Logger.step('Checking for projects file...');
const projectFile = project_file_finder_1.ProjectFileFinder.find(cwd);
if (!projectFile) {
const possibleFiles = project_file_finder_1.ProjectFileFinder.getSupportedFileNames();
throw new errors_1.ProjectionError('No projects file found', errors_1.ErrorCodes.FILE_NOT_FOUND, {
solution: `Create one of: ${possibleFiles.join(', ')}`,
required: 'A projects file is required to build the site'
});
}
logger_1.Logger.success(`Found projects file: ${projectFile.path}`);
// Step 4: Load deployment configuration
logger_1.Logger.step('Loading deployment configuration...');
const config = await deployment_config_1.DeploymentConfigLoader.load(cwd, options);
logger_1.Logger.success('Configuration loaded');
// Step 5: Display pre-deployment summary
logger_1.Logger.newline();
logger_1.Logger.header('📋 Deployment Summary');
logger_1.Logger.keyValue('Repository', validation.remoteUrl);
logger_1.Logger.keyValue('Branch', config.branch);
logger_1.Logger.keyValue('Build Directory', config.buildDir);
logger_1.Logger.keyValue('Base URL', config.baseUrl);
if (config.homepage) {
logger_1.Logger.keyValue('Custom Domain', config.homepage);
}
if (options.dryRun) {
logger_1.Logger.keyValue('Mode', 'DRY RUN (no changes will be made)');
}
logger_1.Logger.newline();
// If dry-run, stop here
if (options.dryRun) {
logger_1.Logger.info('Dry run complete. No deployment was performed.');
return;
}
// Step 6: Build the site (unless --no-build flag is set)
if (!options.noBuild) {
logger_1.Logger.newline();
logger_1.Logger.header('🔨 Building site');
logger_1.Logger.newline();
try {
// Use shared build helper
await build_helper_1.BuildHelper.runBuild({
cwd,
outputDir: config.buildDir,
clean: true
});
}
catch (error) {
logger_1.Logger.newline();
logger_1.Logger.error('Deployment aborted due to build failure');
logger_1.Logger.newline();
throw new errors_1.ProjectionError('Build failed', errors_1.ErrorCodes.RUNTIME_ERROR, { originalError: error.message });
}
}
else {
logger_1.Logger.newline();
logger_1.Logger.info('Skipping build (--no-build flag set)');
logger_1.Logger.dim(`Using existing files in ${config.buildDir}`);
logger_1.Logger.newline();
}
// Step 7: Configure and execute gh-pages deployment
logger_1.Logger.newline();
logger_1.Logger.header('📦 Deploying to GitHub Pages');
logger_1.Logger.newline();
try {
// Prepare build directory path
const buildDirPath = path.isAbsolute(config.buildDir)
? config.buildDir
: path.join(cwd, config.buildDir);
// Verify build directory exists
if (!fs.existsSync(buildDirPath)) {
throw new errors_1.ProjectionError(`Build directory not found: ${buildDirPath}`, errors_1.ErrorCodes.FILE_NOT_FOUND, {
solution: 'Run the build first or check the --dir option',
required: 'The build directory must exist before deployment'
});
}
// Add .nojekyll file to disable Jekyll processing
const nojekyllPath = path.join(buildDirPath, '.nojekyll');
if (!fs.existsSync(nojekyllPath)) {
logger_1.Logger.step('Adding .nojekyll file...');
fs.writeFileSync(nojekyllPath, '', 'utf8');
logger_1.Logger.success('.nojekyll file added');
}
// Add CNAME file if homepage is configured
if (config.homepage) {
const cnamePath = path.join(buildDirPath, 'CNAME');
logger_1.Logger.step(`Adding CNAME file for ${config.homepage}...`);
fs.writeFileSync(cnamePath, config.homepage, 'utf8');
logger_1.Logger.success('CNAME file added');
}
// Configure gh-pages options
const ghPagesOptions = {
branch: config.branch,
dest: '.',
message: options.message || `Deploy to GitHub Pages - ${new Date().toISOString()}`,
remote: config.remote,
dotfiles: true, // Include .nojekyll
add: true, // Preserve commit history
...(options.force && { force: true }) // Force push if requested
};
logger_1.Logger.step('Publishing to GitHub Pages...');
logger_1.Logger.dim(`Branch: ${config.branch}`);
logger_1.Logger.dim(`Remote: ${config.remote}`);
logger_1.Logger.newline();
// Publish to gh-pages
await new Promise((resolve, reject) => {
ghpages.publish(buildDirPath, ghPagesOptions, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
// Display success message
logger_1.Logger.newline();
logger_1.Logger.icon('🎉', 'Deployment successful!', '\x1b[32m');
logger_1.Logger.newline();
// Generate GitHub Pages URL
const repoName = deployment_config_1.DeploymentConfigLoader.extractRepoName(config.repositoryUrl);
const githubPagesUrl = config.homepage || deployment_config_1.DeploymentConfigLoader.generateGitHubPagesUrl(config.repositoryUrl);
logger_1.Logger.keyValue('Site URL', githubPagesUrl);
logger_1.Logger.keyValue('Branch', config.branch);
if (ghPagesOptions.message) {
logger_1.Logger.keyValue('Commit', ghPagesOptions.message);
}
logger_1.Logger.newline();
logger_1.Logger.info('Your site has been deployed to GitHub Pages!');
logger_1.Logger.dim('Note: It may take a few minutes for GitHub Pages to update.');
logger_1.Logger.newline();
// Provide instructions for enabling GitHub Pages if needed
logger_1.Logger.info('If this is your first deployment:');
logger_1.Logger.dim('1. Go to your repository settings on GitHub');
logger_1.Logger.dim(`2. Navigate to Pages section`);
logger_1.Logger.dim(`3. Ensure the source is set to the '${config.branch}' branch`);
logger_1.Logger.newline();
}
catch (error) {
logger_1.Logger.newline();
logger_1.Logger.error('Deployment failed');
logger_1.Logger.newline();
// Handle gh-pages specific errors
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('permission denied') || errorMessage.includes('authentication')) {
logger_1.Logger.dim('Authentication failed. Please check your Git credentials.');
logger_1.Logger.newline();
logger_1.Logger.info('Solutions:');
logger_1.Logger.dim('• Configure Git credentials: git config credential.helper store');
logger_1.Logger.dim('• Use SSH keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh');
logger_1.Logger.dim('• Check repository permissions');
}
else if (errorMessage.includes('rejected') || errorMessage.includes('conflict')) {
logger_1.Logger.dim('Push rejected due to conflicts.');
logger_1.Logger.newline();
logger_1.Logger.info('Solutions:');
logger_1.Logger.dim('• Use --force flag to force push (caution: overwrites remote)');
logger_1.Logger.dim('• Manually resolve conflicts in the gh-pages branch');
}
else if (errorMessage.includes('not found') || errorMessage.includes('does not exist')) {
logger_1.Logger.dim('Repository or remote not found.');
logger_1.Logger.newline();
logger_1.Logger.info('Solutions:');
logger_1.Logger.dim('• Verify the remote URL: git remote -v');
logger_1.Logger.dim('• Check repository permissions');
logger_1.Logger.dim('• Ensure the repository exists on GitHub');
}
else {
logger_1.Logger.dim(error.message);
}
logger_1.Logger.newline();
}
throw new errors_1.ProjectionError('Deployment failed', errors_1.ErrorCodes.RUNTIME_ERROR, { originalError: error.message });
}
}
catch (error) {
if (error instanceof errors_1.ProjectionError) {
logger_1.Logger.error(error.message);
logger_1.Logger.newline();
if (error.details.solution) {
logger_1.Logger.info(`Solution: ${error.details.solution}`);
}
if (error.details.required) {
logger_1.Logger.dim(error.details.required);
}
throw error;
}
// Re-throw unexpected errors
throw error;
}
}
//# sourceMappingURL=deploy.js.map