UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

349 lines (348 loc) 13.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = require("path"); /** * Known configuration keys based on LtConfig interface * Used for validation against unknown keys */ const KNOWN_KEYS = { commands: { blocks: { add: { noConfirm: 'boolean' }, }, cli: { create: { author: 'string', link: 'boolean', noConfirm: 'boolean' }, }, components: { add: { noConfirm: 'boolean' }, }, config: { init: { noConfirm: 'boolean' }, }, deployment: { domain: 'string', gitHub: 'boolean', gitLab: 'boolean', noConfirm: 'boolean', prodRunner: 'string', testRunner: 'string', }, directus: { dockerSetup: { database: ['postgres', 'mysql', 'sqlite'], name: 'string', noConfirm: 'boolean', port: 'number', version: 'string', }, remove: { noConfirm: 'boolean' }, typegen: { noConfirm: 'boolean', output: 'string', token: 'string', url: 'string' }, }, frontend: { angular: { branch: 'string', copy: 'string', link: 'string', localize: 'boolean', noConfirm: 'boolean' }, nuxt: { branch: 'string', copy: 'string', link: 'string' }, }, fullstack: { apiBranch: 'string', apiCopy: 'string', apiLink: 'string', apiMode: ['Rest', 'GraphQL', 'Both'], frameworkMode: ['npm', 'vendor'], frontend: ['angular', 'nuxt'], frontendBranch: 'string', frontendCopy: 'string', frontendFrameworkMode: ['npm', 'vendor'], frontendLink: 'string', git: 'boolean', gitLink: 'string', noConfirm: 'boolean', }, git: { baseBranch: 'string', clean: { noConfirm: 'boolean' }, clear: { noConfirm: 'boolean' }, create: { base: 'string', noConfirm: 'boolean' }, defaultBranch: 'string', forcePull: { noConfirm: 'boolean' }, get: { mode: ['hard'], noConfirm: 'boolean' }, noConfirm: 'boolean', rebase: { base: 'string', noConfirm: 'boolean' }, rename: { noConfirm: 'boolean' }, reset: { noConfirm: 'boolean' }, squash: { author: 'string', base: 'string', noConfirm: 'boolean' }, undo: { noConfirm: 'boolean' }, update: { skipInstall: 'boolean' }, }, npm: { reinit: { noConfirm: 'boolean', update: 'boolean' }, }, server: { addProp: { skipLint: 'boolean' }, create: { apiMode: ['Rest', 'GraphQL', 'Both'], author: 'string', branch: 'string', controller: ['Rest', 'GraphQL', 'Both', 'auto'], copy: 'string', description: 'string', git: 'boolean', link: 'string', noConfirm: 'boolean', }, module: { controller: ['Rest', 'GraphQL', 'Both', 'auto'], noConfirm: 'boolean', skipLint: 'boolean', }, object: { skipLint: 'boolean' }, permissions: { console: 'boolean', failOnWarnings: 'boolean', format: ['md', 'json', 'html'], noConfirm: 'boolean', open: 'boolean', output: 'string', path: 'string', }, }, tools: { crawl: { concurrency: 'number', depth: 'number|all', includeImages: 'boolean', includeSitemap: 'boolean', maxPages: 'number', noConfirm: 'boolean', out: 'string', prune: 'boolean', renderJs: 'boolean', selector: 'string', timeout: 'number', }, noConfirm: 'boolean', }, typescript: { create: { author: 'string', noConfirm: 'boolean', updatePackages: 'boolean' }, }, }, defaults: { apiMode: ['Rest', 'GraphQL', 'Both'], author: 'string', baseBranch: 'string', controller: ['Rest', 'GraphQL', 'Both', 'auto'], domain: 'string', noConfirm: 'boolean', packageManager: ['npm', 'pnpm', 'yarn'], skipInstall: 'boolean', skipLint: 'boolean', }, meta: { // meta allows additional properties _additionalProperties: true, description: 'string', name: 'string', tags: 'array', version: 'string', }, }; /** * Validate a config object against known keys */ function validateConfig(config, knownKeys, path = '') { const result = { errors: [], warnings: [] }; if (typeof config !== 'object' || config === null) { return result; } for (const key of Object.keys(config)) { const currentPath = path ? `${path}.${key}` : key; const value = config[key]; const expectedType = knownKeys[key]; // Skip $schema key (used for IDE support) if (key === '$schema') { continue; } // Check if key is known if (expectedType === undefined) { // Check if additional properties are allowed if (knownKeys._additionalProperties) { continue; } result.warnings.push(`Unknown key: ${currentPath}`); continue; } // Validate type if (typeof expectedType === 'string') { // Simple type check. `'a|b'` means union (e.g. "number|all"). if (expectedType.includes('|')) { const tokens = expectedType.split('|').map((t) => t.trim()); const ok = tokens.some((token) => { if (token === 'string') return typeof value === 'string'; if (token === 'number') return typeof value === 'number'; if (token === 'boolean') return typeof value === 'boolean'; if (token === 'array') return Array.isArray(value); // Everything else is treated as a string literal enum member. return value === token; }); if (!ok) { result.errors.push(`${currentPath}: expected ${tokens.join(' | ')}, got ${typeof value}`); } } else if (expectedType === 'string' && typeof value !== 'string') { result.errors.push(`${currentPath}: expected string, got ${typeof value}`); } else if (expectedType === 'boolean' && typeof value !== 'boolean') { result.errors.push(`${currentPath}: expected boolean, got ${typeof value}`); } else if (expectedType === 'number' && typeof value !== 'number') { result.errors.push(`${currentPath}: expected number, got ${typeof value}`); } else if (expectedType === 'array' && !Array.isArray(value)) { result.errors.push(`${currentPath}: expected array, got ${typeof value}`); } } else if (Array.isArray(expectedType)) { // Enum check if (!expectedType.includes(value)) { result.errors.push(`${currentPath}: expected one of [${expectedType.join(', ')}], got "${value}"`); } } else if (typeof expectedType === 'object') { // Nested object - recurse if (typeof value !== 'object' || value === null) { result.errors.push(`${currentPath}: expected object, got ${typeof value}`); } else { const nested = validateConfig(value, expectedType, currentPath); result.errors.push(...nested.errors); result.warnings.push(...nested.warnings); } } } return result; } /** * Validate lt.config file */ const ValidateCommand = { alias: ['v'], description: 'Validate config file', hidden: false, name: 'validate', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { const { filesystem, print: { error, info, success, warning }, } = toolbox; info('Validating lt.config...'); info(''); // Find config files const cwd = filesystem.cwd(); const configFiles = ['lt.config.json', 'lt.config.yaml', 'lt.config']; const foundFiles = []; for (const file of configFiles) { if (filesystem.exists((0, path_1.join)(cwd, file))) { foundFiles.push(file); } } if (foundFiles.length === 0) { error('No lt.config file found in current directory.'); info(''); info('Run "lt config init" to create a configuration file.'); return 'config validate: no file'; } // Warn about multiple files if (foundFiles.length > 1) { warning(`Multiple config files found: ${foundFiles.join(', ')}`); warning(`Only ${foundFiles[0]} will be used (highest priority).`); info(''); } const configFile = foundFiles[0]; const configPath = (0, path_1.join)(cwd, configFile); // Try to parse the config let parsedConfig; try { const content = filesystem.read(configPath); if (!content || content.trim() === '') { error(`${configFile}: File is empty`); return 'config validate: empty file'; } if (configFile.endsWith('.json')) { parsedConfig = JSON.parse(content); } else if (configFile.endsWith('.yaml')) { const yaml = require('js-yaml'); parsedConfig = yaml.load(content); } else { // Auto-detect try { parsedConfig = JSON.parse(content); } catch (_a) { const yaml = require('js-yaml'); parsedConfig = yaml.load(content); } } } catch (e) { error(`${configFile}: Parse error`); error(` ${e.message}`); return 'config validate: parse error'; } success(`${configFile}: Syntax OK`); // Validate against schema const validation = validateConfig(parsedConfig, KNOWN_KEYS); // Report errors if (validation.errors.length > 0) { info(''); error('Errors:'); for (const err of validation.errors) { error(` - ${err}`); } } // Report warnings if (validation.warnings.length > 0) { info(''); warning('Warnings:'); for (const warn of validation.warnings) { warning(` - ${warn}`); } } // Summary info(''); if (validation.errors.length === 0 && validation.warnings.length === 0) { success('Configuration is valid!'); } else if (validation.errors.length === 0) { success(`Configuration is valid with ${validation.warnings.length} warning(s).`); } else { error(`Configuration has ${validation.errors.length} error(s) and ${validation.warnings.length} warning(s).`); } // Show schema hint info(''); info('Tip: Add "$schema" to your config for IDE autocomplete:'); if (configFile.endsWith('.json')) { info(' "$schema": "./node_modules/@lenne.tech/cli/schemas/lt.config.schema.json"'); } else { info(' For YAML files, use a JSON Schema-aware editor with:'); info(' # yaml-language-server: $schema=./node_modules/@lenne.tech/cli/schemas/lt.config.schema.json'); } if (!toolbox.parameters.options.fromGluegunMenu) { process.exit(); } return validation.errors.length === 0 ? 'config validate: valid' : 'config validate: invalid'; }), }; exports.default = ValidateCommand;