UNPKG

add-manifest

Version:

Add Manifest backend

258 lines (257 loc) 10.1 kB
import { Command } from '@oclif/core'; import axios from 'axios'; import { exec as execCp } from 'node:child_process'; import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import ora from 'ora'; import treeKill from 'tree-kill'; import { parse } from 'jsonc-parser'; import { updateExtensionJsonFile } from '../utils/UpdateExtensionJsonFile.js'; import { updatePackageJsonFile } from '../utils/UpdatePackageJsonFile.js'; import { updateSettingsJsonFile } from '../utils/UpdateSettingsJsonFile.js'; import { getLatestPackageVersion } from '../utils/GetLatestPackageVersion.js'; const exec = promisify(execCp); export class MyCommand extends Command { /** * The run method is called when the command is run. * * Steps: * 1. Create a folder with the name `manifest`. * 2. Create a file inside the folder with the name `manifest.yml`. * 3. Update the `package.json` file with the new packages and scripts. * 4. Update the .vscode/extensions.json file with the recommended extensions. * 5. Update the .vscode/settings.json file with the recommended settings. * 6. Update the .gitignore file with the recommended settings. * 7. Update the .env file with the environment variables. * 8. If no README.md file exists, create one. * 9. Install the new packages. * 10. Serve the new app. * 11. Wait for the server to start. * 12. Seed the database. * 13. Open the browser. */ async run() { const folderName = 'manifest'; const initialFileName = 'backend.yml'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const assetFolderPath = path.join(__dirname, '..', '..', 'assets'); const spinner = ora('Add Manifest to your project...').start(); // * 1. Create a folder with the name `manifest`. // Construct the folder path. This example creates the folder in the current working directory. const folderPath = path.join(process.cwd(), folderName); // Check if the folder already exists if (fs.existsSync(folderPath)) { spinner.fail(`Error: The "${folderName}" folder already exists in the current directory. Please remove it and try again.`); process.exit(1); } // Create the folder fs.mkdirSync(folderPath); // * 2. Create a file inside the folder with the name `manifest.yml`. // Path where the new file should be created const newFilePath = path.join(folderPath, initialFileName); // Read the content from the asset file const content = fs.readFileSync(path.join(assetFolderPath, initialFileName), 'utf8'); // Write the content to the new file fs.writeFileSync(newFilePath, content); spinner.succeed(); spinner.start('Update package.json file...'); // Update package.json file. const packagePath = path.join(process.cwd(), 'package.json'); let packageJson; if (fs.existsSync(packagePath)) { packageJson = parse(fs.readFileSync(packagePath, 'utf8')); } else { packageJson = JSON.parse(fs.readFileSync(path.join(assetFolderPath, 'default-package.json'), 'utf8')); } const manifestLatestVersion = await getLatestPackageVersion('manifest'); fs.writeFileSync(packagePath, updatePackageJsonFile({ fileContent: packageJson, newPackages: { manifest: `^${manifestLatestVersion}` }, newScripts: { manifest: 'node node_modules/manifest/scripts/watch/watch.js', 'manifest:seed': 'node node_modules/manifest/dist/manifest/src/seed/scripts/seed.js' } })); spinner.succeed(); spinner.start('Add settings...'); // Update .vscode/extensions.json file. const vscodeDirPath = path.join(process.cwd(), '.vscode'); const extensionsFilePath = path.join(vscodeDirPath, 'extensions.json'); let extensionsJson; // Ensure the `.vscode` Directory Exists if (!fs.existsSync(vscodeDirPath)) { fs.mkdirSync(vscodeDirPath); } // Read or Initialize `extensions.json` if (fs.existsSync(extensionsFilePath)) { extensionsJson = parse(fs.readFileSync(extensionsFilePath, 'utf8')); } else { extensionsJson = { recommendations: [] }; } fs.writeFileSync(extensionsFilePath, updateExtensionJsonFile({ extensions: ['redhat.vscode-yaml'], fileContent: extensionsJson })); // Update .vscode/extensions.json file. const settingsFilePath = path.join(vscodeDirPath, 'settings.json'); let settingsJson; // Read or Initialize `settings.json` if (fs.existsSync(settingsFilePath)) { settingsJson = parse(fs.readFileSync(settingsFilePath, 'utf8')); } else { settingsJson = {}; } fs.writeFileSync(settingsFilePath, updateSettingsJsonFile({ fileContent: settingsJson, settings: { 'yaml.schemas': { 'https://schema.manifest.build/schema.json': '**/manifest/**.yml' } } })); // * 7. Update the .env file with the environment variables. const gitignorePath = path.join(process.cwd(), '.gitignore'); let gitignoreContent = ''; if (fs.existsSync(gitignorePath)) { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); } const newGitignoreLines = [ 'node_modules', '.env', 'public', 'manifest/backend.db' ]; newGitignoreLines.forEach((line) => { if (!gitignoreContent.includes(line)) { gitignoreContent += `\n${line}`; } }); fs.writeFileSync(gitignorePath, gitignoreContent); spinner.succeed(); // * 8. Add a README.md file if it doesn't exist. const readmeFilePath = path.join(process.cwd(), 'README.md'); if (!fs.existsSync(readmeFilePath)) { fs.writeFileSync(readmeFilePath, fs.readFileSync(path.join(assetFolderPath, 'README.md'), 'utf8')); } // * 9. Install the new packages. spinner.start('Install dependencies...'); // Install deps. try { await exec('npm install'); } catch (error) { spinner.fail(`Execution error: ${error}`); } // Serve the new app. spinner.succeed(); spinner.start('Add environment variables...'); // Add environment variables to .env file const envFilePath = path.join(process.cwd(), '.env'); const envJWTSecret = `TOKEN_SECRET_KEY=${crypto .randomBytes(32) .toString('hex')}`; let envContent; if (fs.existsSync(envFilePath)) { envContent = fs.readFileSync(envFilePath, 'utf8'); envContent += `\n` + envJWTSecret; } else { envContent = envJWTSecret; } fs.writeFileSync(envFilePath, envContent); spinner.succeed(); spinner.start('Build the database...'); let serveTask = null; try { // We run the manifest script to build the database. serveTask = exec('npm run manifest'); await this.waitForServerToBeReady(); spinner.succeed(); } catch (error) { spinner.fail(`Execution error: ${error}`); } spinner.start('Seed initial data...'); try { await exec('npm run manifest:seed'); } catch (error) { spinner.fail(`Execution error: ${error}`); } spinner.succeed(); console.log(); console.log('🎉 Manifest successfully installed !'); console.log(); console.log('🚀 Run `npm run manifest` to start the server.'); console.log(); await this.silentKill(serveTask?.child?.pid || 0); process.exit(); } /** * Check if the server is ready. * * @returns {Promise<boolean>} - Returns a promise that resolves to a boolean. * **/ async isServerReady() { return axios .get('http://localhost:1111/api/health') .then(() => true) .catch(() => false); } /** * Wait for the server to be ready. * * @returns {Promise<void>} - Returns a promise that resolves to void when the server is ready. * **/ async waitForServerToBeReady() { let serverReady = false; while (!serverReady) { serverReady = await this.isServerReady(); if (!serverReady) { await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1s before retrying } } } /** * Transform a JSON with comments to a JSON without comments. * * @param {string} jsonWithComments - The JSON with comments. * * @returns {string} - The JSON without comments. * **/ removeComments(jsonString) { return jsonString .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments .replace(/\/\/.*$/gm, ''); // Remove single-line comments } /** * Kill a process without logging an error if it fails. * * @param {number} pid - The process ID. * @returns {Promise<void>} - A promise that resolves when the process is killed. * */ silentKill(pid) { return new Promise((resolve, reject) => { treeKill(pid, 'SIGKILL', (err) => { if (err) { reject(`Failed to kill process: ${err}`); } else { resolve(); } }); }); } }