@codehance/rapid-stack
Version:
A modern full-stack development toolkit for rapid application development
1,172 lines (1,002 loc) • 42 kB
JavaScript
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { handlePrompt, getConfigField, validateRequiredFields } = require('../../lib/utils');
const BaseGenerator = require('../base');
module.exports = class extends BaseGenerator {
constructor(args, opts) {
super(args, opts);
// Add debug option
this.option('debug', {
desc: 'Enable debug mode',
type: Boolean,
default: false
});
// Add progress indicator properties
this._spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
this._spinnerIndex = 0;
this._spinnerInterval = null;
}
_startSpinner(message) {
this._spinnerIndex = 0;
this._spinnerInterval = setInterval(() => {
process.stdout.write(`\r${this._spinnerChars[this._spinnerIndex]} ${message}`);
this._spinnerIndex = (this._spinnerIndex + 1) % this._spinnerChars.length;
}, 80);
}
_stopSpinner() {
if (this._spinnerInterval) {
clearInterval(this._spinnerInterval);
process.stdout.write('\r\x1b[K'); // Clear the line
}
}
async prompting() {
validateRequiredFields();
// Get app name from config, defaulting to 'frontend' if not found
const appName = getConfigField('config.app_name', 'frontend');
const answers = await handlePrompt(this, [{
type: 'input',
name: 'appName',
message: 'What would you like to name your app?',
default: appName,
validate: (input) => {
if (!input) {
return 'App name cannot be empty';
}
if (!/^[a-z0-9-]+$/.test(input)) {
return 'App name can only contain lowercase letters, numbers, and hyphens';
}
return true;
}
}]);
this.answers = {
projectName: 'frontend', // Always use 'frontend' as the project name
appName: answers.appName // Store the app name separately for wrangler
};
// Set flag indicating prompting is complete
this._isPromptingComplete = true;
}
async configuring() {
const { projectName } = this.answers;
const projectPath = path.join(process.cwd(), projectName);
// Check if project exists
if (fs.existsSync(projectPath)) {
const { action } = await handlePrompt(this, [{
type: 'list',
name: 'action',
message: 'Project directory already exists. What would you like to do?',
choices: [
{ name: 'Update existing project', value: 'update' },
{ name: 'Cancel installation', value: 'cancel' }
]
}]);
if (action === 'cancel') {
this.log('Installation cancelled.');
process.exit(0);
}
}
}
checkPrerequisites() {
try {
// Check if Node.js is installed
const nodeVersion = execSync('node -v').toString();
this.log('Node.js version:', nodeVersion.trim());
// Check if npm is installed
const npmVersion = execSync('npm -v').toString();
this.log('npm version:', npmVersion.trim());
// Check if Ionic CLI is installed
try {
const ionicVersion = execSync('ionic -v').toString();
this.log('Ionic CLI version:', ionicVersion.trim());
} catch (error) {
this.log('Installing Ionic CLI...');
execSync('npm install -g @ionic/cli');
this.log('Ionic CLI has been installed successfully.');
}
} catch (error) {
this.log('Error: Node.js is not installed. Please install Node.js before continuing.');
process.exit(1);
}
}
async install() {
const { projectName } = this.answers;
const projectPath = path.join(process.cwd(), projectName);
const projectExists = fs.existsSync(projectPath);
try {
if (!projectExists) {
this.log(`Creating new Ionic Angular project in current directory...`);
// Start the spinner
this._startSpinner('Creating Ionic project...');
// Run ionic start command
execSync(`ionic start ${projectName} blank --type=angular --capacitor --no-git`, { stdio: 'inherit' });
// Stop the spinner
this._stopSpinner();
this.log('✓ Ionic project created successfully');
} else {
this.log(`Project "${projectName}" already exists. Updating dependencies...`);
}
// Change to the project directory
process.chdir(projectPath);
// Read the existing package.json
const existingPackageJson = JSON.parse(
fs.readFileSync('package.json', 'utf8')
);
// Get the Capacitor core version
const capacitorCoreVersion = existingPackageJson.dependencies['@capacitor/core'];
this.log(`Detected Capacitor core version: ${capacitorCoreVersion}`);
// Read the template package.json
const templatePackageJson = JSON.parse(
this.fs.read(this.templatePath('package.json.erb'))
);
// Extract major version from Capacitor core
const majorVersion = parseInt(capacitorCoreVersion.split('.')[0].replace('^', ''));
// Update Capacitor platform versions to match the core version
templatePackageJson.dependencies['@capacitor/android'] = `^${majorVersion}.0.0`;
templatePackageJson.dependencies['@capacitor/ios'] = `^${majorVersion}.0.0`;
// Check if dependencies need to be updated
const needsUpdate = this._checkDependenciesNeedUpdate(
existingPackageJson,
templatePackageJson
);
if (needsUpdate) {
// Merge dependencies
existingPackageJson.dependencies = {
...existingPackageJson.dependencies,
...templatePackageJson.dependencies
};
// Merge devDependencies
existingPackageJson.devDependencies = {
...existingPackageJson.devDependencies,
...templatePackageJson.devDependencies
};
// Write the updated package.json
fs.writeFileSync('package.json', JSON.stringify(existingPackageJson, null, 2));
// Install the new dependencies
this.log('Installing/updating dependencies...');
execSync('npm install', { stdio: 'inherit' });
} else {
this.log('All required dependencies are already installed.');
}
// Handle .gitignore
await this._setupGitignore(projectPath);
// Handle capacitor.config.ts
await this._setupCapacitorConfig(projectPath);
// Handle README.md
await this._setupReadme(projectPath);
// Handle interceptors
await this._setupInterceptors(projectPath);
// Handle error service
await this._setupErrorService(projectPath);
// Handle auth service
await this._setupAuthService(projectPath);
// Handle storage service
await this._setupStorageService(projectPath);
// Handle logging service
await this._setupLoggingService(projectPath);
// Handle routes service
await this._setupRoutesService(projectPath);
// Handle toast service
await this._setupToastService(projectPath);
// Handle GraphQL service
await this._setupGraphQLService(projectPath);
// Handle shared directory
await this._setupSharedDirectory(projectPath);
// Handle app component files
await this._setupAppComponent(projectPath);
// Handle theme variables
await this._setupThemeVariables(projectPath);
// Handle main.ts
await this._setupMainTs(projectPath);
// Handle Apollo config
await this._setupApolloConfig(projectPath);
// Handle auth guard
await this._setupAuthGuard(projectPath);
// Update angular.json
await this._updateAngularJson(projectPath);
// Handle Dockerfile
await this._setupDockerfile(projectPath);
// Handle logging mutation
await this._setupLoggingMutation(projectPath);
// Handle environment files
await this._setupEnvironmentFiles(projectPath);
// Handle wrangler.jsonc
await this._setupWranglerConfig(projectPath);
this._printSummary(process.cwd(), projectName, projectExists);
} catch (error) {
this.log('Error processing project:', error.message);
process.exit(1);
}
}
async _setupGitignore(projectPath) {
const gitignorePath = path.join(projectPath, '.gitignore');
// Check if .gitignore exists
if (fs.existsSync(gitignorePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: '.gitignore file already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping .gitignore setup.');
return;
}
}
// Read the template content and write it directly
const templateContent = this.fs.read(this.templatePath('gitignore.erb'));
fs.writeFileSync(gitignorePath, templateContent);
this.log('✓ Added .gitignore file');
}
async _setupCapacitorConfig(projectPath) {
const configPath = path.join(projectPath, 'capacitor.config.ts');
// Check if config file exists
if (fs.existsSync(configPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'capacitor.config.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping capacitor.config.ts setup.');
return;
}
}
// Read the template content and write it directly
const templateContent = this.fs.read(this.templatePath('capacitor.config.ts.erb'));
const interpolatedContent = templateContent.replace(/<%= projectName %>/g, this.answers.projectName);
fs.writeFileSync(configPath, interpolatedContent);
this.log('✓ Updated capacitor.config.ts');
}
async _setupReadme(projectPath) {
const readmePath = path.join(projectPath, 'README.md');
// Check if README.md exists
if (fs.existsSync(readmePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'README.md already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping README.md setup.');
return;
}
}
// Read the template content and write it directly
const templateContent = this.fs.read(this.templatePath('README.md.erb'));
fs.writeFileSync(readmePath, templateContent);
this.log('✓ Added README.md with build instructions');
}
async _setupInterceptors(projectPath) {
const interceptorsDir = path.join(projectPath, 'src', 'app', 'interceptors');
// Create interceptors directory if it doesn't exist
if (!fs.existsSync(interceptorsDir)) {
fs.mkdirSync(interceptorsDir, { recursive: true });
}
// Handle http-error.interceptor.ts
const interceptorPath = path.join(interceptorsDir, 'http-error.interceptor.ts');
if (fs.existsSync(interceptorPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'http-error.interceptor.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping http-error.interceptor.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/interceptors/http-error.interceptor.ts.erb'));
fs.writeFileSync(interceptorPath, templateContent);
this.log('✓ Added http-error.interceptor.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/interceptors/http-error.interceptor.ts.erb'));
fs.writeFileSync(interceptorPath, templateContent);
this.log('✓ Added http-error.interceptor.ts');
}
// Handle http-error.interceptor.spec.ts
const specPath = path.join(interceptorsDir, 'http-error.interceptor.spec.ts');
if (fs.existsSync(specPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'http-error.interceptor.spec.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping http-error.interceptor.spec.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/interceptors/http-error.interceptor.spec.ts.erb'));
fs.writeFileSync(specPath, templateContent);
this.log('✓ Added http-error.interceptor.spec.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/interceptors/http-error.interceptor.spec.ts.erb'));
fs.writeFileSync(specPath, templateContent);
this.log('✓ Added http-error.interceptor.spec.ts');
}
}
async _setupErrorService(projectPath) {
const errorsDir = path.join(projectPath, 'src', 'app', 'services', 'errors');
// Create errors directory if it doesn't exist
if (!fs.existsSync(errorsDir)) {
fs.mkdirSync(errorsDir, { recursive: true });
}
// Handle error.service.ts
const servicePath = path.join(errorsDir, 'error.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'error.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping error.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/errors/error.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added error.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/errors/error.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added error.service.ts');
}
// Handle error.service.spec.ts
const specPath = path.join(errorsDir, 'error.service.spec.ts');
if (fs.existsSync(specPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'error.service.spec.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping error.service.spec.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/errors/error.service.spec.ts.erb'));
fs.writeFileSync(specPath, templateContent);
this.log('✓ Added error.service.spec.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/errors/error.service.spec.ts.erb'));
fs.writeFileSync(specPath, templateContent);
this.log('✓ Added error.service.spec.ts');
}
}
async _setupAuthService(projectPath) {
const authDir = path.join(projectPath, 'src', 'app', 'services', 'auth');
// Create auth directory if it doesn't exist
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
// Handle auth.service.ts
const servicePath = path.join(authDir, 'auth.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'auth.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping auth.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/auth/auth.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added auth.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/auth/auth.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added auth.service.ts');
}
}
async _setupStorageService(projectPath) {
const initDir = path.join(projectPath, 'src', 'app', 'services', 'init');
// Create init directory if it doesn't exist
if (!fs.existsSync(initDir)) {
fs.mkdirSync(initDir, { recursive: true });
}
// Handle storage.service.ts
const servicePath = path.join(initDir, 'storage.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'storage.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping storage.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/init/storage.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added storage.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/init/storage.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added storage.service.ts');
}
}
async _setupLoggingService(projectPath) {
const logsDir = path.join(projectPath, 'src', 'app', 'services', 'logs');
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Handle logging.service.ts
const servicePath = path.join(logsDir, 'logging.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'logging.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping logging.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/logs/logging.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added logging.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/logs/logging.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added logging.service.ts');
}
}
async _setupRoutesService(projectPath) {
const routesDir = path.join(projectPath, 'src', 'app', 'services', 'routes');
// Create routes directory if it doesn't exist
if (!fs.existsSync(routesDir)) {
fs.mkdirSync(routesDir, { recursive: true });
}
// Handle routes.service.ts
const servicePath = path.join(routesDir, 'routes.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'routes.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping routes.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/routes/routes.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added routes.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/routes/routes.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added routes.service.ts');
}
}
async _setupToastService(projectPath) {
const uiDir = path.join(projectPath, 'src', 'app', 'services', 'ui');
// Create ui directory if it doesn't exist
if (!fs.existsSync(uiDir)) {
fs.mkdirSync(uiDir, { recursive: true });
}
// Handle toast.service.ts
const servicePath = path.join(uiDir, 'toast.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'toast.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping toast.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/ui/toast.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added toast.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/ui/toast.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added toast.service.ts');
}
}
async _setupGraphQLService(projectPath) {
const servicesDir = path.join(projectPath, 'src', 'app', 'services');
// Handle graphql.service.ts
const servicePath = path.join(servicesDir, 'graphql.service.ts');
if (fs.existsSync(servicePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'graphql.service.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping graphql.service.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/graphql.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added graphql.service.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/services/graphql.service.ts.erb'));
fs.writeFileSync(servicePath, templateContent);
this.log('✓ Added graphql.service.ts');
}
}
async _setupSharedDirectory(projectPath) {
const sharedDir = path.join(projectPath, 'src', 'app', 'shared');
const templateSharedDir = this.templatePath('src/app/shared');
// Check if shared directory exists
if (fs.existsSync(sharedDir)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'shared directory already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping shared directory setup.');
return;
}
}
// Create shared directory if it doesn't exist
if (!fs.existsSync(sharedDir)) {
fs.mkdirSync(sharedDir, { recursive: true });
}
// Function to recursively copy directory contents
const copyDirectory = (source, target) => {
if (!fs.existsSync(target)) {
fs.mkdirSync(target, { recursive: true });
}
const files = fs.readdirSync(source);
for (const file of files) {
const sourcePath = path.join(source, file);
const targetPath = path.join(target, file);
if (fs.statSync(sourcePath).isDirectory()) {
copyDirectory(sourcePath, targetPath);
} else {
const templateContent = this.fs.read(sourcePath);
fs.writeFileSync(targetPath, templateContent);
}
}
};
// Copy the entire shared directory structure
copyDirectory(templateSharedDir, sharedDir);
this.log('✓ Added shared directory structure with all contents');
}
async _setupAppComponent(projectPath) {
const appDir = path.join(projectPath, 'src', 'app');
const files = ['app.component.html', 'app.component.scss', 'app.component.ts'];
for (const file of files) {
const targetPath = path.join(appDir, file);
const templatePath = this.templatePath(`src/app/${file}.erb`);
if (fs.existsSync(targetPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: `${file} already exists. Do you want to overwrite it?`,
default: true
}]);
if (!confirmOverwrite) {
this.log(`Skipping ${file} setup.`);
continue;
}
}
const templateContent = this.fs.read(templatePath);
fs.writeFileSync(targetPath, templateContent);
this.log(`✓ Added ${file}`);
}
}
async _setupThemeVariables(projectPath) {
const themeDir = path.join(projectPath, 'src', 'theme');
// Create theme directory if it doesn't exist
if (!fs.existsSync(themeDir)) {
fs.mkdirSync(themeDir, { recursive: true });
}
// Handle variables.scss
const variablesPath = path.join(themeDir, 'variables.scss');
if (fs.existsSync(variablesPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'variables.scss already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping variables.scss setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/theme/variables.scss.erb'));
fs.writeFileSync(variablesPath, templateContent);
this.log('✓ Added variables.scss');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/theme/variables.scss.erb'));
fs.writeFileSync(variablesPath, templateContent);
this.log('✓ Added variables.scss');
}
}
async _setupMainTs(projectPath) {
const srcDir = path.join(projectPath, 'src');
// Handle main.ts
const mainPath = path.join(srcDir, 'main.ts');
if (fs.existsSync(mainPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'main.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping main.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/main.ts.erb'));
fs.writeFileSync(mainPath, templateContent);
this.log('✓ Added main.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/main.ts.erb'));
fs.writeFileSync(mainPath, templateContent);
this.log('✓ Added main.ts');
}
}
async _setupApolloConfig(projectPath) {
const graphqlDir = path.join(projectPath, 'src', 'app', 'graphql');
// Create graphql directory if it doesn't exist
if (!fs.existsSync(graphqlDir)) {
fs.mkdirSync(graphqlDir, { recursive: true });
}
// Handle apollo.config.ts
const configPath = path.join(graphqlDir, 'apollo.config.ts');
if (fs.existsSync(configPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'apollo.config.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping apollo.config.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/graphql/apollo.config.ts.erb'));
fs.writeFileSync(configPath, templateContent);
this.log('✓ Added apollo.config.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/graphql/apollo.config.ts.erb'));
fs.writeFileSync(configPath, templateContent);
this.log('✓ Added apollo.config.ts');
}
}
async _setupAuthGuard(projectPath) {
const guardsDir = path.join(projectPath, 'src', 'app', 'auth', 'guards');
// Create guards directory if it doesn't exist
if (!fs.existsSync(guardsDir)) {
fs.mkdirSync(guardsDir, { recursive: true });
}
// Handle auth.guard.ts
const guardPath = path.join(guardsDir, 'auth.guard.ts');
if (fs.existsSync(guardPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'auth.guard.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping auth.guard.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/auth/guards/auth.guard.ts.erb'));
fs.writeFileSync(guardPath, templateContent);
this.log('✓ Added auth.guard.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/auth/guards/auth.guard.ts.erb'));
fs.writeFileSync(guardPath, templateContent);
this.log('✓ Added auth.guard.ts');
}
}
async _updateAngularJson(projectPath) {
const angularJsonPath = path.join(projectPath, 'angular.json');
if (!fs.existsSync(angularJsonPath)) {
this.log('angular.json not found. Skipping update.');
return;
}
try {
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
// Update the architect section
if (angularJson.projects && angularJson.projects.app && angularJson.projects.app.architect) {
const architect = angularJson.projects.app.architect;
// Update build configuration
if (architect.build) {
architect.build.options.outputPath = "www";
// Update budgets for component styles
if (architect.build.configurations && architect.build.configurations.production) {
const budgets = architect.build.configurations.production.budgets;
if (budgets) {
const styleBudget = budgets.find(budget => budget.type === 'anyComponentStyle');
if (styleBudget) {
styleBudget.maximumWarning = '6kb';
styleBudget.maximumError = '10kb';
}
}
}
}
// Update serve configuration
if (architect.serve) {
architect.serve.configurations = {
production: {
buildTarget: "app:build:production"
},
development: {
buildTarget: "app:build:development"
},
ci: {
progress: false
}
};
architect.serve.defaultConfiguration = "development";
}
// Update test configuration
if (architect.test) {
architect.test.options = {
main: "src/test.ts",
polyfills: "src/polyfills.ts",
tsConfig: "tsconfig.spec.json",
karmaConfig: "karma.conf.js",
inlineStyleLanguage: "scss",
assets: [
{
glob: "**/*",
input: "src/assets",
output: "assets"
}
],
styles: ["src/global.scss", "src/theme/variables.scss"],
scripts: []
};
architect.test.configurations = {
ci: {
progress: false,
watch: false
}
};
}
// Update lint configuration
if (architect.lint) {
architect.lint.options = {
lintFilePatterns: ["src/**/*.ts", "src/**/*.html"]
};
}
// Write the updated angular.json back to file
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2));
this.log('✓ Updated angular.json configuration');
} else {
this.log('Could not find architect section in angular.json');
}
} catch (error) {
this.log('Error updating angular.json:', error.message);
}
}
async _setupDockerfile(projectPath) {
const dockerfilePath = path.join(projectPath, 'Dockerfile');
// Handle Dockerfile
if (fs.existsSync(dockerfilePath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'Dockerfile already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping Dockerfile setup.');
} else {
const templateContent = this.fs.read(this.templatePath('Dockerfile.erb'));
fs.writeFileSync(dockerfilePath, templateContent);
this.log('✓ Added Dockerfile');
}
} else {
const templateContent = this.fs.read(this.templatePath('Dockerfile.erb'));
fs.writeFileSync(dockerfilePath, templateContent);
this.log('✓ Added Dockerfile');
}
}
async _setupLoggingMutation(projectPath) {
const mutationsDir = path.join(projectPath, 'src', 'app', 'graphql', 'mutations');
// Create mutations directory if it doesn't exist
if (!fs.existsSync(mutationsDir)) {
fs.mkdirSync(mutationsDir, { recursive: true });
}
// Handle logging.mutation.ts
const mutationPath = path.join(mutationsDir, 'logging.mutation.ts');
if (fs.existsSync(mutationPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'logging.mutation.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping logging.mutation.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/app/graphql/mutations/logging.mutation.ts.erb'));
fs.writeFileSync(mutationPath, templateContent);
this.log('✓ Added logging.mutation.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/app/graphql/mutations/logging.mutation.ts.erb'));
fs.writeFileSync(mutationPath, templateContent);
this.log('✓ Added logging.mutation.ts');
}
}
async _setupEnvironmentFiles(projectPath) {
const environmentsDir = path.join(projectPath, 'src', 'environments');
// Create environments directory if it doesn't exist
if (!fs.existsSync(environmentsDir)) {
fs.mkdirSync(environmentsDir, { recursive: true });
}
// Generate encryption key using openssl
const encryptionKey = execSync('openssl rand -hex 32').toString().trim();
this.log('Generated new encryption key');
// Handle environment.ts
const devEnvPath = path.join(environmentsDir, 'environment.ts');
if (fs.existsSync(devEnvPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'environment.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping environment.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/environments/environment.ts.erb'));
const interpolatedContent = templateContent.replace(/<%= encryptionKey %>/g, encryptionKey);
fs.writeFileSync(devEnvPath, interpolatedContent);
this.log('✓ Added environment.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/environments/environment.ts.erb'));
const interpolatedContent = templateContent.replace(/<%= encryptionKey %>/g, encryptionKey);
fs.writeFileSync(devEnvPath, interpolatedContent);
this.log('✓ Added environment.ts');
}
// Handle environment.prod.ts
const prodEnvPath = path.join(environmentsDir, 'environment.prod.ts');
if (fs.existsSync(prodEnvPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'environment.prod.ts already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping environment.prod.ts setup.');
} else {
const templateContent = this.fs.read(this.templatePath('src/environments/environment.prod.ts.erb'));
const interpolatedContent = templateContent.replace(/<%= encryptionKey %>/g, encryptionKey);
fs.writeFileSync(prodEnvPath, interpolatedContent);
this.log('✓ Added environment.prod.ts');
}
} else {
const templateContent = this.fs.read(this.templatePath('src/environments/environment.prod.ts.erb'));
const interpolatedContent = templateContent.replace(/<%= encryptionKey %>/g, encryptionKey);
fs.writeFileSync(prodEnvPath, interpolatedContent);
this.log('✓ Added environment.prod.ts');
}
}
async _setupWranglerConfig(projectPath) {
const wranglerPath = path.join(projectPath, 'wrangler.jsonc');
// Handle wrangler.jsonc
if (fs.existsSync(wranglerPath)) {
const { confirmOverwrite } = await handlePrompt(this, [{
type: 'confirm',
name: 'confirmOverwrite',
message: 'wrangler.jsonc already exists. Do you want to overwrite it?',
default: true
}]);
if (!confirmOverwrite) {
this.log('Skipping wrangler.jsonc setup.');
return;
}
}
// Get current date in YYYY-MM-DD format
const currentDate = new Date().toISOString().split('T')[0];
// Read the template content and write it directly
const templateContent = this.fs.read(this.templatePath('wrangler.jsonc.erb'));
const interpolatedContent = templateContent
.replace(/<%= compatibility_date %>/g, currentDate)
.replace(/<%= appName %>/g, this.answers.appName + '-worker');
fs.writeFileSync(wranglerPath, interpolatedContent);
this.log('✓ Added wrangler.jsonc configuration');
}
_checkDependenciesNeedUpdate(existingJson, templateJson) {
// Check dependencies
for (const [dep, version] of Object.entries(templateJson.dependencies)) {
if (!existingJson.dependencies[dep] || existingJson.dependencies[dep] !== version) {
return true;
}
}
// Check devDependencies
for (const [dep, version] of Object.entries(templateJson.devDependencies)) {
if (!existingJson.devDependencies[dep] || existingJson.devDependencies[dep] !== version) {
return true;
}
}
return false;
}
_printSummary(installPath, projectName, projectExists) {
this.log('\n=== Installation Summary ===');
this.log('\nProject Setup:');
this.log(`✓ ${projectExists ? 'Updated' : 'Created'} Ionic Angular project: ${projectName}`);
this.log(`✓ Location: ${path.join(installPath, projectName)}`);
this.log('✓ Dependencies:');
this.log(' • @capacitor/android and @capacitor/ios');
this.log(' • @ionic/storage-angular');
this.log(' • Apollo GraphQL packages');
this.log(' • GraphQL');
this.log('✓ GitHub Actions:');
this.log(' • Added frontend build and deployment workflow');
this.log('✓ Git:');
this.log(' • Added .gitignore file');
this.log('✓ Capacitor:');
this.log(' • Updated capacitor.config.ts');
this.log('✓ Documentation:');
this.log(' • Added README.md with build instructions');
this.log('✓ Interceptors:');
this.log(' • Added HTTP error interceptor and tests');
this.log('✓ Services:');
this.log(' • Added error service and tests');
this.log(' • Added auth service');
this.log(' • Added storage service');
this.log(' • Added logging service');
this.log(' • Added routes service');
this.log(' • Added toast service');
this.log(' • Added GraphQL service');
this.log('✓ Shared Directory:');
this.log(' • Added interfaces, components, pages, and base directories');
this.log('✓ App Component:');
this.log(' • Added app.component.html, app.component.scss, and app.component.ts');
this.log('✓ Theme:');
this.log(' • Added variables.scss with custom color palette');
this.log('✓ Application:');
this.log(' • Added main.ts with app bootstrap configuration');
this.log('✓ GraphQL:');
this.log(' • Added apollo.config.ts with authentication setup');
this.log('✓ Authentication:');
this.log(' • Added auth guard for protected routes');
this.log('✓ Angular Configuration:');
this.log(' • Updated angular.json with proper build and serve configurations');
this.log('✓ Docker:');
this.log(' • Added Dockerfile with multi-stage build configuration');
this.log('✓ GraphQL Mutations:');
this.log(' • Added logging mutation for error tracking');
this.log('✓ Environment Configuration:');
this.log(' • Added environment.ts and environment.prod.ts with secure encryption key');
this.log('✓ Wrangler Configuration:');
this.log(' • Added wrangler.jsonc configuration');
this.log('\nNext Steps:');
this.log('1. cd into your project directory:');
this.log(` cd ${projectName}`);
this.log('2. Start the development server:');
this.log(' ionic serve');
this.log('\nAvailable Commands:');
this.log('• ionic serve - Start the development server');
this.log('• ionic build - Build the app for production');
this.log('• ionic capacitor add android - Add Android platform');
this.log('• ionic capacitor add ios - Add iOS platform');
this.log('• ionic capacitor copy - Copy web assets to native platforms');
this.log('• ionic capacitor open - Open native project in IDE');
}
_debugLog(message) {
if (this.options.debug) {
this.log(`[DEBUG] ${message}`);
}
}
};