@saiforceone/dirt-cli
Version:
Official CLI Utility for the D.I.R.T stack
475 lines (426 loc) • 13.8 kB
text/typescript
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 ScaffoldOutput = DIRTStackCLI.ScaffoldOutput;
import DIRTCoreOpts = DIRTStackCLI.DIRTCoreOpts;
import DIRTDatabaseOpt = DIRTStackCLI.DIRTDatabaseOpt;
import { generateDatabaseSettings } from './databaseUtils.js';
import Frontend = DIRTStackCLI.Frontend;
import { toTitleCase } from './feUtils.js';
import ConsoleLogger from '../utils/ConsoleLogger.js';
import {
writeReactFrontendPage,
writeVueFrontendPage,
} from './frontendUtils.js';
import LogType = DIRTStackCLI.LogType;
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(): Promise<ScaffoldOutput> {
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: string,
pythonExecutablePath: string
): Promise<ScaffoldOutput> {
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(): Promise<ScaffoldOutput> {
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: string
): Promise<ScaffoldOutput> {
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 as Error).message;
return output;
}
try {
await copy(templateBaseDir, destinationBase, FILE_COPY_OPTS);
} catch (e) {
output.error = (e as Error).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: DIRTCoreOpts
): Promise<ScaffoldOutput> {
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 as Error).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: string,
projectName: string,
destination: string
): Promise<ScaffoldOutput> {
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: string,
destination: string
): Promise<ScaffoldOutput> {
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: string,
destination: string,
databaseOpt: Omit<'None', DIRTDatabaseOpt>
): Promise<ScaffoldOutput> {
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 as Error).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: string
): Promise<ScaffoldOutput> {
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: string,
destinationPath: string
): Promise<ScaffoldOutput> {
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 as Error).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: string,
controllerName: string,
frontend: Frontend,
logType: LogType
): Promise<ScaffoldOutput> {
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 as Error).message
}`;
output.error = `Failed to generate controller views file with error: ${
(e as Error).message
}`;
return output;
}
}