@stilljs/cli
Version:
This is the CLI tool for StillJS Framework
558 lines (439 loc) • 22.1 kB
JavaScript
import { Command, program } from 'commander';
import fs from 'fs';
import yocto from 'yocto-spinner';
import colors from 'yoctocolors';
import { FileHelper } from './helper/FileHelper.js';
import { FrameworkHelper } from './helper/FrameworkHelper.js';
import { RouterHelper } from './helper/RouterHelper.js';
export class StillCmd {
/** @type { Command } */ program;
static stillProjectRootDir = [];
static createTypeOptions = {
'COMPONENT': 'COMPONENT',
'PROJECT': 'PROJECT',
}
filePathTracker = [];
constructor() {
this.program = program;
this.program.version('0.1');
this.program
.command('init')
.description(
'Start a project in the current folder as the\n'
+ '- example: ' + colors.bold(colors.green('still init')) + ' starts still project in the current folder\n'
);
this.program
.command('lone')
.description(
'Setup a project in the current folder:\n'
+ '- example: ' + colors.bold(colors.green('still lone')) + ' setup lone project in the current folder \n'
);
this.program
.command('serve')
.description('Run the project and open to in the default browser\n');
this.program
.command('create <type> <name> [args...]')
.option('--lone', 'instruction for Lone Component generation')
.description(
'Create a Component as the bellow examples:\n'
+ '- example1: ' + colors.bold(colors.green('still create cp LeftMenu')) + ' create a new component with name LeftMenu \n'
+ '- example2: ' + colors.bold(colors.green('still create cp LeftMenu --lone')) + ' create a new component with name LeftMenu for within Lone project \n'
);
this.program
.command('c <type> <name>')
.option('--lone', 'instruction for Lone Component generation')
.description('Alias for create, produces the same result:\n');
this.program
.command('install <arguments...>')
.description(
'Install new library form NPM:\n'
+ '- example1: ' + colors.bold(colors.green('still install @stilljs/tabulator')) + ' install StillJS tabulator grid component\n'
);
this.program
.command('route <action>')
.description(
'Display all existing routes\n'
+ '- example1: ' + colors.bold(colors.green('still route list')) + '\n'
+ '- example2: ' + colors.bold(colors.green('still route l')) + '\n'
);
this.program
.command('r <action>')
.description('Alias to route, produces the same result\n');
this.program
.command('app <action>')
.description(
'Allow to prosecute operation on top of the application\n'
+ '- example: ' + colors.bold(colors.green('still app serve'))
+ ' opens the app in the browser in the default port of 8181\n'
);
this.program
.command('A <action>')
.description(
'Alias for app, produces the same result:\n'
);
this.program.parse(process.argv);
const opts = this.parseCmdType();
(async () => await this.cmd(opts))();
}
/** @param { { create, install, routes } } opts */
async cmd(opts) {
if (opts.create) await this.create(opts);
else if (opts.install) await this.install(opts);
else if (opts.route) await this.listRoutes(opts);
else if (opts.app) await this.runAppOperation(opts);
else this.showGenericHelp();
}
async create(opts) {
const { COMPONENT, PROJECT } = StillCmd.createTypeOptions;
const cmdArgs = String(opts.create);
const isCreateComp = this.checkCreateOption(COMPONENT, cmdArgs)
if (isCreateComp) return await this.createComponent(opts)
const isCreateProj = this.checkCreateOption(PROJECT, cmdArgs);
if (isCreateProj) return await this.createNewProject(opts);
}
async createNewProject(opts) {
const helper = FrameworkHelper, isLone = opts.isLone, isInit = opts.isInit;
let projectName = opts.isLone ? 'lone' : opts.name;
if (opts.isInit) projectName = 'still_tmp';
this.newCmdLine();
const projName = (isLone || isInit) ? ' ' : ` (${colors.bold(projectName)}) `;
this.cmdMessage(`New project${projName}creation initiated:`);
const spinner = yocto({ text: 'Downloading StillJS from @stilljs/core' });
const result = await helper.createNewStillProject(this, spinner, projectName, isLone, isInit);
if (result.result !== false) {
spinner.success(`Project${projName}created`);
this.newCmdLine();
const unwrapSpinner = yocto({ text: 'Unwrapping root folder' });
try {
helper.unwrapStillJSFolder(projectName, isLone, isInit);
unwrapSpinner.success(`Process creation ran successfully`)
} catch (error) {
unwrapSpinner.error(`Error on unwrapping${projName}project`);
}
if (isLone || isInit) {
const loneDocLink = `https://still-js.github.io/stilljs-doc/installation-and-running-cdn/#cdn-basic-code-sample`;
this.cmdMessage(`\t- You're all set, use still-cli to generate the your component`);
if (isLone) {
this.cmdMessage(`\t example: ${colors.bold(colors.green('npx still create component app/MyComponen --lone'))}`);
this.cmdMessage(`\t and after creating it you can embed them, follow the example at the bellow link:`);
this.cmdMessage('\t\t' + colors.underline(loneDocLink));
}
} else {
this.cmdMessage(`\t- type ${colors.bold(colors.green(`cd ${projectName}`))} to enter project folder`);
this.cmdMessage(`\t and then type ${colors.bold(colors.green('npm run dev'))} to open in the browser`);
}
this.newCmdLine();
}
if (result.result === false) {
this.cmdMessage(colors.bgRed(`Failed to create${projName}project`));
if (result.reason === 'overwriting') {
this.cmdMessage(`\t- Seems like you're accidentally overriding an exsiting project because especific`);
this.cmdMessage(`\t files (e.g. ${colors.underline(colors.bold('app/'))} and ${colors.underline(colors.bold('route.map.js'))}) files are present in the current folder`);
}
this.newCmdLine();
}
}
wrongProjectTypeWarning(type = null, spinner = null) {
if (spinner) spinner.error(colors.red(`Faile to create the component follow the cause:`))
if (type === 'still') {
this.newCmdLine();
this.cmdMessage(colors.red(`${colors.bold('Wrong Component creation instructions')}: `));
this.cmdMessage(
`\tYou're inside a regular ${colors.bold('Still')} project, you cannot use --lone parameter\n`
);
this.newCmdLine();
} else if (type === 'lone') {
this.newCmdLine();
this.cmdMessage(colors.red(`${colors.bold('Wrong Component creation instructions')}: `));
this.cmdMessage(
`\tYou're inside a Still ${colors.bold('Lone/CDN')} based project, you need to use --lone parameter as the examples:\n`
+ '\texample1: ' + colors.bold(colors.green('npx still create component path-to/MyComponent --lone'))
+ '\n\texample2: ' + colors.bold(colors.green('npx still c cp path-to/MyComponent --lone'))
);
this.newCmdLine();
}
}
async createComponent(opts) {
let cmpName = opts.name.startsWith('./') ? opts.name.replace('./', '') : opts.name;
let cmpPath = cmpName.split('/');
cmpName = cmpPath.at(-1), cmpPath.pop();
if (cmpName.toLowerCase().endsWith('.js')) {
this.cmdMessage(colors.red(`${colors.bold('\nWrong Component creation instructions')}: `));
this.cmdMessage(
`\tComponent name cannot have extension type (e.g. .js) nor special carechter\n`
);
return;
}
if (opts.isLone) {
if (FileHelper.stillProjectExists() || !FileHelper.loneProjectExists())
return this.wrongProjectTypeWarning('lone');
this.createLoneComponent(opts, cmpName, cmpPath);
}
else {
if (FileHelper.loneProjectExists() && !FileHelper.stillProjectExists())
return this.wrongProjectTypeWarning('lone');
this.createNewComponent(opts, cmpName, cmpPath);
};
}
async createNewComponent(opts, cmpName, cmpPath) {
StillCmd.stillProjectRootDir = [];
this.newCmdLine();
const spinner = yocto({ text: `Creating new component ${opts.name}` }).start();
const isRootFolder = FileHelper.isItRootFolder(spinner, this, false).flag;
const cmpPathParam = cmpPath?.join('/');
if (isRootFolder && cmpPath[0] != 'app' && !opts.isLone)
return FileHelper.wrongFolderCmpCreationError(spinner, this);
const fileMetadata = { cmpPath, cmpName, isLone: opts.isLone };
this.cmdMessage(`\n Component will be created at ${cmpPath != '' ? cmpPath.join('/') : './'}/`);
await this.getRootDirThenRunCallback(fileMetadata, spinner, null,
async ({ cmpPath, cmpName, routeFile, filePath, wrongProjectType }, spinnerObj) => {
if (wrongProjectType) return this.wrongProjectTypeWarning('lone', spinner);
const doesCmpExists = RouterHelper.checkIfRouteExists(routeFile, cmpName);
if (doesCmpExists) {
this.newCmdLine();
spinnerObj.error(colors.red(`${colors.bold('Component name conflict error')}: `));
this.cmdMessage(
colors.bgRed(`\t Component with name ${colors.bold(cmpName)} already exists, please choose another name to avoid conflict`)
);
this.newCmdLine();
return;
}
let rootFolder = StillCmd.stillProjectRootDir.join('/');
const {
dirPath, fileName, createdFolder
} = await FileHelper.parseDirTree(cmpPath, cmpName);
try {
if (FileHelper.stillProjectExists()) filePath = cmpPathParam;
if(isRootFolder) {
const totalFolders = cmpPathParam.split('/').length;
rootFolder = '/'.repeat(totalFolders).split('').join('..');
}
const cmpFullPath = `${filePath}/${cmpName}.js`;
FileHelper.createComponentFile(cmpName, rootFolder, dirPath, fileName);
spinnerObj.success(`Component ${cmpFullPath} created successfully`);
const routeSpinner = yocto({ text: `Creating the route` }).start();
const addRoute = RouterHelper.updateProjectRoutes(routeFile, cmpName, cmpFullPath, filePath);
if (addRoute)
routeSpinner.success(`New route added with path ${cmpFullPath}`);
else {
routeSpinner.error(`
Failed to a route for ${cmpName},
you can anyway add it manually in the route.map.js file
`);
}
this.newCmdLine();
} catch (error) {
spinnerObj.error('Erro on creating component: ' + fileName);
this.cmdMessage(error.message);
this.newCmdLine();
}
});
}
async createLoneComponent(opts, cmpName, cmpPath) {
StillCmd.stillProjectRootDir = [];
this.newCmdLine();
const spinner = yocto({ text: `Creating new component ${opts.name}` }).start();
const isRootFolder = FileHelper.isItRootFolder(spinner, this, false, true).flag;
if (isRootFolder && cmpPath[0] != 'app' && opts.isLone)
return FileHelper.wrongFolderCmpCreationError(spinner, this, opts.isLone);
const fileMetadata = { cmpPath, cmpName, isLone: opts.isLone, isRootFolder };
this.cmdMessage(`\n Component will be created at ${cmpPath != '' ? cmpPath.join('/') + '/' : './'}`);
await this.getRootDirThenRunCallback(fileMetadata, spinner, null,
async ({ cmpPath, cmpName, routeFile, filePath, wrongProjectType }, spinnerObj) => {
if (wrongProjectType) return this.wrongProjectTypeWarning('still', spinner);
const doesCmpExists = RouterHelper.checkIfRouteExists(routeFile, cmpName);
if (doesCmpExists) {
spinnerObj.error(`Component with name ${cmpName} already exists, please choose another name to avoid conflict`);
return;
}
const rootFolder = StillCmd.stillProjectRootDir.join('/');
const {
dirPath,
fileName
} = await FileHelper.parseDirTree(cmpPath, cmpName);
try {
let cmpFullPath = FileHelper.createComponentFile(
cmpName, rootFolder, dirPath, fileName, opts.isLone
);
if (!isRootFolder) cmpFullPath = `${filePath}/${cmpName}.js`;
spinnerObj.success(`Component ${cmpFullPath} created successfully`);
const routeSpinner = yocto({ text: `Creating the route` }).start();
const addRoute = RouterHelper.updateProjectRoutes(routeFile, cmpName, cmpFullPath, filePath);
if (addRoute) routeSpinner.success(`New route added with path ${cmpFullPath}`);
else {
routeSpinner.error(`
Failed to a route for ${cmpName},
you can anyway add it manually in the route.map.js file
`);
}
this.newCmdLine();
} catch (error) {
spinnerObj.error('Erro on creating component: ' + fileName);
this.cmdMessage(error.message);
this.newCmdLine();
}
});
}
async install(opts) {
const pkgName = opts.pkg, helper = FrameworkHelper;
const spinner = yocto({ text: `Setting up ${pkgName} instalation process` }).start();
const { flag } = FileHelper.isItRootFolder(spinner, this, false);
this.newCmdLine();
this.cmdMessage(`Starting instalation for (${colors.bold(pkgName)}):`);
const result = await helper.runInstallStillPkg(
opts.arguments, null, !flag, { spinner, cmdObj: this }
);
if (result) {
spinner.success(`Package ${colors.bold(pkgName)} installed`);
this.newCmdLine();
const unwrapSpinner = yocto({ text: `Unwrapping ${colors.bold(pkgName)}` });
try {
helper.unwrapInstalledPkg();
unwrapSpinner.success(`Installation process finished successfully`)
} catch (error) {
unwrapSpinner.error(`Error on unwrapping ${colors.bold(pkgName)} package`);
}
this.newCmdLine();
}
if (!result)
spinner.error(`Failed to create ${colors.bold(pkgName)} project`);
}
showGenericHelp() {
this.newCmdLine();
this.cmdMessage(`Type still -h/--help to know what options are provided`);
this.newCmdLine();
}
checkCreateOption(type, cmdArgs) {
if (type == StillCmd.createTypeOptions.COMPONENT) {
return (
cmdArgs.indexOf('cp') == 0
|| cmdArgs.indexOf('component') == 0
)
}
if (type == StillCmd.createTypeOptions.PROJECT) {
return (
cmdArgs.indexOf('pj') == 0
|| cmdArgs.indexOf('project') == 0
)
}
}
/**
* @param {{ cmpPath, cmpName, routeFile, isLone }, spinnerObj} cb
*/
async getRootDirThenRunCallback(fileMetadata, spinner, dir, cb, callNum = 0, countFolderBack = 0) {
let enteredPath = fileMetadata?.cmpPath?.length ? fileMetadata.cmpPath.join('/') : '';
let actualDir = dir || (`${process.cwd()}/${enteredPath}`);
if (actualDir.endsWith('/..')) {
countFolderBack++;
const sliceIdx = eval(`-${countFolderBack}`);
const currPath = actualDir.split('/').slice(0, sliceIdx).slice(sliceIdx)[0];
this.filePathTracker.push(currPath);
}
if (callNum == 10 && fileMetadata?.isLone)
return FileHelper.noLoneProjectFolderError(spinner, this);
if (fileMetadata?.isRootFolder && fileMetadata?.isLone)
return await cb({ ...fileMetadata, routeFile: `${process.env.PWD}/config/route.map.js` }, spinner);
if (callNum == 10)
return FileHelper.noStillProjectFolderError(spinner, this);
if (!fs.existsSync(actualDir + '/')) {
const newDirPath = actualDir.split('/');
const dirName = newDirPath.pop();
this.filePathTracker.push(dirName);
actualDir = newDirPath.join('/');
StillCmd.stillProjectRootDir.push('..');
this.getRootDirThenRunCallback(fileMetadata, spinner, `${actualDir}`, cb, (callNum + 1), countFolderBack);
return;
}
spinner.text = 'Serching project root folder';
let { flag, wrongProjectType } = FileHelper.wasRootFolderReached(actualDir, fileMetadata?.isLone, fileMetadata == null);
if (flag) {
if (FileHelper.stillProjectExists()) {
wrongProjectType = false, actualDir = process.env.PWD;
}
const filePath = this.filePathTracker.reverse().join('/')
this.cmdMessage(` -- Validated Still.js project folder`);
yocto().start().success(`Found project root folder`);
fileMetadata = { ...fileMetadata, filePath, wrongProjectType };
await cb({ ...fileMetadata, routeFile: `${actualDir}/config/route.map.js` }, spinner);
} else {
StillCmd.stillProjectRootDir.push('..');
await this.getRootDirThenRunCallback(fileMetadata, spinner, `${actualDir}/..`, cb, (callNum + 1), countFolderBack);
return;
}
}
async runLoneCmpCreation(cb = () => { }, params, spinner) {
await cb({ ...params }, spinner)
}
async runAppOperation(opts) {
const spinner = yocto();
if (opts.action == 'serve') {
spinner.text = `Startin the server`;
spinner.start();
StillCmd.silentConsoleLog = true;
await this.getRootDirThenRunCallback(null, spinner, null,
async ({ routeFile }) => FrameworkHelper.openApp(routeFile)
);
spinner.stop();
StillCmd.silentConsoleLog = false;
} else {
this.newCmdLine();
this.cmdMessage(`Invalid command argument ${colors.bgRed(` ${opts.action} `)}`);
this.newCmdLine();
spinner.start().error(`Failed to start the server`);
this.newCmdLine();
}
}
async listRoutes(opts) {
let isListRoutes = opts.action == 'list' || opts.action == 'l';
this.newCmdLine();
const spinner = yocto({ text: 'Searching and parsing routes...' }).start();
this.newCmdLine();
StillCmd.silentConsoleLog = true;
if (isListRoutes) {
await this.getRootDirThenRunCallback(null, spinner, null,
async ({ routeFile }, spinnerInstance) => {
await RouterHelper.listRoutes(routeFile);
}
);
}
StillCmd.silentConsoleLog = false;
this.newCmdLine();
spinner.success(`Routes listed successfully`);
this.newCmdLine();
}
static silentConsoleLog = false;
newCmdLine() {
if (!StillCmd.silentConsoleLog) console.log(`\n`);
}
cmdMessage(msg) {
if (!StillCmd.silentConsoleLog) console.log(`${msg}`);
}
parseCmdType() {
let opts;
const command = process.argv.slice(2);
const createOpts = ['init', 'lone', 'create', 'c']
if (createOpts.includes(command[0])) {
opts = {
'create': createOpts.slice(0, 2).includes(command[0]) ? 'pj' : command[1],
name: command[0] == 'init' ? process.env.PWD : command[2],
isLone: command[3] == '--lone' || command[0] == 'lone',
isInit: command[0] == 'init'
};
}
if (command[0] == 'route' || command[0] == 'r')
opts = { 'route': command[0], action: command[1] };
if (command[0] == 'app' || command[0] == 'A' || command[0] == 'serve') {
const action = command[0] == 'serve' ? command[0] : command[1];
const app = command[0] == 'serve' ? 'app' : command[0];
opts = { app, action };
}
if (command[0] == 'install' || command[0] == 'i') {
opts = {
'install': command[0],
arguments: command.slice(1).join(' '),
pkg: command[1]
};
}
return opts;
}
}