@fwdslsh/unify
Version:
A lightweight, framework-free static site generator with Bun native APIs
324 lines (312 loc) • 9.15 kB
JavaScript
import { UnifyError } from "../utils/errors.js";
/**
* Calculate Levenshtein distance between two strings
* @param {string} a First string
* @param {string} b Second string
* @returns {number} Edit distance
*/
function levenshteinDistance(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[b.length][a.length];
}
/**
* Find the closest command suggestion for a typo
* @param {string} input The user's input
* @param {string[]} commands Available commands
* @returns {string|null} Closest command or null if no good match
*/
function findClosestCommand(input, commands) {
const maxDistance = 2; // Only suggest if edit distance is 2 or less
let bestMatch = null;
let bestDistance = Infinity;
for (const command of commands) {
const distance = levenshteinDistance(input.toLowerCase(), command.toLowerCase());
if (distance < bestDistance && distance <= maxDistance) {
bestDistance = distance;
bestMatch = command;
}
}
return bestMatch;
}
/**
* Command-line argument parser for unify
* Handles parsing of CLI arguments and options
*/
export function parseArgs(argv) {
const args = {
command: null,
source: "src",
output: "dist",
port: 3000,
host: "localhost",
prettyUrls: false,
baseUrl: "https://example.com",
clean: false,
sitemap: true,
failOn: null, // Can be 'warning', 'error', or null (default: null = only fail on fatal errors)
minify: false,
verbose: false,
help: false,
version: false,
copy: null,
layouts: null,
template: null, // For init command - which starter template to use
};
// Only the first non-option argument is considered a command
let commandFound = false;
let i = 0;
const validCommands = ['build', 'watch', 'serve', 'init'];
while (i < argv.length) {
const arg = argv[i];
const nextArg = argv[i + 1];
// Help/version flags should always be recognized
if (arg === '--help' || arg === '-h') {
args.help = true;
i++;
continue;
}
if (arg === '--version' || arg === '-v') {
args.version = true;
i++;
continue;
}
// If not a flag/option, and command not yet found, treat as command or error
if (!arg.startsWith('-') && !commandFound) {
if (validCommands.includes(arg)) {
args.command = arg;
commandFound = true;
i++;
continue;
} else {
// If help/version is set, don't throw error
if (args.help || args.version) {
i++;
continue;
}
const suggestion = findClosestCommand(arg, validCommands);
const suggestions = [];
if (suggestion) {
suggestions.push(`Did you mean "${suggestion}"?`);
}
suggestions.push(
'Use --help to see valid options',
'Check for typos in the command name',
'Check the documentation for supported commands'
);
const error = new UnifyError(
`Unknown command: ${arg}`,
null,
null,
suggestions
);
error.errorType = 'UsageError';
throw error;
}
}
// If not a flag/option, and command already found
if (!arg.startsWith('-') && commandFound) {
// For init command, the first non-flag argument is the template name
if (args.command === 'init' && !args.template) {
args.template = arg;
i++;
continue;
}
// Otherwise treat as unknown option
const error = new UnifyError(
`Unknown option: ${arg}`,
null,
null,
[
'Use --help to see valid options',
'Check for typos in the argument'
]
);
error.errorType = 'UsageError';
throw error;
}
// Options with values
if ((arg === '--source' || arg === '-s') && nextArg && !nextArg.startsWith('-')) {
args.source = nextArg;
i += 2;
continue;
}
if ((arg === '--output' || arg === '-o') && nextArg && !nextArg.startsWith('-')) {
args.output = nextArg;
i += 2;
continue;
}
if ((arg === '--copy') && nextArg && !nextArg.startsWith('-')) {
args.copy = nextArg;
i += 2;
continue;
}
// Handle --copy without value
if (arg === '--copy') {
const error = new UnifyError(
'The --copy option requires a glob pattern value',
null,
null,
[
'Provide a glob pattern like: --copy "./assets/**/*.*"',
'Use quotes around patterns with special characters',
'Check the documentation for glob pattern examples'
]
);
error.errorType = 'UsageError';
throw error;
}
if ((arg === '--port' || arg === '-p') && nextArg && !nextArg.startsWith('-')) {
args.port = parseInt(nextArg, 10);
if (isNaN(args.port) || args.port < 1 || args.port > 65535) {
throw new UnifyError(
'Port must be a number between 1 and 65535',
null,
null,
[
'Use a port number like 3000, 8080, or 8000',
'Check that the port is not already in use',
'Valid port range is 1-65535'
]
);
}
i += 2;
continue;
}
if (arg === '--host' && nextArg && !nextArg.startsWith('-')) {
args.host = nextArg;
i += 2;
continue;
}
if (arg === '--pretty-urls' || arg === '-u') {
args.prettyUrls = true;
i++;
continue;
}
if (arg === '--base-url' && nextArg && !nextArg.startsWith('-')) {
args.baseUrl = nextArg;
i += 2;
continue;
}
if (arg === '--clean') {
args.clean = true;
i++;
continue;
}
if (arg === '--no-sitemap') {
args.sitemap = false;
i++;
continue;
}
if (arg === '--fail-on' && nextArg && !nextArg.startsWith('-')) {
const validLevels = ['warning', 'error'];
if (validLevels.includes(nextArg)) {
args.failOn = nextArg;
i += 2;
continue;
} else {
const error = new UnifyError(
`Invalid --fail-on level: ${nextArg}`,
null,
null,
[
'Valid levels are: warning, error',
'Use --fail-on warning to fail on any warning or error',
'Use --fail-on error to fail only on errors (default behavior)',
'Omit --fail-on to only fail on fatal build errors'
]
);
error.errorType = 'UsageError';
throw error;
}
}
if (arg === '--fail-on') {
const error = new UnifyError(
'The --fail-on option requires a level value',
null,
null,
[
'Valid levels are: warning, error',
'Use --fail-on warning to fail on any warning or error',
'Use --fail-on error to fail only on errors',
'Omit --fail-on to only fail on fatal build errors'
]
);
error.errorType = 'UsageError';
throw error;
}
if (arg === '--minify' || arg === '-m') {
args.minify = true;
i++;
continue;
}
if (arg === '--verbose' || arg === '-V') {
args.verbose = true;
i++;
continue;
}
// Unknown arguments
if (arg.startsWith('-')) {
const validOptions = [
'--help', '-h', '--version', '-v', '--source', '-s', '--output', '-o',
'--copy', '--port', '-p', '--host', '--layouts', '-l', '--templates',
'--pretty-urls', '--base-url', '--clean', '--no-sitemap',
'--fail-on', '--minify', '--verbose', '-u', '-m', '-V'
];
const suggestion = findClosestCommand(arg, validOptions);
const suggestions = [];
if (suggestion) {
suggestions.push(`Did you mean "${suggestion}"?`);
}
suggestions.push(
'Use --help to see valid options',
'Check for typos in the option name',
'Check the documentation for supported flags'
);
const error = new UnifyError(
`Unknown option: ${arg}`,
null,
null,
suggestions
);
error.errorType = 'UsageError';
throw error;
} else {
// Non-option argument that's not a command
const error = new UnifyError(
`Unknown option: ${arg}`,
null,
null,
[
'Use --help to see valid options',
'Check for typos in the argument'
]
);
error.errorType = 'UsageError';
throw error;
}
}
if (!args.command && !args.help && !args.version) {
// Default to build if no command found and not help/version
args.command = 'build';
}
return args;
}