🥂 Initial commit
This commit is contained in:
43
tools/build.js
Normal file
43
tools/build.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { join } from 'path';
|
||||
import { cwd } from 'process';
|
||||
import { mkdirSync, rmSync, existsSync } from 'fs';
|
||||
import { buildScss } from './tasks/scss.js';
|
||||
import { buildImg } from './tasks/img.js';
|
||||
import { buildTemplates } from './tasks/templates.js';
|
||||
import { Logger } from './utils/logger.js';
|
||||
import { buildFonts } from './tasks/fonts.js';
|
||||
const srcPath = join(cwd(), 'src');
|
||||
const distPath = join(cwd(), 'dist');
|
||||
|
||||
const logger = new Logger('build', 'info', 'brightMagenta');
|
||||
|
||||
function exit(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log('')
|
||||
logger.info('Build completed successfully');
|
||||
}
|
||||
|
||||
process.exit(err ? 1 : 0);
|
||||
}
|
||||
|
||||
async function build() {
|
||||
// cleaning, remove dist folder
|
||||
if (existsSync(distPath)) {
|
||||
rmSync(distPath, { recursive: true });
|
||||
}
|
||||
|
||||
// recreate dist folder
|
||||
mkdirSync(distPath, { recursive: true });
|
||||
|
||||
// start building tasks
|
||||
await Promise.all([
|
||||
buildScss(srcPath, distPath),
|
||||
buildImg(srcPath, distPath),
|
||||
buildFonts(srcPath, distPath),
|
||||
buildTemplates(srcPath, distPath),
|
||||
]);
|
||||
}
|
||||
|
||||
build().then(exit).catch(exit);
|
34
tools/deploy.js
Normal file
34
tools/deploy.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { Logger } from './utils/logger.js';
|
||||
import { getArg } from './utils/funcs.js';
|
||||
import { resolve } from 'path';
|
||||
import { cwd } from 'process';
|
||||
import { deploy } from './tasks/deploy.js';
|
||||
|
||||
const logger = new Logger('deploy', 'info', 'brightMagenta');
|
||||
|
||||
const src = './src';
|
||||
const dist = './dist';
|
||||
const serviceName = getArg('--service', 'gitea');
|
||||
const srcPath = resolve(cwd(), src);
|
||||
const distPath = resolve(cwd(), dist);
|
||||
const serverPath = resolve(
|
||||
cwd(),
|
||||
getArg('--sever', 'D:/users/lucas/Desktop/dev/server/gitea')
|
||||
);
|
||||
|
||||
logger.info('Deploy started!');
|
||||
logger.info(`Service name: '${serviceName}'`);
|
||||
logger.info(`Src path: ${srcPath}`);
|
||||
logger.info(`Dist path: ${distPath}`);
|
||||
logger.info(`Server path: ${serverPath}`);
|
||||
|
||||
function exit(err) {
|
||||
err && logger.error(err);
|
||||
process.exit(err ? 1 : 0);
|
||||
}
|
||||
|
||||
async function executeDeployTask() {
|
||||
await deploy(srcPath, distPath, serverPath, serviceName);
|
||||
}
|
||||
|
||||
executeDeployTask().then(exit).catch(exit);
|
19
tools/restart.js
Normal file
19
tools/restart.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { getArg } from './utils/funcs.js';
|
||||
import { restartService } from './tasks/restart-service.js';
|
||||
import { Logger } from './utils/logger.js';
|
||||
|
||||
const logger = new Logger('restart', 'info', 'brightMagenta');
|
||||
const serviceName = getArg('--service', 'gitea');
|
||||
|
||||
function exit(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log('');
|
||||
logger.info('Build completed successfully');
|
||||
}
|
||||
|
||||
process.exit(err ? 1 : 0);
|
||||
}
|
||||
|
||||
restartService(serviceName).then(exit).catch(exit);
|
40
tools/serve.js
Normal file
40
tools/serve.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { watch } from 'chokidar';
|
||||
import { TaskDebouncer } from './utils/task-debouncer.js';
|
||||
import { Logger } from './utils/logger.js';
|
||||
import { getArg } from './utils/funcs.js';
|
||||
import { resolve } from 'path';
|
||||
import { cwd } from 'process';
|
||||
import { deploy } from './tasks/deploy.js';
|
||||
|
||||
const src = './src';
|
||||
const dist = './dist';
|
||||
const serviceName = getArg('--service', 'gitea');
|
||||
const srcPath = resolve(cwd(), src);
|
||||
const distPath = resolve(cwd(), dist);
|
||||
const serverPath = resolve(
|
||||
cwd(),
|
||||
getArg('--sever', 'D:/users/lucas/Desktop/dev/server/gitea')
|
||||
);
|
||||
const debouncer = new TaskDebouncer(300);
|
||||
const logger = new Logger('serve', 'info', 'brightMagenta');
|
||||
|
||||
logger.info('Serve task started!');
|
||||
logger.info('Watching for changes...');
|
||||
logger.info(`Service name: '${serviceName}'`);
|
||||
logger.info(`Src path: ${srcPath}`);
|
||||
logger.info(`Dist path: ${distPath}`);
|
||||
logger.info(`Server path: ${serverPath}`);
|
||||
|
||||
const watcher = watch([`${src}/**/*`], {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on('change', (file) => debouncer.add(
|
||||
deploy,
|
||||
srcPath,
|
||||
distPath,
|
||||
serverPath,
|
||||
serviceName,
|
||||
file,
|
||||
));
|
34
tools/tasks/copy-to.js
Normal file
34
tools/tasks/copy-to.js
Normal file
@ -0,0 +1,34 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
|
||||
const logger = new Logger(copyTo.name, 'info', 'brightYellow');
|
||||
|
||||
export async function copyTo(sourcePath, targetPath) {
|
||||
logger.info(`Copying ${sourcePath} to ${targetPath}`);
|
||||
await recursiveCopy(sourcePath, targetPath);
|
||||
logger.info(`Copy has finished!`);
|
||||
}
|
||||
|
||||
async function recursiveCopy(sourcePath, targetPath) {
|
||||
// Create the target directory if it doesn't exist
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
fs.mkdirSync(targetPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all files and directories in the source path
|
||||
const files = fs.readdirSync(sourcePath, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const sourceFile = path.join(sourcePath, file.name);
|
||||
const targetFile = path.join(targetPath, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
// Recursively copy directories
|
||||
await recursiveCopy(sourceFile, targetFile);
|
||||
} else {
|
||||
// Copy files
|
||||
fs.copyFileSync(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
}
|
33
tools/tasks/deploy.js
Normal file
33
tools/tasks/deploy.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { buildScss } from './scss.js';
|
||||
import { buildFonts } from './fonts.js';
|
||||
import { buildTemplates } from './templates.js';
|
||||
import { copyTo } from './copy-to.js';
|
||||
import { restartService } from './restart-service.js';
|
||||
import { extname } from 'path';
|
||||
|
||||
const logger = new Logger('deploy', 'info', 'brightMagenta');
|
||||
|
||||
export async function deploy(srcPath, distPath, serverPath, serviceName, file = null) {
|
||||
logger.info('Deploying...');
|
||||
|
||||
let shouldRestart = true;
|
||||
|
||||
// check if it's an scss
|
||||
if (file !== null && file !== undefined && extname(file) === '.scss') {
|
||||
shouldRestart = false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await buildScss(srcPath, distPath);
|
||||
await buildFonts(srcPath, distPath);
|
||||
await buildTemplates(srcPath, distPath);
|
||||
await copyTo(distPath, serverPath);
|
||||
shouldRestart && await restartService(serviceName);
|
||||
|
||||
logger.info('Deployment successful!');
|
||||
} catch (error) {
|
||||
logger.error(`Deployment failed: ${error}`);
|
||||
}
|
||||
}
|
32
tools/tasks/fonts.js
Normal file
32
tools/tasks/fonts.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { copyFileSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFiles } from '../utils/funcs.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
|
||||
const logger = new Logger(buildFonts.name, 'info', 'brightCyan');
|
||||
const imgSrc = 'themes/fonts';
|
||||
const imgDest = '/public/fonts';
|
||||
|
||||
export async function buildFonts(srcHome, distHome) {
|
||||
logger.info('Fonts build has started');
|
||||
const fontsSrcPath = join(srcHome, imgSrc);
|
||||
const fontsDestPath = join(distHome, imgDest);
|
||||
|
||||
mkdirSync(fontsDestPath, { recursive: true });
|
||||
|
||||
const files = readFiles(fontsSrcPath, [
|
||||
'.woff',
|
||||
'.woff2',
|
||||
'.ttf',
|
||||
'.eot',
|
||||
'.svg',
|
||||
'.otf',
|
||||
]);
|
||||
|
||||
for (const file of files) {
|
||||
// just copy the file
|
||||
copyFileSync(join(fontsSrcPath, file), join(fontsDestPath, file));
|
||||
}
|
||||
|
||||
logger.info('Fonts build has finished');
|
||||
}
|
129
tools/tasks/img.js
Normal file
129
tools/tasks/img.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { fabric } from 'fabric';
|
||||
import imageminZopfli from 'imagemin-zopfli';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { join, basename } from 'path';
|
||||
import { optimize } from 'svgo';
|
||||
import { readFiles } from '../utils/funcs.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { mkdirSync, copyFileSync } from 'fs';
|
||||
|
||||
const logger = new Logger(buildImg.name, 'info', 'brightGreen');
|
||||
const imgSrc = 'themes/img';
|
||||
const imgDest = '/public/img';
|
||||
|
||||
export async function buildImg(srcHome, distHome) {
|
||||
logger.info('Images build has started');
|
||||
const imgSrcPath = join(srcHome, imgSrc);
|
||||
const imgDestPath = join(distHome, imgDest);
|
||||
const images = { logos: { logo: undefined, favicon: undefined }, others: [] };
|
||||
mkdirSync(imgDestPath, { recursive: true });
|
||||
|
||||
const files = readFiles(imgSrcPath, ['.svg', '.png', '.jpg', '.webp', '.gif']);
|
||||
|
||||
// Separate logo.svg and favicon.svg from the rest
|
||||
files.forEach((file) => {
|
||||
if (file === 'logo.svg') {
|
||||
images.logos.logo = join(imgSrcPath, file);
|
||||
} else if (file === 'favicon.svg') {
|
||||
images.logos.favicon = join(imgSrcPath, file);
|
||||
} else {
|
||||
images.others.push(join(imgSrcPath, file));
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
processLogos(images.logos, imgDestPath),
|
||||
processOthers(images.others, imgDestPath),
|
||||
])
|
||||
|
||||
logger.info('Images build has finished');
|
||||
}
|
||||
|
||||
async function processLogos(logos, distHome) {
|
||||
const promises = [];
|
||||
|
||||
if (logos.logo) {
|
||||
const svg = await readFile(logos.logo, 'utf8');
|
||||
promises.push(generate(svg, join(distHome, 'logo.svg'), { size: 32 }));
|
||||
promises.push(generate(svg, join(distHome, 'logo.png'), { size: 512 }));
|
||||
}
|
||||
|
||||
if (logos.favicon) {
|
||||
const svg = await readFile(logos.favicon, 'utf8');
|
||||
promises.push(
|
||||
generate(svg, join(distHome, 'favicon.svg'), { size: 32 }),
|
||||
generate(svg, join(distHome, 'favicon.png'), { size: 180 }),
|
||||
generate(svg, join(distHome, 'apple-touch-icon.png'), { size: 180, bg: true }),
|
||||
generate(svg, join(distHome, 'avatar_default.png'), { size: 200, bg: true })
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
function loadSvg(svg) {
|
||||
return new Promise((resolve) => {
|
||||
fabric.loadSVGFromString(svg, (objects, options) => {
|
||||
resolve({ objects, options });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function generate(svg, path, { size, bg }) {
|
||||
if (String(path).endsWith('.svg')) {
|
||||
const { data } = optimize(svg, {
|
||||
plugins: [
|
||||
'preset-default',
|
||||
'removeDimensions',
|
||||
{
|
||||
name: 'addAttributesToSVGElement',
|
||||
params: { attributes: [{ height: size }] },
|
||||
},
|
||||
],
|
||||
});
|
||||
await writeFile(path, data);
|
||||
return;
|
||||
}
|
||||
|
||||
const { objects, options } = await loadSvg(svg);
|
||||
const canvas = new fabric.Canvas();
|
||||
|
||||
|
||||
const newWidth = size * options.width / options.height;
|
||||
canvas.setDimensions({ width: newWidth, height: size });
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(
|
||||
options.width ? newWidth / options.width : 1,
|
||||
options.height ? size / options.height : 1
|
||||
);
|
||||
|
||||
if (bg) {
|
||||
canvas.add(
|
||||
new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: size * (1 / (size / options.height)),
|
||||
width: size * (1 / (size / options.width)),
|
||||
fill: 'black',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
canvas.add(fabric.util.groupSVGElements(objects, options));
|
||||
canvas.renderAll();
|
||||
|
||||
let png = Buffer.from([]);
|
||||
for await (const chunk of canvas.createPNGStream()) {
|
||||
png = Buffer.concat([png, chunk]);
|
||||
}
|
||||
|
||||
png = await imageminZopfli({ more: true })(png);
|
||||
await writeFile(path, png);
|
||||
}
|
||||
|
||||
async function processOthers(others, distHome) {
|
||||
// just copy the rest of the images to dist
|
||||
for (const img of others) {
|
||||
copyFileSync(img, join(distHome, basename(img)));
|
||||
}
|
||||
}
|
31
tools/tasks/restart-service.js
Normal file
31
tools/tasks/restart-service.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
import { Logger } from '../utils/logger.js';
|
||||
const logger = new Logger(restartService.name, 'info', 'brightRed');
|
||||
|
||||
export async function restartService(serviceName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`Restarting '${serviceName}' service...`);
|
||||
|
||||
let command;
|
||||
let args;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
command = 'cmd.exe';
|
||||
args = ['/c', 'net', 'stop', serviceName, '&&', 'net', 'start', serviceName];
|
||||
} else {
|
||||
command = 'sudo';
|
||||
args = ['systemctl', 'restart', serviceName];
|
||||
}
|
||||
|
||||
exec(`${command} ${args.join(' ')}`, (error, stdout) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to restart '${serviceName}' service: ${error}`);
|
||||
reject(error);
|
||||
} else {
|
||||
logger.info(`'${serviceName}' service restarted!`);
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
64
tools/tasks/scss.js
Normal file
64
tools/tasks/scss.js
Normal file
@ -0,0 +1,64 @@
|
||||
import { mkdirSync, readdirSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { compile } from 'sass';
|
||||
|
||||
const logger = new Logger(buildScss.name, 'debug', 'pink');
|
||||
const scss_home = 'themes/scss';
|
||||
const css_home = '/public/css';
|
||||
|
||||
export async function buildScss(src_home, dist_home) {
|
||||
logger.info('SCSS build has started');
|
||||
const themes = get_themes(src_home);
|
||||
mkdirSync(join(dist_home, css_home), { recursive: true });
|
||||
|
||||
for (const theme of themes) {
|
||||
logger.debug(`Building ${theme.name} theme`);
|
||||
|
||||
const result = compile(theme.path, {
|
||||
loadPaths: [join(src_home, scss_home), join(src_home, '../node_modules')],
|
||||
quietDeps: true,
|
||||
logger: {
|
||||
debug: logger.simpleDebug.bind(logger),
|
||||
info: logger.simpleInfo.bind(logger),
|
||||
warn: logger.simpleWarn.bind(logger),
|
||||
error: logger.simpleError.bind(logger),
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug(`Writing ${theme.name} theme to disk`);
|
||||
|
||||
writeFileSync(
|
||||
join(dist_home, css_home, `theme-${theme.name}.css`),
|
||||
result.css
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('SCSS build has finished');
|
||||
}
|
||||
|
||||
|
||||
function get_themes(src_home) {
|
||||
return readdirSync(join(src_home, scss_home)).filter(
|
||||
(fn) => fn.endsWith('.scss') && !fn.startsWith('_')
|
||||
).map((file) => ({
|
||||
name: file.replace('.scss', ''),
|
||||
path: join(src_home, scss_home, file),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
// for (const flavor of Object.keys(variants)) {
|
||||
// for (const accent of accents) {
|
||||
// const input = builder(flavor, accent);
|
||||
// const result = compileString(input, {
|
||||
// loadPaths: [join(__dirname, 'src'), join(__dirname, 'node_modules')],
|
||||
// });
|
||||
|
||||
// mkdirSync(join(__dirname, 'dist'), { recursive: true });
|
||||
// writeFileSync(
|
||||
// join(__dirname, 'dist', `theme-catppuccin-${flavor}-${accent}.css`),
|
||||
// result.css
|
||||
// );
|
||||
// }
|
||||
// }
|
17
tools/tasks/templates.js
Normal file
17
tools/tasks/templates.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { join } from 'path';
|
||||
import { copyFolderRecursiveSync } from '../utils/funcs.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
|
||||
const logger = new Logger(buildTemplates.name, 'info', 'blue');
|
||||
const imgSrc = 'templates';
|
||||
const imgDest = '/';
|
||||
|
||||
export async function buildTemplates(srcHome, distHome) {
|
||||
logger.info('Fonts build has started');
|
||||
const tmplSrcPath = join(srcHome, imgSrc);
|
||||
const tmplDestPath = join(distHome, imgDest);
|
||||
|
||||
// just copy the entire tmplSrcPath to tmplDestPath
|
||||
copyFolderRecursiveSync(tmplSrcPath, tmplDestPath);
|
||||
logger.info('Templates build has finished');
|
||||
}
|
162
tools/utils/funcs.js
Normal file
162
tools/utils/funcs.js
Normal file
@ -0,0 +1,162 @@
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { basename, join } from 'path';
|
||||
|
||||
export function readFiles(path, extensions) {
|
||||
return readdirSync(path).filter((file) =>
|
||||
extensions.some((ext) => file.endsWith(ext))
|
||||
);
|
||||
}
|
||||
|
||||
function copyFileSync(source, target) {
|
||||
var targetFile = target;
|
||||
|
||||
// If target is a directory, a new file with the same name will be created
|
||||
if (existsSync(target)) {
|
||||
if (lstatSync(target).isDirectory()) {
|
||||
targetFile = join(target, basename(source));
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(targetFile, readFileSync(source));
|
||||
}
|
||||
|
||||
export function copyFolderRecursiveSync(source, target) {
|
||||
var files = [];
|
||||
|
||||
var targetFolder = join(target, basename(source));
|
||||
if (!existsSync(targetFolder)) {
|
||||
mkdirSync(targetFolder);
|
||||
}
|
||||
|
||||
// Copy
|
||||
if (lstatSync(source).isDirectory()) {
|
||||
files = readdirSync(source);
|
||||
files.forEach(function (file) {
|
||||
var curSource = join(source, file);
|
||||
if (lstatSync(curSource).isDirectory()) {
|
||||
copyFolderRecursiveSync(curSource, targetFolder);
|
||||
} else {
|
||||
copyFileSync(curSource, targetFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bjoerge/debounce-promise/blob/master/index.js
|
||||
export function debounce(fn, wait = 0, options = {}) {
|
||||
let lastCallAt;
|
||||
let deferred;
|
||||
let timer;
|
||||
let pendingArgs = [];
|
||||
return function debounced(...args) {
|
||||
const currentWait = getWait(wait);
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
const isCold = !lastCallAt || currentTime - lastCallAt > currentWait;
|
||||
|
||||
lastCallAt = currentTime;
|
||||
|
||||
if (isCold && options.leading) {
|
||||
return options.accumulate
|
||||
? Promise.resolve(fn.call(this, [args])).then((result) => result[0])
|
||||
: Promise.resolve(fn.call(this, ...args));
|
||||
}
|
||||
|
||||
if (deferred) {
|
||||
clearTimeout(timer);
|
||||
} else {
|
||||
deferred = defer();
|
||||
}
|
||||
|
||||
pendingArgs.push(args);
|
||||
timer = setTimeout(flush.bind(this), currentWait);
|
||||
|
||||
if (options.accumulate) {
|
||||
const argsIndex = pendingArgs.length - 1;
|
||||
return deferred.promise.then((results) => results[argsIndex]);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
function flush() {
|
||||
const thisDeferred = deferred;
|
||||
clearTimeout(timer);
|
||||
|
||||
Promise.resolve(
|
||||
options.accumulate
|
||||
? fn.call(this, pendingArgs)
|
||||
: fn.apply(this, pendingArgs[pendingArgs.length - 1])
|
||||
).then(thisDeferred.resolve, thisDeferred.reject);
|
||||
|
||||
pendingArgs = [];
|
||||
deferred = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getWait(wait) {
|
||||
return typeof wait === 'function' ? wait() : wait;
|
||||
}
|
||||
|
||||
function defer() {
|
||||
const deferred = {};
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
return deferred;
|
||||
}
|
||||
|
||||
export async function sequence(tasks) {
|
||||
const results = [];
|
||||
for (const task of tasks) {
|
||||
results.push(await task());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
async function tasksRunner(tasks, abort) {
|
||||
let result = null;
|
||||
for (const task of tasks) {
|
||||
if (abort.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
result = await task(result);
|
||||
}
|
||||
}
|
||||
|
||||
// each task should return a promise
|
||||
// each task takes the result of the previous task as an argument
|
||||
// the output of the last task is the output of the sequence
|
||||
export async function sequenceStream(tasks) {
|
||||
const abort = new AbortController();
|
||||
|
||||
abort.signal.addEventListener('abort', () => {
|
||||
console.log('sequenceStream aborted');
|
||||
});
|
||||
|
||||
return [
|
||||
tasksRunner(tasks, abort),
|
||||
abort,
|
||||
]
|
||||
}
|
||||
|
||||
export const getArg = (flag, def) => {
|
||||
const args = process.argv.slice(2);
|
||||
const flagIndex = args.findIndex(arg => arg === flag);
|
||||
|
||||
if (flagIndex !== -1 && flagIndex + 1 < args.length) {
|
||||
return args[flagIndex + 1];
|
||||
}
|
||||
|
||||
return def || null;
|
||||
};
|
105
tools/utils/logger.js
Normal file
105
tools/utils/logger.js
Normal file
@ -0,0 +1,105 @@
|
||||
const LOG_LEVEL_MAP = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
const ANSI_COLORS = {
|
||||
reset: '\x1b[0m',
|
||||
black: '\x1b[30m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
gray: '\x1b[90m',
|
||||
brightRed: '\x1b[91m',
|
||||
brightGreen: '\x1b[92m',
|
||||
brightYellow: '\x1b[93m',
|
||||
brightBlue: '\x1b[94m',
|
||||
brightMagenta: '\x1b[95m',
|
||||
brightCyan: '\x1b[96m',
|
||||
pink: '\x1b[38;2;255;182;193m'
|
||||
};
|
||||
|
||||
export class Logger {
|
||||
/**
|
||||
* @param {string} ctx
|
||||
* @param {'debug'|'info'|'warn'|'error'} log_level - default: 'debug'
|
||||
* @param {'red'|'green'|'yellow'|'blue'|'magenta'|'cyan'|'white'|'gray'|'brightRed'|
|
||||
* 'brightGreen'|'brightYellow'|'brightBlue'|'brightMagenta'|'brightCyan'|'pink'} color - default: 'magenta'
|
||||
*/
|
||||
constructor(ctx, log_level, color = 'magenta') {
|
||||
this.ctx = ctx;
|
||||
this.log_level = LOG_LEVEL_MAP[log_level || 'debug'];
|
||||
this.color = ANSI_COLORS[color] || ANSI_COLORS.reset;
|
||||
|
||||
if (this.log_level === undefined) {
|
||||
throw new Error(`Invalid log level: ${log_level}`);
|
||||
}
|
||||
}
|
||||
|
||||
debug(...args) {
|
||||
if (!this.#canLog('debug')) return;
|
||||
this.log('DEBUG', false, ...args);
|
||||
}
|
||||
|
||||
info(...args) {
|
||||
if (!this.#canLog('info')) return;
|
||||
this.log('INFO', false, ...args);
|
||||
}
|
||||
|
||||
warn(...args) {
|
||||
if (!this.#canLog('warn')) return;
|
||||
this.log('WARN', false, ...args);
|
||||
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
if (!this.#canLog('error')) return;
|
||||
this.log('ERROR', false, ...args);
|
||||
}
|
||||
|
||||
log(level, simple, ...args) {
|
||||
if (simple) {
|
||||
args = [args[0]];
|
||||
}
|
||||
|
||||
if (level === 'ERROR') {
|
||||
console.error(
|
||||
`${this.color}[${level}] [${this.ctx}]${ANSI_COLORS.reset}`,
|
||||
...args
|
||||
);
|
||||
} else {
|
||||
console.log('🍵', `${this.color}[${this.ctx}]${ANSI_COLORS.reset}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
simpleDebug(...args) {
|
||||
if (!this.#canLog('debug')) return;
|
||||
this.log('DEBUG', true, ...args);
|
||||
}
|
||||
|
||||
simpleInfo(...args) {
|
||||
if (!this.#canLog('info')) return;
|
||||
this.log('INFO', true, ...args);
|
||||
}
|
||||
|
||||
simpleWarn(...args) {
|
||||
if (!this.#canLog('warn')) return;
|
||||
this.log('WARN', true, ...args);
|
||||
|
||||
}
|
||||
|
||||
simpleError(...args) {
|
||||
if (!this.#canLog('error')) return;
|
||||
this.log('ERROR', true, ...args);
|
||||
}
|
||||
|
||||
#canLog(level) {
|
||||
return this.log_level <= LOG_LEVEL_MAP[level];
|
||||
}
|
||||
}
|
67
tools/utils/task-debouncer.js
Normal file
67
tools/utils/task-debouncer.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* #### TaskDebouncer
|
||||
*
|
||||
* Executes a task after a certain delay, but cancels the execution if
|
||||
* a new task is sent before the delay expires. Also, if a task is
|
||||
* already being executed, the new task is queued and executed after
|
||||
* the current one finishes. It will only execute the task that was
|
||||
* sent last.
|
||||
*/
|
||||
export class TaskDebouncer {
|
||||
constructor(debounceDelay) {
|
||||
this.debounceDelay = debounceDelay;
|
||||
this.queued = undefined;
|
||||
this.isProcessing = false;
|
||||
this.timerId = null;
|
||||
}
|
||||
|
||||
#clearQueue() {
|
||||
this.queued = undefined;
|
||||
}
|
||||
|
||||
#enqueue(executor, args) {
|
||||
this.queued = { executor, args };
|
||||
this.#processQueue();
|
||||
}
|
||||
|
||||
#setProcessing(value) {
|
||||
this.isProcessing = value;
|
||||
}
|
||||
|
||||
async #processQueue() {
|
||||
if (this.isProcessing || !this.queued) {
|
||||
return;
|
||||
}
|
||||
const { executor, args } = this.queued;
|
||||
this.#clearQueue();
|
||||
|
||||
// execute the task
|
||||
this.#setProcessing(true);
|
||||
await executor(...args);
|
||||
this.#setProcessing(false);
|
||||
|
||||
// continue with the next task
|
||||
this.#continue();
|
||||
}
|
||||
|
||||
#continue() {
|
||||
if (this.queued) {
|
||||
this.#processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task to the queue. If a task is already being executed,
|
||||
* the new task is queued and executed after the current one finishes.
|
||||
* It will only execute the task if no other task is sent before the
|
||||
* delay expires or before the current task finishes.
|
||||
*
|
||||
* IOW, it will only execute the task that was sent last.
|
||||
*/
|
||||
add(executor, ...args) {
|
||||
clearTimeout(this.timerId);
|
||||
this.timerId = setTimeout(() => {
|
||||
this.#enqueue(executor, args);
|
||||
}, this.debounceDelay);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user