1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-19 05:09:41 +02:00

Implement yaml config (#271)

This commit is contained in:
Nikita Melnikov 2022-09-29 06:41:24 +08:00 committed by GitHub
parent 5a7f1c843b
commit 13762096c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 164 additions and 479 deletions

View file

@ -1,15 +0,0 @@
{
"title": "CodeX Docs",
"description": "A block-styled editor with clean JSON output",
"menu": [
"Guides",
{"title": "CodeX", "uri": "https://codex.so"}
],
"startPage": "",
"misprintsChatId": "12344564",
"yandexMetrikaId": "",
"carbon": {
"serve": "",
"placement": ""
}
}

1
.gitignore vendored
View file

@ -66,6 +66,7 @@ typings/
# Database files
.db/
db/
.testdb/
# Cache of babel and others

31
app-config.yaml Normal file
View file

@ -0,0 +1,31 @@
port: 3000
host: "localhost"
uploads: "./uploads"
password: secretpassword
frontend:
title: "CodeX Docs"
description: "A block-styled editor with clean JSON output"
startPage: ""
misprintsChatId: "12344564"
yandexMetrikaId: ""
carbon:
serve: ""
placement: ""
menu:
- "Guides"
- title: "CodeX"
uri: "https://codex.so"
auth:
secret: supersecret
hawk:
# frontendToken: "123"
# backendToken: "123"
database:
driver: local
local:
path: ./db
mongodb:
uri: mongodb://localhost:27017

View file

@ -1,8 +0,0 @@
{
"port": 3000,
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads",
"secret": "iamasecretstring",
"favicon": ""
}

View file

@ -1,8 +0,0 @@
{
"port": 3000,
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "/uploads",
"secret": "iamasecretstring",
"favicon": ""
}

View file

@ -1,8 +0,0 @@
{
"port": 3001,
"database": ".testdb",
"rcFile": "./src/test/.codexdocsrc",
"uploads": "public/uploads_test",
"secret": "iamasecretstring",
"favicon": ""
}

View file

@ -9,9 +9,6 @@ services:
env_file:
- .env
volumes:
- ./.env:/usr/src/app/.env
- ./.codexdocsrc:/usr/src/app/.codexdocsrc:ro
- ./config/production.json:/usr/src/app/config/production.json:ro
- ./public/uploads:/uploads
- ./.db:/usr/src/app/.db
- /usr/src/app/node_modules
- ./db:/usr/src/app/db
- ./app-config.yaml:/usr/src/app/app-config.yaml

View file

@ -2,7 +2,7 @@
FROM node:16.14.0-alpine3.15 as build
## Install build toolchain, install node deps and compile native add-ons
RUN apk add --no-cache python3 make g++
RUN apk add --no-cache python3 make g++ git
WORKDIR /usr/src/app

View file

@ -22,7 +22,9 @@
"@codexteam/shortcuts": "^1.2.0",
"@hawk.so/javascript": "^3.0.1",
"@hawk.so/nodejs": "^3.1.4",
"arg": "^5.0.2",
"config": "^3.3.6",
"config-loader": "https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26",
"cookie-parser": "^1.4.5",
"csurf": "^1.11.0",
"debug": "^4.3.2",
@ -40,7 +42,8 @@
"node-fetch": "^3.2.10",
"open-graph-scraper": "^4.9.0",
"twig": "^1.15.4",
"uuid4": "^2.0.2"
"uuid4": "^2.0.2",
"zod": "^3.19.1"
},
"devDependencies": {
"@babel/core": "^7.17.5",

View file

@ -3,15 +3,13 @@ import path from 'path';
import { fileURLToPath } from 'url';
import cookieParser from 'cookie-parser';
import morgan from 'morgan';
import rcParser from './utils/rcparser.js';
import routes from './routes/index.js';
import HttpException from './exceptions/httpException.js';
import * as dotenv from 'dotenv';
import config from 'config';
import HawkCatcher from '@hawk.so/nodejs';
import os from 'os';
import appConfig from 'config';
import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js';
import appConfig from "./utils/appConfig.js";
/**
* The __dirname CommonJS variables are not available in ES modules.
@ -22,20 +20,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config();
const app = express();
const localConfig = rcParser.getConfiguration();
const localConfig = appConfig.frontend;
// Initialize the backend error tracking catcher.
if (process.env.HAWK_TOKEN_BACKEND) {
HawkCatcher.init(process.env.HAWK_TOKEN_BACKEND);
if (appConfig.hawk?.backendToken) {
HawkCatcher.init(appConfig.hawk.backendToken);
}
// Get url to upload favicon from config
const favicon: string = appConfig.get('favicon');
const favicon = appConfig.favicon
app.locals.config = localConfig;
// Set client error tracking token as app local.
if (process.env.HAWK_TOKEN_CLIENT) {
app.locals.config.hawkClientToken = process.env.HAWK_TOKEN_CLIENT;
if (appConfig.hawk?.frontendToken) {
app.locals.config.hawkClientToken = appConfig.hawk.frontendToken;
}
// view engine setup
@ -69,7 +67,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../../public')));
app.use('/uploads', express.static(config.get('uploads')));
app.use('/uploads', express.static(appConfig.uploads));
app.use('/favicon', express.static(downloadedFaviconFolder));
app.use('/', routes);
@ -78,7 +76,7 @@ app.use('/', routes);
// global error handler
app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) {
// send any type of error to hawk server.
if (process.env.HAWK_TOKEN_BACKEND && err instanceof Error) {
if (appConfig.hawk?.backendToken && err instanceof Error) {
HawkCatcher.send(err);
}
// only send Http based exception to client.

View file

@ -2,10 +2,10 @@ import fileType from 'file-type';
import fetch from 'node-fetch';
import fs from 'fs';
import nodePath from 'path';
import config from 'config';
import File, { FileData } from '../models/file.js';
import crypto from '../utils/crypto.js';
import deepMerge from '../utils/objects.js';
import appConfig from "../utils/appConfig.js";
const random16 = crypto.random16;
@ -71,7 +71,7 @@ class Transport {
const type = await fileType.fromBuffer(buffer);
const ext = type ? type.ext : nodePath.extname(url).slice(1);
fs.writeFileSync(`${config.get('uploads')}/${filename}.${ext}`, buffer);
fs.writeFileSync(`${appConfig.uploads}/${filename}.${ext}`, buffer);
const fetchedContentType: string | null = fetchedFile.headers.get('content-type');
let fetchedMimeType: string | undefined;
@ -87,7 +87,7 @@ class Transport {
const file = new File({
name: url,
filename: `${filename}.${ext}`,
path: `${config.get('uploads')}/${filename}.${ext}`,
path: `${appConfig.uploads}/${filename}.${ext}`,
size: buffer.length,
mimetype: mimeType,
});

View file

@ -2,9 +2,9 @@ import { Request, Response, Router } from 'express';
import multer, { StorageEngine } from 'multer';
import mime from 'mime';
import mkdirp from 'mkdirp';
import config from 'config';
import Transport from '../../controllers/transport.js';
import { random16 } from '../../utils/crypto.js';
import appConfig from "../../utils/appConfig.js";
const router = Router();
@ -15,7 +15,7 @@ const router = Router();
*/
const storage: StorageEngine = multer.diskStorage({
destination: (req, file, cb) => {
const dir: string = config.get('uploads') || 'public/uploads';
const dir: string = appConfig.uploads || 'public/uploads';
mkdirp(dir);
cb(null, dir);

View file

@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import config from 'config';
import csrf from 'csurf';
import appConfig from "../utils/appConfig.js";
const router = express.Router();
const csrfProtection = csrf({ cookie: true });
@ -22,7 +22,7 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
*/
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
try {
if (!process.env.PASSWORD) {
if (!appConfig.password) {
res.render('auth', {
title: 'Login page',
header: 'Password not set',
@ -32,7 +32,7 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
return;
}
if (req.body.password !== process.env.PASSWORD) {
if (req.body.password !== appConfig.password) {
res.render('auth', {
title: 'Login page',
header: 'Wrong password',
@ -46,7 +46,7 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
iss: 'Codex Team',
sub: 'auth',
iat: Date.now(),
}, process.env.PASSWORD + config.get('secret'));
}, appConfig.password + appConfig.auth.secret);
res.cookie('authToken', token, {
httpOnly: true,

View file

@ -1,6 +1,6 @@
import config from 'config';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import appConfig from "../../utils/appConfig.js";
/**
@ -14,14 +14,14 @@ export default async function verifyToken(req: Request, res: Response, next: Nex
const token = req.cookies.authToken;
try {
if (!process.env.PASSWORD) {
if (!appConfig.password) {
res.locals.isAuthorized = false;
next();
return;
}
const decodedToken = jwt.verify(token, process.env.PASSWORD + config.get('secret'));
const decodedToken = jwt.verify(token, appConfig.password + appConfig.auth.secret);
res.locals.isAuthorized = !!decodedToken;

View file

@ -0,0 +1,74 @@
import { loadConfig } from 'config-loader';
import * as process from 'process';
import arg from 'arg';
import path from 'path';
import { z } from "zod";
/**
* Configuration for Hawk errors catcher
*/
const HawkConfig = z.object({
backendToken: z.string().optional(), // Hawk backend token
frontendToken: z.string().optional(), // Hawk frontend token
})
const LocalDatabaseConfig = z.object({
driver: z.literal('local'),
local: z.object({
path: z.string()
})
})
const AuthConfig = z.object({
secret: z.string() // Secret for JWT
})
const FrontendConfig = z.object({
title: z.string(), // Title for pages
description: z.string(), // Description for pages
startPage: z.string(), // Start page
misprintsChatId: z.string().optional(), // Telegram chat id for misprints
yandexMetrikaId: z.string().optional(), // Yandex metrika id
carbon: z.object({
serve: z.string().optional(), // Carbon serve url
placement: z.string().optional(), // Carbon placement
}),
menu: z.array(z.union([z.string(), z.object({title: z.string(), uri: z.string()})])), // Menu for pages
})
/**
* Application configuration
*/
const AppConfig = z.object({
port: z.number(), // Port to listen on
host: z.string(), // Host to listen on
favicon: z.string().optional(), // Path or URL to favicon
uploads: z.string(), // Path to uploads folder
hawk: HawkConfig.optional().nullable(), // Hawk configuration
password: z.string(), // Password for admin panel
frontend: FrontendConfig, // Frontend configuration
auth: AuthConfig, // Auth configuration
database: LocalDatabaseConfig, // Database configuration
})
export type AppConfig = z.infer<typeof AppConfig>;
const args = arg({ /* eslint-disable @typescript-eslint/naming-convention */
'--config': [ String ],
'-c': '--config',
});
const cwd = process.cwd();
const paths = (args['--config'] || ['./app-config.yaml']).map((configPath) => {
if (path.isAbsolute(configPath)) {
return configPath;
}
return path.join(cwd, configPath);
});
const loadedConfig = loadConfig<AppConfig>(...paths);
const appConfig = AppConfig.parse(loadedConfig)
export default appConfig;

View file

@ -1,6 +1,6 @@
import Datastore from 'nedb';
import config from 'config';
import path from 'path';
import appConfig from "../appConfig.js";
/**
* Init function for nedb instance
@ -10,7 +10,7 @@ import path from 'path';
*/
export default function initDb(name: string): Datastore {
return new Datastore({
filename: path.resolve(`${config.get('database')}/${name}.db`),
filename: path.resolve(`${appConfig.database.local.path}/${name}.db`),
autoload: true,
});
}

View file

@ -1,132 +0,0 @@
import fs from 'fs';
import path from 'path';
import config from 'config';
import { fileURLToPath } from 'url';
/**
* The __dirname CommonJS variables are not available in ES modules.
* https://nodejs.org/api/esm.html#no-__filename-or-__dirname
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rcPath = path.resolve(__dirname, '../../../', config.get('rcFile') || './.codexdocsrc');
/**
* @typedef {object} menu
* @property {string} title - menu option title
* @property {string} uri - menu option href
*/
interface Menu {
title: string;
uri: string;
[key: string]: string;
}
/**
* @typedef {object} RCData
* @property {string} title - website title
* @property {Menu[]} menu - options for website menu
*/
interface RCData {
title: string;
menu: Menu[];
[key: string]: string | Menu[];
}
/**
* @class RCParser
* @classdesc Class to parse runtime configuration file for CodeX Docs engine
*/
export default class RCParser {
/**
* Default CodeX Docs configuration
*
* @static
* @returns {{title: string, menu: Array}}
*/
public static get DEFAULTS():RCData {
return {
title: 'CodeX Docs',
menu: [],
};
}
/**
* Find and parse runtime configuration file
*
* @static
* @returns {{title: string, menu: []}}
*/
public static getConfiguration(): RCData {
if (!fs.existsSync(rcPath)) {
return RCParser.DEFAULTS;
}
const file = fs.readFileSync(rcPath, 'utf-8');
const rConfig = RCParser.DEFAULTS;
let userConfig;
try {
userConfig = JSON.parse(file);
} catch (e) {
console.log('CodeX Docs rc file should be in JSON format.');
return RCParser.DEFAULTS;
}
for (const option in userConfig) {
if (Object.prototype.hasOwnProperty.call(userConfig, option)) {
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
}
}
if (!(rConfig.menu instanceof Array)) {
console.log('Menu section in the rc file must be an array.');
rConfig.menu = RCParser.DEFAULTS.menu;
}
rConfig.menu = rConfig.menu.filter((option: string | Menu, i:number) => {
i = i + 1;
if (typeof option === 'string') {
return true;
}
if (!option || option instanceof Array || typeof option !== 'object') {
console.log(`Menu option #${i} in rc file must be a string or an object`);
return false;
}
const { title, uri } = option;
if (!title || typeof title !== 'string') {
console.log(`Menu option #${i} title must be a string.`);
return false;
}
if (!uri || typeof uri !== 'string') {
console.log(`Menu option #${i} uri must be a string.`);
return false;
}
return true;
});
rConfig.menu = rConfig.menu.map((option: string | Menu) => {
if (typeof option === 'string') {
return {
title: option,
/* Replace all non alpha- and numeric-symbols with '-' */
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-'),
};
}
return option;
});
return rConfig;
}
}

View file

@ -3,15 +3,15 @@
*/
import app from '../backend/app.js';
import http from 'http';
import config from 'config';
import Debug from 'debug';
import appConfig from "../backend/utils/appConfig.js";
const debug = Debug.debug('codex.editor.docs:server');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(config.get('port') || '3000');
const port = normalizePort(appConfig.port.toString() || '3000');
app.set('port', port);

View file

@ -1,272 +0,0 @@
import { expect } from 'chai';
import fs from 'fs';
import path from 'path';
import config from 'config';
import sinon = require('sinon');
import rcParser from '../backend/utils/rcparser.js';
const rcPath = path.resolve(process.cwd(), config.get('rcFile'));
describe('RC file parser test', () => {
afterEach(() => {
if (fs.existsSync(rcPath)) {
fs.unlinkSync(rcPath);
}
});
it('Default config', async () => {
const parsedConfig = rcParser.getConfiguration();
expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS);
});
it('Invalid JSON formatted config', () => {
const invalidJson = '{title: "Codex Docs"}';
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, invalidJson, 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('CodeX Docs rc file should be in JSON format.')).to.be.true;
expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS);
spy.restore();
});
it('Normal config', () => {
const normalConfig = {
title: 'Documentation',
menu: [
{ title: 'Option 1', uri: '/option1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(parsedConfig).to.be.deep.equal(normalConfig);
});
it('Missed title', () => {
const normalConfig = {
menu: [
{ title: 'Option 1', uri: '/option1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(parsedConfig.menu).to.be.deep.equal(normalConfig.menu);
expect(parsedConfig.title).to.be.equal(rcParser.DEFAULTS.title);
});
it('Missed menu', () => {
const normalConfig = {
title: 'Documentation',
};
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu);
});
it('Menu is not an array', () => {
const normalConfig = {
title: 'Documentation',
menu: {
0: { title: 'Option 1', uri: '/option1' },
1: { title: 'Option 2', uri: '/option2' },
2: { title: 'Option 3', uri: '/option3' },
},
};
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const spy = sinon.spy(console, 'log');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu section in the rc file must be an array.')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu);
spy.restore();
});
it('Menu option is a string', () => {
const normalConfig = {
title: 'Documentation',
menu: [
'Option 1',
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 1', uri: '/option-1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
});
it('Menu option is not a string or an object', () => {
const normalConfig = {
title: 'Documentation',
menu: [
[ { title: 'Option 1', uri: '/option1' } ],
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu option #1 in rc file must be a string or an object')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
});
it('Menu option title is undefined', () => {
const normalConfig = {
title: 'Documentation',
menu: [
{ uri: '/option1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
});
it('Menu option title is not a string', () => {
const normalConfig = {
title: 'Documentation',
menu: [
{ title: [], uri: '/option1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
});
it('Menu option uri is undefined', () => {
const normalConfig = {
title: 'Documentation',
menu: [
{ title: 'Option 1' },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
});
it('Menu option title is not a string', () => {
const normalConfig = {
title: 'Documentation',
menu: [
{ title: 'Option 1', uri: [] },
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
],
};
const expectedMenu = [
{ title: 'Option 2', uri: '/option2' },
{ title: 'Option 3', uri: '/option3' },
];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration();
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true;
expect(parsedConfig.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
});
});

View file

@ -1658,6 +1658,11 @@ arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -2182,6 +2187,14 @@ concurrently@^7.1.0:
tree-kill "^1.2.2"
yargs "^17.3.1"
"config-loader@https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26":
version "0.0.1"
resolved "https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26"
dependencies:
js-yaml "^4.1.0"
lodash.isarray "^4.0.0"
lodash.merge "^4.6.2"
config@^3.3.6:
version "3.3.7"
resolved "https://registry.yarnpkg.com/config/-/config-3.3.7.tgz#4310410dc2bf4e0effdca21a12a4035860a24ee4"
@ -3761,9 +3774,10 @@ js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
js-yaml@4.1.0:
js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
@ -3978,6 +3992,11 @@ lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
lodash.isarray@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403"
integrity sha512-V8ViWvoNlXpCrB6Ewaj3ScRXUpmCvqp4tJUxa3dlovuJj/8lp3SND5Kw4v5OeuHgoyw4qJN+gl36qZqp6WYQ6g==
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
@ -6333,3 +6352,8 @@ yn@^2.0.0:
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
zod@^3.19.1:
version "3.19.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==