dna-template-cli
Version:
DNA Template CLI v0.3.4 - Enhanced Commands Added (enhanced-create, enhanced-list, enhanced-validate)
829 lines (787 loc) • 28.1 kB
JavaScript
"use strict";
/**
* @fileoverview Project Generator - Creates projects from templates
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectGenerator = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const child_process_1 = require("child_process");
const handlebars_1 = tslib_1.__importDefault(require("handlebars"));
const template_registry_1 = require("./template-registry");
const logger_1 = require("../utils/logger");
const error_handler_1 = require("../utils/error-handler");
class ProjectGenerator {
constructor(config, options, progressTracker) {
this.config = config;
this.options = options;
this.progressTracker = progressTracker;
// Initialize template registry
this.registry = new template_registry_1.TemplateRegistry();
}
async validateConfiguration() {
await this.registry.load();
const template = this.registry.getTemplate(this.config.template);
if (!template) {
throw (0, error_handler_1.createCLIError)(`Template "${this.config.template}" not found`, 'TEMPLATE_NOT_FOUND');
}
// Validate DNA modules compatibility
const incompatibleModules = this.config.dnaModules.filter(moduleId => !template.dnaModules.includes(moduleId));
if (incompatibleModules.length > 0) {
logger_1.logger.warn(`Some DNA modules may not be compatible with this template: ${incompatibleModules.join(', ')}`);
}
// Check system requirements
await this.validateSystemRequirements(template);
}
async validateSystemRequirements(template) {
// Check Node.js version if required
if (template.requirements?.node) {
const nodeVersion = process.version;
logger_1.logger.debug(`Node.js version: ${nodeVersion}, required: ${template.requirements.node}`);
// Note: In a real implementation, you'd use semver to validate
}
// Check available disk space
const stats = await fs_extra_1.default.stat(path_1.default.dirname(this.config.path));
const availableSpace = stats.size || 0;
const requiredSpace = 500 * 1024 * 1024; // 500MB minimum
if (availableSpace > 0 && availableSpace < requiredSpace) {
throw (0, error_handler_1.createCLIError)('Insufficient disk space for template generation', 'INSUFFICIENT_DISK_SPACE', 'Please free up disk space and try again');
}
}
async prepareDirectory() {
if (this.options.dryRun) {
logger_1.logger.debug(`[DRY RUN] Would prepare directory: ${this.config.path}`);
return;
}
// Create backup if directory exists and we're overwriting
if (await fs_extra_1.default.pathExists(this.config.path) && this.options.overwrite) {
if (this.options.backup) {
this.backupPath = `${this.config.path}.backup.${Date.now()}`;
await fs_extra_1.default.copy(this.config.path, this.backupPath);
logger_1.logger.debug(`Created backup at: ${this.backupPath}`);
}
await fs_extra_1.default.remove(this.config.path);
}
// Create project directory
await fs_extra_1.default.ensureDir(this.config.path);
// Set appropriate permissions
if (process.platform !== 'win32') {
await fs_extra_1.default.chmod(this.config.path, 0o755);
}
}
async generateFiles() {
if (this.options.dryRun) {
logger_1.logger.debug(`[DRY RUN] Would generate files from template: ${this.config.template}`);
return;
}
const template = this.registry.getTemplate(this.config.template);
const templatePath = this.getTemplatePath(template);
logger_1.logger.debug(`Generating files from template: ${templatePath}`);
// Check if template directory exists
if (!await fs_extra_1.default.pathExists(templatePath)) {
logger_1.logger.warn(`Template directory not found: ${templatePath}, using built-in generation`);
await this.generateFromTemplate(template);
return;
}
// Copy and process template files
await this.copyAndProcessTemplate(templatePath);
logger_1.logger.debug('Template files generated successfully');
}
getTemplatePath(template) {
// Resolve template path based on the actual template structure
// Templates are organized as: templates/{category}/{template-id}/
const category = this.getTemplateCategory(template.type);
return path_1.default.resolve(process.cwd(), '..', '..', 'templates', category, template.id);
}
getTemplateCategory(templateType) {
// Map template types to directory categories
const categoryMap = {
'ai-saas': 'ai-native',
'ai-mobile': 'ai-native',
'data-visualization': 'performance',
'real-time-collaboration': 'performance',
'high-performance-api': 'performance',
'flutter-universal': 'cross-platform',
'react-native-hybrid': 'cross-platform',
'electron-modern': 'cross-platform',
'tauri-native': 'cross-platform',
'pwa-advanced': 'cross-platform',
'foundation': 'foundation'
};
return categoryMap[templateType] || 'foundation';
}
async generateFromTemplate(template) {
if (this.options.dryRun) {
logger_1.logger.debug(`[DRY RUN] Would generate from template: ${template.name}`);
return;
}
// Generate basic project structure based on template metadata
const files = await this.generateTemplateFiles(template);
for (const [filePath, content] of files) {
const fullPath = path_1.default.join(this.config.path, filePath);
await fs_extra_1.default.ensureDir(path_1.default.dirname(fullPath));
await fs_extra_1.default.writeFile(fullPath, content);
// Set executable permissions for scripts
if (filePath.endsWith('.sh') || filePath.startsWith('scripts/')) {
if (process.platform !== 'win32') {
await fs_extra_1.default.chmod(fullPath, 0o755);
}
}
}
}
async generateTemplateFiles(template) {
const files = new Map();
const variables = this.config.variables;
// Generate package.json
const packageJson = {
name: this.config.name,
version: '0.1.0',
description: `Generated with DNA CLI using ${template.name} template`,
private: true,
scripts: {
dev: this.getDevScript(template.framework),
build: this.getBuildScript(template.framework),
start: this.getStartScript(template.framework),
test: 'jest',
lint: 'eslint . --ext .ts,.tsx,.js,.jsx',
'lint:fix': 'eslint . --ext .ts,.tsx,.js,.jsx --fix',
format: 'prettier --write .',
typecheck: 'tsc --noEmit',
},
dependencies: this.getFrameworkDependencies(template.framework),
devDependencies: this.getDevDependencies(template.framework),
engines: {
node: '>=18.0.0',
npm: '>=8.0.0',
},
};
files.set('package.json', JSON.stringify(packageJson, null, 2));
// Generate README.md
const readmeContent = this.generateReadme(template, variables);
files.set('README.md', readmeContent);
// Generate .gitignore
const gitignoreContent = this.generateGitignore(template.framework);
files.set('.gitignore', gitignoreContent);
// Generate framework-specific files
const frameworkFiles = await this.generateFrameworkFiles(template);
frameworkFiles.forEach((content, filePath) => {
files.set(filePath, content);
});
// Generate DNA module files
for (const moduleId of this.config.dnaModules) {
const moduleFiles = await this.generateDNAModuleFiles(moduleId, template.framework);
moduleFiles.forEach((content, filePath) => {
files.set(filePath, content);
});
}
return files;
}
async copyAndProcessTemplate(templatePath) {
if (this.options.dryRun) {
logger_1.logger.debug(`[DRY RUN] Would copy template from: ${templatePath}`);
return;
}
// Copy template files with variable replacement
await this.copyTemplateRecursive(templatePath, this.config.path);
}
async copyTemplateRecursive(sourcePath, targetPath) {
const items = await fs_extra_1.default.readdir(sourcePath);
for (const item of items) {
const sourceItemPath = path_1.default.join(sourcePath, item);
const targetItemPath = path_1.default.join(targetPath, item);
const stat = await fs_extra_1.default.stat(sourceItemPath);
if (stat.isDirectory()) {
await fs_extra_1.default.ensureDir(targetItemPath);
await this.copyTemplateRecursive(sourceItemPath, targetItemPath);
}
else {
// Process file through Handlebars if it's a template
if (item.endsWith('.hbs')) {
// Template file - process with Handlebars
const templateContent = await fs_extra_1.default.readFile(sourceItemPath, 'utf8');
const compiledTemplate = handlebars_1.default.compile(templateContent);
// Create template context with project variables and DNA module helpers
const templateContext = this.createTemplateContext();
const processedContent = compiledTemplate(templateContext);
// Remove .hbs extension from target file
const finalTargetPath = targetItemPath.replace(/\.hbs$/, '');
await fs_extra_1.default.writeFile(finalTargetPath, processedContent);
logger_1.logger.debug(`Processed template: ${item} -> ${path_1.default.basename(finalTargetPath)}`);
}
else {
// Regular file - copy as-is
await fs_extra_1.default.copy(sourceItemPath, targetItemPath);
}
}
}
}
createTemplateContext() {
// Register Handlebars helpers for DNA modules
this.registerHandlebarsHelpers();
return {
project: {
name: this.config.name,
kebabName: this.config.name.toLowerCase().replace(/\s+/g, '-'),
camelName: this.config.name.replace(/\s+(.)/g, (_, char) => char.toUpperCase()).replace(/^\w/, c => c.toLowerCase()),
pascalName: this.config.name.replace(/\s+(.)/g, (_, char) => char.toUpperCase()).replace(/^\w/, c => c.toUpperCase()),
description: this.config.variables.description || `A project generated with DNA CLI`,
framework: this.config.framework,
type: this.config.template
},
dnaModules: this.config.dnaModules,
variables: this.config.variables,
hasModule: (moduleId) => this.config.dnaModules.includes(moduleId),
framework: this.config.framework,
timestamp: new Date().toISOString()
};
}
registerHandlebarsHelpers() {
// Helper to check if a DNA module is included
handlebars_1.default.registerHelper('hasModule', function (moduleId) {
return this.dnaModules && this.dnaModules.includes(moduleId);
});
// Helper for conditional module dependencies
handlebars_1.default.registerHelper('ifModule', function (moduleId, options) {
if (this.dnaModules && this.dnaModules.includes(moduleId)) {
return options.fn(this);
}
else {
return options.inverse(this);
}
});
// Helper for framework-specific code
handlebars_1.default.registerHelper('ifFramework', function (frameworkName, options) {
if (this.framework === frameworkName) {
return options.fn(this);
}
else {
return options.inverse(this);
}
});
// Helper for case transformations
handlebars_1.default.registerHelper('kebabCase', function (str) {
return str.toLowerCase().replace(/\s+/g, '-');
});
handlebars_1.default.registerHelper('camelCase', function (str) {
return str.replace(/\s+(.)/g, (_, char) => char.toUpperCase()).replace(/^\w/, c => c.toLowerCase());
});
handlebars_1.default.registerHelper('pascalCase', function (str) {
return str.replace(/\s+(.)/g, (_, char) => char.toUpperCase()).replace(/^\w/, c => c.toUpperCase());
});
}
async installDependencies() {
if (this.options.dryRun) {
logger_1.logger.debug(`[DRY RUN] Would install dependencies using ${this.config.packageManager}`);
return;
}
const packageManager = this.config.packageManager || 'npm';
const installCommand = this.getInstallCommand(packageManager);
const [cmd, ...cmdArgs] = installCommand;
const cwd = this.config.path;
if (!cwd) {
throw new Error('Project path is not defined');
}
if (!cmd) {
throw new Error('Install command is not defined');
}
await this.runCommand(cmd, cmdArgs, {
cwd,
stdio: this.options.progress ? 'pipe' : 'inherit',
});
}
async initializeGit() {
if (this.options.dryRun) {
logger_1.logger.debug('[DRY RUN] Would initialize git repository');
return;
}
// Initialize git repository
await this.runCommand('git', ['init'], { cwd: this.config.path });
// Add all files
await this.runCommand('git', ['add', '.'], { cwd: this.config.path });
// Create initial commit
await this.runCommand('git', ['commit', '-m', 'Initial commit from DNA CLI'], {
cwd: this.config.path
});
}
async finalize() {
if (this.options.dryRun) {
logger_1.logger.debug('[DRY RUN] Would finalize project setup');
return;
}
// Generate project configuration file
const dnaConfig = {
template: this.config.template,
framework: this.config.framework,
modules: this.config.dnaModules,
generated: new Date().toISOString(),
version: '0.1.0',
};
const configPath = path_1.default.join(this.config.path, 'dna.config.json');
await fs_extra_1.default.writeJSON(configPath, dnaConfig, { spaces: 2 });
// Clean up temporary files
await this.cleanupTempFiles();
}
async rollback() {
try {
// Remove created directory if it exists
if (await fs_extra_1.default.pathExists(this.config.path)) {
await fs_extra_1.default.remove(this.config.path);
}
// Restore backup if it exists
if (this.backupPath && await fs_extra_1.default.pathExists(this.backupPath)) {
await fs_extra_1.default.move(this.backupPath, this.config.path);
logger_1.logger.info('Restored backup directory');
}
}
catch (error) {
logger_1.logger.warn('Failed to rollback changes:', error);
}
}
// Helper methods
getDevScript(framework) {
switch (framework) {
case 'nextjs': return 'next dev';
case 'react-native': return 'react-native start';
case 'flutter': return 'flutter run';
default: return 'npm run start';
}
}
getBuildScript(framework) {
switch (framework) {
case 'nextjs': return 'next build';
case 'react-native': return 'react-native bundle --platform android --dev false';
case 'flutter': return 'flutter build';
default: return 'tsc';
}
}
getStartScript(framework) {
switch (framework) {
case 'nextjs': return 'next start';
case 'react-native': return 'react-native run-android';
case 'flutter': return 'flutter run --release';
default: return 'node dist/index.js';
}
}
getFrameworkDependencies(framework) {
const deps = {
nextjs: {
'next': '^14.0.0',
'react': '^18.0.0',
'react-dom': '^18.0.0',
},
'react-native': {
'react': '^18.0.0',
'react-native': '^0.72.0',
},
flutter: {},
};
return deps[framework] || {};
}
getDevDependencies(framework) {
const common = {
'@types/node': '^20.0.0',
'typescript': '^5.3.0',
'eslint': '^8.57.0',
'prettier': '^3.2.0',
'jest': '^29.0.0',
'@types/jest': '^29.0.0',
};
const frameworkSpecific = {
nextjs: {
'@types/react': '^18.0.0',
'@types/react-dom': '^18.0.0',
'eslint-config-next': '^14.0.0',
},
'react-native': {
'@types/react': '^18.0.0',
'@react-native/metro-config': '^0.72.0',
'metro-react-native-babel-preset': '^0.76.0',
},
};
return {
...common,
...(frameworkSpecific[framework] || {}),
};
}
async generateFrameworkFiles(template) {
const files = new Map();
switch (template.framework) {
case 'nextjs':
files.set('next.config.js', this.generateNextConfig());
files.set('tsconfig.json', this.generateTSConfig());
files.set('tailwind.config.js', this.generateTailwindConfig());
files.set('src/pages/index.tsx', this.generateNextIndexPage());
break;
case 'react-native':
files.set('metro.config.js', this.generateMetroConfig());
files.set('babel.config.js', this.generateBabelConfig());
files.set('src/App.tsx', this.generateRNApp());
break;
case 'flutter':
files.set('pubspec.yaml', this.generatePubspecYaml());
files.set('lib/main.dart', this.generateFlutterMain());
break;
}
return files;
}
async generateDNAModuleFiles(moduleId, framework) {
const files = new Map();
// This would integrate with the actual DNA module system
// For now, just create placeholder files
files.set(`src/modules/${moduleId}.ts`, `// ${moduleId} module configuration\nexport const ${moduleId}Config = {};\n`);
return files;
}
generateReadme(template, variables) {
return `# ${variables['projectName'] || this.config.name}
Generated with DNA CLI using the **${template.name}** template.
## Features
${template.features.map((feature) => `- ${feature}`).join('\n')}
## Getting Started
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Start development server:
\`\`\`bash
npm run dev
\`\`\`
3. Open your browser and navigate to the development URL
## DNA Modules
This project includes the following DNA modules:
${this.config.dnaModules.map(module => `- \`${module}\``).join('\n')}
## Scripts
- \`npm run dev\` - Start development server
- \`npm run build\` - Build for production
- \`npm run start\` - Start production server
- \`npm test\` - Run tests
- \`npm run lint\` - Lint code
- \`npm run format\` - Format code
## Documentation
For more information about this template and DNA CLI, visit:
- [DNA CLI Documentation](https://github.com/dna-templates/cli)
- [Template Documentation](https://templates.dna-cli.com/${template.id})
Generated on ${new Date().toLocaleDateString()}
`;
}
generateGitignore(framework) {
const common = `# Dependencies
node_modules/
.pnp
.pnp.js
# Production builds
dist/
build/
out/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary folders
tmp/
temp/
`;
const frameworkSpecific = {
nextjs: `
# Next.js
.next/
next-env.d.ts
`,
'react-native': `
# React Native
.expo/
.expo-shared/
android/app/build/
ios/build/
*.ipa
*.apk
`,
flutter: `
# Flutter
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
`,
};
return common + (frameworkSpecific[framework] || '');
}
// Framework-specific file generators
generateNextConfig() {
return `/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
typescript: {
ignoreBuildErrors: false,
},
eslint: {
ignoreDuringBuilds: false,
},
};
module.exports = nextConfig;
`;
}
generateTSConfig() {
return `{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
`;
}
generateTailwindConfig() {
return `/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
};
`;
}
generateNextIndexPage() {
return `import { NextPage } from 'next';
const HomePage: NextPage = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md mx-auto text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Welcome to ${this.config.name}
</h1>
<p className="text-gray-600 mb-8">
Generated with DNA CLI
</p>
<div className="space-y-4">
<button className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">
Get Started
</button>
</div>
</div>
</div>
);
};
export default HomePage;
`;
}
generateMetroConfig() {
return `const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const config = {};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
`;
}
generateBabelConfig() {
return `module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
`;
}
generateRNApp() {
return `import React from 'react';
import {SafeAreaView, Text, StyleSheet, View} from 'react-native';
const App: React.FC = () => {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Welcome to ${this.config.name}</Text>
<Text style={styles.subtitle}>Generated with DNA CLI</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
});
export default App;
`;
}
generatePubspecYaml() {
return `name: ${this.config.name.replace(/-/g, '_')}
description: Generated with DNA CLI
version: 1.0.0+1
environment:
sdk: '>=3.2.0 <4.0.0'
flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
`;
}
generateFlutterMain() {
return `import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '${this.config.name}',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: '${this.config.name}'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Welcome to ${this.config.name}',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Text(
'Generated with DNA CLI',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
);
}
}
`;
}
getInstallCommand(packageManager) {
switch (packageManager) {
case 'yarn': return ['yarn', 'install'];
case 'pnpm': return ['pnpm', 'install'];
case 'bun': return ['bun', 'install'];
default: return ['npm', 'install'];
}
}
async runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(command, args, {
cwd: options.cwd || process.cwd(),
stdio: options.stdio || 'inherit',
shell: process.platform === 'win32',
});
child.on('close', (code) => {
if (code === 0) {
resolve();
}
else {
reject(new Error(`Command failed with exit code ${code}: ${command} ${args.join(' ')}`));
}
});
child.on('error', (error) => {
reject(error);
});
});
}
async cleanupTempFiles() {
// Clean up any temporary files created during generation
// This is a placeholder for future cleanup logic
}
}
exports.ProjectGenerator = ProjectGenerator;
//# sourceMappingURL=project-generator.js.map