@adinsure-ops/ops-cli
Version:
Operations CLI for working with AdInsure
410 lines (408 loc) • 18.2 kB
JavaScript
import { __awaiter } from "tslib";
import { Flags, ux } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import CommandBase from '../command.base.js';
class CopyTransformation {
constructor(srcPath, dstPath) {
this.name = this.constructor.name;
this.srcPath = srcPath;
this.dstPath = dstPath;
}
transform() {
try {
fs.accessSync(this.srcPath, fs.constants.R_OK);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error_) {
const error = error_.code === 'ENOENT' ? new Error(`${this.name}: source file does not exist <${this.srcPath}>`) : new Error(`${this.name}: ${error_} <${this.srcPath}>`);
throw error;
}
ux.log(`Copying from ${this.srcPath} to ${this.dstPath}`);
try {
fs.copySync(this.srcPath, this.dstPath, { overwrite: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
throw new Error(`${this.name}: ${error} <${this.srcPath}>`);
}
}
}
class JavascriptTransformation {
constructor(srcPath, dstPath, javascriptFile) {
this.name = this.constructor.name;
this.srcPath = srcPath;
this.dstPath = dstPath;
this.jsFile = javascriptFile;
}
importTransformationFunction() {
return __awaiter(this, void 0, void 0, function* () {
try {
const { default: transformationFunction } = yield import(this.jsFile);
return transformationFunction;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
throw new Error(`${this.name}: module file ${this.jsFile} not found <${this.srcPath}>`);
}
else if (error instanceof SyntaxError) {
throw new SyntaxError(`${this.name}: ${error.message} in '${this.jsFile}' <${this.srcPath}>`);
}
throw new Error(`${this.name}: ${error} <${this.srcPath}>`);
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
executeTransformationFunction(transformationFunction) {
return __awaiter(this, void 0, void 0, function* () {
try {
const result = yield transformationFunction(this.srcPath);
if (typeof result !== 'string') {
throw new TypeError(`${this.name}: transformation functions must return string, the script returned ${typeof (result)} <${this.srcPath}>`);
}
return result;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
if (error instanceof TypeError && error.message.search(`/.*must return.*/`) !== -1) {
console.log(error.message.search(`/.*must return.*/`));
throw new Error(`${this.name}: invalid function in ${this.jsFile} <${this.srcPath}>`);
}
/*
Rethrow anything errors that were thrown in the above try block
*/
if (error.message && error.message.search(`${this.name}`.length > 0)) {
throw error;
}
throw new Error(`${this.name}: ${error} <${this.srcPath}>`);
}
});
}
transform() {
return __awaiter(this, void 0, void 0, function* () {
// Check if source exists, if not, issue a warning
try {
fs.accessSync(this.srcPath);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
if (error.code === 'ENOENT') {
const errMsg = `${this.name}: source does not exist, relying on transformation to find proper source file <${this.srcPath}>`;
ux.log(`${chalk.yellow(errMsg)}`);
}
else {
throw new Error(`${this.name}: ${error} <${this.srcPath}>`);
}
}
const msg = `Transforming from ${this.srcPath} to ${this.dstPath} using ${this.jsFile}`;
ux.log(`${chalk.yellow(msg)}`);
// eslint-disable-next-line no-useless-catch
try {
// Import transformation function
const transFunction = yield this.importTransformationFunction();
// Execute transformation function
const transResult = yield this.executeTransformationFunction(transFunction);
fs.outputFileSync(this.dstPath, transResult, { encoding: 'utf8', mode: 0o644, flag: 'w' });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
throw error;
}
});
}
}
class RemovalTransformation {
constructor(srcPath) {
this.name = this.constructor.name;
this.srcPath = srcPath;
}
transform() {
try {
fs.accessSync(this.srcPath, fs.constants.W_OK);
ux.log(`Removing ${this.srcPath}.`);
fs.removeSync(this.srcPath);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
if (error.code === 'ENOENT') {
const errMsg = `${this.name}: target does not exist, skipping removal <${this.srcPath}>`;
ux.log(`${chalk.yellow(errMsg)}`);
}
else {
throw new Error(`${this.name}: ${error} <${this.srcPath}>`);
}
}
}
}
class PrepareContent extends CommandBase {
parseDotfileLine(line, sourceDirectory) {
return __awaiter(this, void 0, void 0, function* () {
const m = line.match('^([!#])?(.+?)({.*?})?$');
const dotfileLine = {
prefix: m[1],
source: path.join(sourceDirectory, path.normalize(m[2].trim())), // should it be absolute path?
transformData: m[3],
};
/*
Sanity check on source file name. Should not contain "{", ":", or "}" characters
*/
const vm = dotfileLine.source.match('[{:}]+');
if (vm !== null) {
throw new SyntaxError(`<${PrepareContent.dotfile}> source file name '${dotfileLine.source}' contains invalid character '${vm[0]}'`);
}
return dotfileLine;
});
}
readDotfile(sourceDirectory) {
return __awaiter(this, void 0, void 0, function* () {
let fileContent;
try {
fileContent = fs.readFileSync(path.join(sourceDirectory, PrepareContent.dotfile), 'utf8');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
if (error.code === 'ENOENT') {
const errMsg = `.opsprepare file not found in ${path.resolve(sourceDirectory)}`;
throw new Error(errMsg);
}
throw new Error(error.message);
}
const lines = fileContent.split(/\r?\n/).filter(line => line.length > 0);
const lineParse = lines.map(line => this.parseDotfileLine(line.trim(), sourceDirectory));
const parsedLines = Promise.all(lineParse).then(lines => lines.filter(line => line.prefix !== '#'));
return parsedLines;
});
}
getTransformation(lineData, baseSourcedir, baseDestinationDir) {
return __awaiter(this, void 0, void 0, function* () {
const targetPath = path.join(baseDestinationDir, lineData.source);
/*
Anything prefixed with ! is a removal
*/
if (lineData.prefix === '!') {
return new RemovalTransformation(targetPath);
}
/*
In case transformation data is present, prepare transformations based on requested plugin.
*/
if (lineData.transformData) {
const transformType = lineData.transformData.match('^{(.+?):.*}$')[1];
switch (transformType) {
case 'cp': {
const mPath = lineData.transformData.match('^{cp:([^:]+)}$');
if (!mPath) {
throw new SyntaxError(`<${PrepareContent.dotfile}> syntax '${lineData.transformData}' is invalid for 'cp' transformation type`);
}
const tPath = path.join(baseDestinationDir, path.dirname(lineData.source), mPath[1]);
return new CopyTransformation(lineData.source, tPath);
}
case 'js': {
/* TODO: Some sanity checking will be required after regex matching
to make sure everything was matched and is defined
*/
// Parse transformation data
const mData = lineData.transformData.match('^{js:(.+?).js}$');
if (!mData) {
throw new SyntaxError(`<${PrepareContent.dotfile}> syntax '${lineData.transformData}' is invalid for 'js' transformation type`);
}
const jsFile = 'file://' + path.resolve(path.join(baseSourcedir, mData[1])) + '.js';
// File path absolute
const filePath = path.resolve(lineData.source);
return new JavascriptTransformation(filePath, targetPath, jsFile);
}
default: {
const p = lineData.prefix || '';
const s = lineData.source;
const t = lineData.transformData || '';
this.error(`No valid transformation can be extrapolated from ${p}${s}${t}`);
}
}
}
/*
Return default transformation
*/
return new CopyTransformation(lineData.source, targetPath);
});
}
prepareTransformations(linesData, baseSourceDir, baseDestinationDir) {
return __awaiter(this, void 0, void 0, function* () {
const transformations = linesData.map(line => this.getTransformation(line, baseSourceDir, baseDestinationDir));
return Promise.all(transformations);
});
}
/*
This function divides transformations into execution groups based on their class
and runs transformations for each group
*/
runTransformations(transformations) {
return __awaiter(this, void 0, void 0, function* () {
const copyTransformList = [];
const modlueTransformList = [];
const excludeList = [];
// Sort transformation into two lists
for (const t of transformations) {
if (t instanceof CopyTransformation) {
copyTransformList.push(t);
}
else if (t instanceof RemovalTransformation) {
excludeList.push(t);
}
else {
modlueTransformList.push(t);
}
}
const t = (transformation) => __awaiter(this, void 0, void 0, function* () {
try {
transformation.transform();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
throw Promise.reject(error.message).catch(error_ => {
this.log(`${chalk.redBright('ERROR: ' + error_)}`);
});
}
});
const doCopyTransform = copyTransformList.map(t);
const doModuleTransform = modlueTransformList.map(t);
const doExclude = excludeList.map(t);
/* Copy first, then transform and finally remove files. This should
ensure that transformations can overwrite previous copies. Removals
can still erase transformations though. If needed, we can remove
before transforming.
*/
yield Promise.all(doCopyTransform);
yield Promise.all(doModuleTransform);
yield Promise.all(doExclude);
});
}
/*
Validates file system on source repository by checking if files or directories exist
*/
/*
async validate(arr: string[][], source: string): Promise<boolean> {
//this.log(`${chalk.blue('Validating if files and directories exist.')}`);
cli.action.start(`${chalk.blue('Validating if files and directories exist')}`, '', {stdout: true});
for (let a of arr[0]) {
let element_path = path.join(source, a);
if(!(fs.existsSync(path.join(element_path)))) {
this.log(`${chalk.red('File or directory: ' + path.join(element_path) + ' could not be found')}`);
return false;
}
}
for (let a of arr[1]) {
let element_path = path.join(source, a);
if(!(fs.existsSync(element_path))) {
this.log(`${chalk.red('File or directory: ' + element_path + ' could not be found')}`);
return false;
}
}
//this.log(`${chalk.green('Validation done.')}`);
cli.action.stop(`${chalk.green('done')}`);
return true;
}
*/
/*
This function copies elements and removes excluded ones.
*/
/*
async copyWorkspace(arr: string[][], dest: string, source: string): Promise<any> {
if (await this.validate(arr, source)) {
cli.action.start(`${chalk.blue('Preparing content')}`, '', {stdout: true});
arr[0].forEach(a => {
this.log('Copying: ', path.join(source, a));
fs.copySync(path.join(source, a), path.join(dest, a), {overwrite: true});
});
this.log(`${chalk.green('Copying done.')}`);
this.log(`${chalk.blue('Removing excluded elements.')}`);
arr[1].forEach(a => {
let curPath = path.join(dest, a);
if (fs.pathExistsSync(curPath)) {
if (fs.lstatSync(curPath).isFile()) {
try {
fs.removeSync(curPath);
this.log('Removing file: ', curPath);
} catch (err: any) {
cli.action.stop(`${chalk.red('failed')}`);
this.error(error.message);
}
} else if (fs.lstatSync(curPath).isDirectory()) {
try {
fs.removeSync(curPath);
this.log('Removing dir: ', curPath);
} catch (err: any) {
cli.action.stop(`${chalk.red('failed')}`);
this.error(error.message);
}
} else {
cli.action.stop(`${chalk.red('error')}`);
this.error('Type not file or directory');
}
}
});
this.log(`${chalk.green('Removing done.')}`);
cli.action.stop(`${chalk.green('done')}`);
} else {
cli.action.stop(`${chalk.red('error')}`);
this.error('Content validation failed.');
}
}
*/
run() {
const _super = Object.create(null, {
run: { get: () => super.run }
});
return __awaiter(this, void 0, void 0, function* () {
_super.run.call(this);
const { flags } = yield this.parse(PrepareContent);
// read the flags and set paths relative to execution directory
let source = path.relative(process.cwd(), flags.source);
source = source || '.';
console.log(source);
const dest = flags.destination;
try {
const dotfileLines = yield this.readDotfile(source);
this.log(`${chalk.blue('Content transformation started.')}`);
const transformations = yield this.prepareTransformations(dotfileLines, source, dest);
yield this.runTransformations(transformations);
this.log(`${chalk.green('Content prepared at: ', dest)}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
this.log(`${chalk.red(error)}`);
/*
In test mode, exit with status 0
*/
if (process.env.OPS_TEST_MODE === 'true') {
this.exit(0);
}
console.log(error.message);
this.exit(1);
}
});
}
}
PrepareContent.dotfile = '.opsprepare';
PrepareContent.description = `copies files as described in ${PrepareContent.dotfile} which is at root of source directory`;
PrepareContent.usage = 'prepare-content -d <destination> [-s <source>]';
PrepareContent.examples = [
`$ ops prepare-content -d .\\my_workspace`,
`$ ops prepare-content -d .\\my_workspace -s .\\my_repository`,
`$ ops prepare-content -d C:\\git\\my_workspace`,
`$ ops prepare-content -d C:\\git\\my_workspace -s C:\\git\\my_repository\\my_repository`,
];
PrepareContent.flags = {
destination: Flags.string({
char: 'd',
description: 'where to copy the workspace files',
required: true,
}),
source: Flags.string({
char: 's',
description: '[optional] source directory, default: current dir',
required: false,
default: '.',
}),
};
export default PrepareContent;