@saiforceone/dirt-cli
Version:
Official CLI Utility for the D.I.R.T stack
331 lines • 13.4 kB
JavaScript
import chalk from 'chalk';
import { exec } from 'child_process';
import fs from 'node:fs';
import constants from 'node:constants';
import path from 'node:path';
import { access, appendFile, cp as copy, mkdir, writeFile, } from 'node:fs/promises';
import { createRequire } from 'module';
import { DIRT_TEMPLATES_FOLDER, DJANGO_TEMPLATES_PATH, INERTIA_DEFAULTS_PATH, PIPENV_VENV_COMMAND, } from '../constants/djangoConstants.js';
import { standardOutputBuilder } from './standardOutputBuilder.js';
import { platform } from 'os';
import { normalizeWinFilePath } from './fileUtils.js';
import { FILE_COPY_OPTS } from '../constants/index.js';
import { FRONTEND_PATHS } from '../constants/feConstants.js';
import { generateDatabaseSettings } from './databaseUtils.js';
import { toTitleCase } from './feUtils.js';
import ConsoleLogger from '../utils/ConsoleLogger.js';
import { writeReactFrontendPage, writeVueFrontendPage, } from './frontendUtils.js';
const require = createRequire(import.meta.url);
const djangoDependencies = require('../../configs/djangoDependencies.json');
/**
* @description This function handles the installation of dependencies via Pipenv
*/
export function installDependencies() {
const output = standardOutputBuilder();
return new Promise((resolve, reject) => {
// use the dependencies file to build the install string
const packageList = Object.keys(djangoDependencies.packages)
.map((pkg) => `${pkg}==${djangoDependencies.packages[pkg]}`)
.join(' ');
const command = `pipenv install ${packageList}`;
exec(command, (error, stdout, stderr) => {
if (error) {
// ConsoleLogger.printMessage(error.message, 'warning');
output.error = error.message;
reject(output);
}
output.success = true;
output.result = stdout ? stdout : stderr;
resolve(output);
});
});
}
/**
* @description This function is responsible for creating the Django project. For this process to work, we
* have to specify which python executable needs to be used as we cannot activate the virtual environment.
* @param {string} projectName This refers to the name of the project which is read from the command line
* @param {string} pythonExecutablePath The path to the python executable so that `startproject` can be kicked off
*/
export function createDjangoProject(projectName, pythonExecutablePath) {
const output = standardOutputBuilder();
return new Promise((resolve, reject) => {
try {
fs.accessSync(pythonExecutablePath, constants.R_OK | constants.X_OK);
}
catch (e) {
output.error = e.message;
reject(output);
}
const venvCommand = PIPENV_VENV_COMMAND;
const projectCommand = `${pythonExecutablePath} -m django startproject ${projectName} .`;
exec(venvCommand, (error) => {
if (error) {
output.error = error.message;
reject(output);
}
exec(projectCommand, (pcError, pcStdout, pcStderr) => {
if (pcError) {
output.error = pcError.toString();
output.result = 'Failed to create Django project.';
reject(output);
}
output.success = true;
output.result = pcStdout ? pcStdout : pcStderr;
resolve(output);
});
});
});
}
/**
* @description Gets the location of the virtual environment that was created so that
* the path to the python executable can be determined.
*/
export function getVirtualEnvLocation() {
const output = standardOutputBuilder();
return new Promise((resolve, reject) => {
exec(PIPENV_VENV_COMMAND, (error, stdout, stderr) => {
if (error) {
output.error = error.message;
reject(output);
}
output.success = true;
output.result = stdout ? stdout : stderr;
resolve(output);
});
});
}
/**
* @description Copies django template data to the destination directory. Overwrites the original manage.py file
* @param {string} destinationBase
*/
export async function copyDjangoSettings(destinationBase) {
const output = standardOutputBuilder();
try {
const currentFileUrl = import.meta.url;
let templateBaseDir = path.resolve(path.normalize(new URL(currentFileUrl).pathname), DJANGO_TEMPLATES_PATH);
if (platform() === 'win32')
templateBaseDir = normalizeWinFilePath(templateBaseDir);
try {
await access(destinationBase, constants.W_OK);
}
catch (e) {
output.error = e.message;
return output;
}
try {
await copy(templateBaseDir, destinationBase, FILE_COPY_OPTS);
}
catch (e) {
output.error = e.message;
return output;
}
output.success = true;
output.result = `File copy results files copied.`;
return output;
}
catch (e) {
output.result = 'Failed to copy files';
output.error = e.toString();
return output;
}
}
export async function copyDjangoHTMLTemplates(options) {
const output = standardOutputBuilder();
try {
const currentFileUrl = import.meta.url;
let templateBaseDir = path.resolve(path.normalize(new URL(currentFileUrl).pathname), FRONTEND_PATHS[options.frontend].BASE_HTML_TEMPLATES_PATH);
if (platform() === 'win32')
templateBaseDir = normalizeWinFilePath(templateBaseDir);
// create dirt_templates folder
const templateDestination = path.join(options.destinationBase, DIRT_TEMPLATES_FOLDER);
// create folder
await mkdir(templateDestination);
// check destination
// await access(options.destinationBase, constants.W_OK);
// copy files
await copy(templateBaseDir, templateDestination, FILE_COPY_OPTS);
output.result = 'Base templates copied';
output.success = true;
return output;
}
catch (e) {
output.result = 'Failed to copy templates';
output.error = e.message;
return output;
}
}
/**
* @description Writes settings for dev mode
* @param {string} secretKey This serves as Django's secret key
* @param {string} projectName The name of the project
* @param {string} destination
*/
export async function writeDevSettings(secretKey, projectName, destination) {
const output = standardOutputBuilder();
try {
const appSettings = `
\n#Add extra apps here
\nINSTALLED_APPS += ['${projectName}']
\n#Secret Key\nSECRET_KEY = "${secretKey}"
`;
await appendFile(destination, appSettings);
output.result = 'Dev settings updated';
output.success = true;
return output;
}
catch (e) {
output.error = e.toString();
return output;
}
}
/**
* @description Writes updated configuration to base settings
* @param {string} projectName
* @param {string} destination
*/
export async function writeBaseSettings(projectName, destination) {
const output = standardOutputBuilder();
try {
await appendFile(destination, `\nWSGI_APPLICATION = "${projectName}.wsgi.application"`);
await appendFile(destination, `\nROOT_URLCONF = "${projectName}.urls"`);
output.result = 'Base settings updated';
output.success = true;
return output;
}
catch (e) {
output.error = e.toString();
return output;
}
}
export async function writeDatabaseSettings(projectName, destination, databaseOpt) {
const output = standardOutputBuilder();
try {
// get the database options object that should be written to settings
const dbSettings = generateDatabaseSettings(projectName, databaseOpt);
// write to file
await appendFile(destination, `\n# DATABASE SETTINGS`);
await appendFile(destination, `\nDATABASES = ${dbSettings}`);
output.success = true;
return output;
}
catch (e) {
output.error = e.message;
return output;
}
}
/**
* @deprecated Replaced with copyAssets
* @description Copies inertia specific urls.py and default views file to the project destination
* @param {string} destinationPath
*/
export async function copyInertiaDefaults(destinationPath) {
const output = standardOutputBuilder();
try {
const currentFileUrl = import.meta.url;
let inertiaDefaultsDir = path.resolve(path.normalize(new URL(currentFileUrl).pathname), INERTIA_DEFAULTS_PATH);
if (platform() === 'win32')
inertiaDefaultsDir = normalizeWinFilePath(inertiaDefaultsDir);
await copy(inertiaDefaultsDir, destinationPath, FILE_COPY_OPTS);
output.success = true;
output.result = `Inertia files copied`;
return output;
}
catch (e) {
output.error = e.toString();
output.result = 'Failed to copy inertia defaults';
return output;
}
}
export async function copyAssets(sourcePath, destinationPath) {
const output = standardOutputBuilder();
try {
const currentFileUrl = import.meta.url;
let assetBuilderSrcPath = path.resolve(path.normalize(new URL(currentFileUrl).pathname), sourcePath);
if (platform() === 'win32')
assetBuilderSrcPath = normalizeWinFilePath(assetBuilderSrcPath);
await copy(assetBuilderSrcPath, destinationPath, FILE_COPY_OPTS);
output.success = true;
return output;
}
catch (e) {
output.error = `Failed to copy assets with error: ${e.message}`;
return output;
}
}
/**
* @description Writes the necessary data to the generated views.py file to make the inertia view work
* @param destinationPath
* @param controllerName
* @param frontend
* @param logType
*/
export async function writeInertiaViewsFile(destinationPath, controllerName, frontend, logType) {
const output = standardOutputBuilder();
try {
// get path to file
const filePath = path.join(destinationPath, controllerName.toLowerCase(), 'views.py');
// construct file contents
const fileContent = `
# Generated using D.I.R.T Stack CLI
\nfrom inertia import inertia
\n# Create your views here.
\n\n@inertia('${toTitleCase(controllerName)}/Index')
def index(request):
\treturn {
\t\t'controllerName': '${controllerName}'
\t}
\n\n
`;
// overwrite original views.py file that was created from django-admin startapp <app_name>
await writeFile(filePath, fileContent, { encoding: 'utf-8' });
// construct the urls.py file for the controller
const urlsFilePath = path.join(destinationPath, controllerName.toLowerCase(), 'urls.py');
const urlsFileContent = `
# Generated using D.I.R.T Stack CLI
\nfrom django.urls import path
\nfrom . import views
\n\nurlpatterns = [
\tpath('', views.index, name='${controllerName}')
]
\n
`;
// overwrite original urls.py file that was created from django-admin startapp <app_name>
await writeFile(urlsFilePath, urlsFileContent, { encoding: 'utf-8' });
// overwrite main urls.py? or print out string to paste into main urls.py
console.log(`
${chalk.green.underline('D.I.R.T CLI Controller Created')}\n
Update your main ${chalk.blue.bold('urls.py')} file as follows\n
1. Import the ${chalk.blue.bold('include')} function from ${chalk.blue.bold('django.urls')}\n
${chalk.green('from django.urls import path, include')}\n
2. Add this entry to ${chalk.bold('urlpatterns')}\n
${chalk.green(`path('${controllerName}/', include('${controllerName}.urls')),`)}\n
3. Navigate to the newly-created controller\n
${chalk.green(`http://127.0.0.1:8000/${controllerName}/`)}
`);
const currentFileUrl = import.meta.url;
if (frontend === 'react') {
const reactFETypes = path.resolve(new URL(currentFileUrl).pathname, FRONTEND_PATHS[frontend].TYPES_PATH);
const feTypesDestination = path.join(destinationPath, `dirt_fe_${frontend}`, 'src', '@types');
await copy(reactFETypes, feTypesDestination, FILE_COPY_OPTS);
}
// write out inertia template files
await mkdir(path.join(destinationPath, `dirt_fe_${frontend}`, 'src', 'pages', toTitleCase(controllerName)));
const templateFileExt = frontend === 'react' ? 'tsx' : 'vue';
// determine target path for Inertia views
const inertiaViewsPath = path.join(destinationPath, `dirt_fe_${frontend}`, 'src', 'pages', toTitleCase(controllerName), `Index.${templateFileExt}`);
if (logType === 'noisyLogs')
ConsoleLogger.printMessage(`Write page component to: ${inertiaViewsPath}`);
const frontendIndexContent = frontend === 'react' ? writeReactFrontendPage() : writeVueFrontendPage();
await writeFile(inertiaViewsPath, frontendIndexContent, {
encoding: 'utf-8',
});
output.success = true;
return output;
}
catch (e) {
output.result = `Failed to generate controller view with error: ${e.message}`;
output.error = `Failed to generate controller views file with error: ${e.message}`;
return output;
}
}
//# sourceMappingURL=djangoUtils.js.map