@gavbarosee/react-kickstart
Version:
A modern CLI tool for creating React applications with various frameworks
382 lines (335 loc) • 11 kB
JavaScript
import path from "path";
import validateProjectName from "validate-npm-package-name";
/**
* Validation utilities - input validation and verification functions
*/
/**
* Validate project name using npm package name rules
* @param {string} projectName - Project name to validate
* @returns {Object} - Validation result with errors/warnings
*/
export function validateProjectNameInput(projectName) {
const result = validateProjectName(projectName);
return {
valid: result.validForNewPackages,
errors: result.errors || [],
warnings: result.warnings || [],
name: projectName,
};
}
/**
* Validate directory path
* @param {string} dirPath - Directory path to validate
* @returns {Object} - Validation result
*/
export function validateDirectoryPath(dirPath) {
if (!dirPath) {
return { valid: false, error: "Directory path is required" };
}
if (typeof dirPath !== "string") {
return { valid: false, error: "Directory path must be a string" };
}
if (dirPath.length === 0) {
return { valid: false, error: "Directory path cannot be empty" };
}
// Check for dangerous paths
const dangerousPaths = ["/", "/usr", "/bin", "/etc", "/var", "/opt"];
if (dangerousPaths.includes(dirPath)) {
return { valid: false, error: "Cannot use system directory" };
}
return { valid: true };
}
/**
* Validate and sanitize project directory name for security
* @param {string} projectDir - Project directory name to validate
* @returns {Object} - Validation result with sanitized name
*/
export function validateProjectDirectory(projectDir) {
if (!projectDir) {
return { valid: true, sanitized: null }; // Allow null/undefined for current directory
}
if (typeof projectDir !== "string") {
return { valid: false, error: "Project directory must be a string" };
}
// Normalize Unicode to prevent Unicode normalization attacks
const normalized = projectDir.normalize("NFC");
// Check for null bytes
if (normalized.includes("\0")) {
return {
valid: false,
error: "Project directory cannot contain null bytes",
};
}
// Check for path traversal attempts
if (
normalized.includes("..") ||
normalized.includes("./") ||
normalized.includes(".\\")
) {
return {
valid: false,
error: "Project directory cannot contain path traversal sequences (../, .\\)",
};
}
// Check for absolute paths
if (path.isAbsolute(normalized)) {
return { valid: false, error: "Project directory must be a relative path" };
}
// Check for dangerous characters
// eslint-disable-next-line no-control-regex
const dangerousChars = /[<>:"|?*\x00-\x1f]/;
if (dangerousChars.test(normalized)) {
return {
valid: false,
error: "Project directory contains invalid characters",
};
}
// Check for reserved names on Windows
const reservedNames = [
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
];
const upperName = normalized.toUpperCase();
if (
reservedNames.includes(upperName) ||
reservedNames.some((name) => upperName.startsWith(name + "."))
) {
return {
valid: false,
error: "Project directory cannot use reserved system names",
};
}
// Check length (reasonable limit)
if (normalized.length > 255) {
return {
valid: false,
error: "Project directory name is too long (max 255 characters)",
};
}
// Don't allow names starting/ending with spaces or dots
if (
normalized.startsWith(" ") ||
normalized.endsWith(" ") ||
normalized.startsWith(".") ||
normalized.endsWith(".")
) {
return {
valid: false,
error: "Project directory cannot start or end with spaces or dots",
};
}
return { valid: true, sanitized: normalized };
}
/**
* Validate user choices object
* @param {Object} userChoices - User configuration choices
* @returns {Object} - Validation result
*/
export function validateUserChoices(userChoices) {
const errors = [];
const warnings = [];
if (!userChoices || typeof userChoices !== "object") {
errors.push("User choices must be an object");
return { valid: false, errors, warnings };
}
// Validate framework
const validFrameworks = ["vite", "nextjs"];
if (!validFrameworks.includes(userChoices.framework)) {
errors.push(
`Invalid framework: ${
userChoices.framework
}. Must be one of: ${validFrameworks.join(", ")}`,
);
}
// Validate package manager
const validPackageManagers = ["npm", "yarn"];
if (!validPackageManagers.includes(userChoices.packageManager)) {
errors.push(
`Invalid package manager: ${
userChoices.packageManager
}. Must be one of: ${validPackageManagers.join(", ")}`,
);
}
// Validate styling
const validStyling = ["css", "tailwind", "styled-components"];
if (!validStyling.includes(userChoices.styling)) {
errors.push(
`Invalid styling option: ${
userChoices.styling
}. Must be one of: ${validStyling.join(", ")}`,
);
}
// Validate routing (only applies to frameworks that show the routing step)
const validRouting = ["none", "react-router"];
if (userChoices.routing && !validRouting.includes(userChoices.routing)) {
errors.push(
`Invalid routing option: ${
userChoices.routing
}. Must be one of: ${validRouting.join(", ")}`,
);
}
// Validate deployment
const validDeployment = ["none", "vercel", "netlify"];
if (userChoices.deployment && !validDeployment.includes(userChoices.deployment)) {
errors.push(
`Invalid deployment option: ${
userChoices.deployment
}. Must be one of: ${validDeployment.join(", ")}`,
);
}
// Validate editor
const validEditors = ["vscode", "cursor"];
if (userChoices.openEditor && !validEditors.includes(userChoices.editor)) {
warnings.push(
`Unknown editor: ${
userChoices.editor
}. Supported editors: ${validEditors.join(", ")}`,
);
}
// Note: No need to check for Next.js + routing conflicts since the routing step
// uses shouldShow() to prevent Next.js users from selecting routing options
// Add comprehensive conflict validation
const conflictWarnings = validateChoiceCombinations(userChoices);
warnings.push(...conflictWarnings);
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate choice combinations and return warnings for potentially conflicting options
* @param {Object} userChoices - User configuration choices
* @returns {Array<string>} - Array of warning messages
*/
export function validateChoiceCombinations(userChoices) {
const warnings = [];
// 1. Styling conflicts: Tailwind + styled-components
if (userChoices.styling === "styled-components") {
warnings.push(
"[!] Using styled-components with component-scoped styles. Consider if you need global styling as well.",
);
}
// 2. API + State Management redundancy warnings
if (userChoices.api && userChoices.api.includes("react-query")) {
if (userChoices.stateManagement === "redux") {
warnings.push(
"[!] React Query + Redux detected. React Query handles server state very well - you might not need Redux for server data. Consider using Redux only for client-side state.",
);
}
if (userChoices.stateManagement === "zustand") {
warnings.push(
"[!] React Query + Zustand detected. React Query handles server state - consider using Zustand primarily for client-side application state.",
);
}
}
// 3. Testing framework + framework optimization warnings
if (userChoices.testing === "jest" && userChoices.framework === "vite") {
warnings.push(
"[!] Using Jest with Vite. Vitest is optimized for Vite and provides better performance and zero-config setup.",
);
}
if (userChoices.testing === "vitest" && userChoices.framework === "nextjs") {
warnings.push(
"[!] Using Vitest with Next.js. Jest is better integrated with Next.js and has built-in optimizations.",
);
}
// 4. TypeScript + linting combination advice
if (userChoices.typescript && !userChoices.linting) {
warnings.push(
"[!] TypeScript without ESLint detected. ESLint with TypeScript rules helps catch additional issues beyond type checking.",
);
}
// 5. Framework-specific styling considerations
if (
userChoices.framework === "nextjs" &&
userChoices.styling === "styled-components"
) {
// This is actually handled well by Next.js, so just informational
warnings.push(
"[i] Next.js + styled-components: SSR support is automatically configured.",
);
}
// 6. Complex setup warning
const complexFeatures = [
userChoices.typescript,
userChoices.linting,
userChoices.stateManagement && userChoices.stateManagement !== "none",
userChoices.api && userChoices.api !== "none",
userChoices.testing && userChoices.testing !== "none",
userChoices.styling !== "css",
].filter(Boolean).length;
if (complexFeatures >= 5) {
warnings.push(
"[i] You've selected many advanced features. This creates a powerful setup but may increase complexity for beginners.",
);
}
// 7. Minimal setup suggestion
if (complexFeatures <= 2 && !userChoices.typescript) {
warnings.push(
"[i] Simple setup detected. Consider adding TypeScript and ESLint for better development experience.",
);
}
return warnings;
}
/**
* Validate editor choice
* @param {string} editor - Editor name
* @returns {boolean} - Whether editor is supported
*/
export function isValidEditor(editor) {
const validEditors = ["vscode", "cursor"];
return validEditors.includes(editor);
}
/**
* Validate framework choice
* @param {string} framework - Framework name
* @returns {boolean} - Whether framework is supported
*/
export function isValidFramework(framework) {
const validFrameworks = ["vite", "nextjs"];
return validFrameworks.includes(framework);
}
/**
* Validate package manager choice
* @param {string} packageManager - Package manager name
* @returns {boolean} - Whether package manager is supported
*/
export function isValidPackageManager(packageManager) {
const validPackageManagers = ["npm", "yarn"];
return validPackageManagers.includes(packageManager);
}
/**
* Get validation error message for project name
* @param {Object} validationResult - Result from validateProjectNameInput
* @returns {string} - Formatted error message
*/
export function getProjectNameErrorMessage(validationResult) {
if (validationResult.valid) {
return "";
}
const allIssues = [...validationResult.errors, ...validationResult.warnings];
return `Invalid project name: ${validationResult.name}\n${allIssues
.map((msg) => ` - ${msg}`)
.join("\n")}`;
}