mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
feat: enhance objective question (#376)
This commit is contained in:
14 files changed
+267
-156
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "2.20.7",
|
||||
"version": "2.21.0",
|
||||
"main": "package.json",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -88,7 +88,7 @@ export default async function readCases(folder: string, cfg: Record<string, any>
|
||||
const checkFile = ensureFile(folder);
|
||||
const result = await readYamlCases(config, checkFile)
|
||||
.catch((e) => { throw changeErrorType(e, FormatError); });
|
||||
result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||
result.count = Object.keys(result.answers || {}).length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||
if (!result.count) {
|
||||
try {
|
||||
result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg);
|
||||
|
||||
@@ -62,10 +62,7 @@ export interface Config {
|
||||
hack?: string;
|
||||
interactor?: string;
|
||||
|
||||
outputs?: {
|
||||
output: string,
|
||||
score: number,
|
||||
}[];
|
||||
answers?: Record<string, [answer: string | string[], score: number]>;
|
||||
}
|
||||
|
||||
export interface ContextSubTask {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { readFile } from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||
import { Context } from './interface';
|
||||
|
||||
@@ -11,27 +13,98 @@ export async function judge({
|
||||
: ('content' in code)
|
||||
? code.content.toString().replace(/\r/g, '')
|
||||
: '';
|
||||
const outputs = answer.split('\n');
|
||||
let answers: { [x: string]: string | string[] } = {};
|
||||
try {
|
||||
answers = yaml.load(answer) as any;
|
||||
assert(typeof answers === 'object');
|
||||
} catch (e) {
|
||||
return end({
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Unable to parse answer.',
|
||||
time: 0,
|
||||
memory: 0,
|
||||
});
|
||||
}
|
||||
let totalScore = 0;
|
||||
let totalStatus = 0;
|
||||
for (const i in config.outputs) {
|
||||
const c = config.outputs[i];
|
||||
outputs[i] = outputs[i] || '';
|
||||
let status = STATUS.STATUS_WRONG_ANSWER;
|
||||
let score = 0;
|
||||
if (outputs[i].trim() === (c.output || c[0]).trim()) {
|
||||
score = c.score || c[1];
|
||||
status = STATUS.STATUS_ACCEPTED;
|
||||
for (const key in config.answers) {
|
||||
const [subtaskId, caseId] = key.split('-').map(Number);
|
||||
const ansInfo = config.answers[key] as [string | string[], number];
|
||||
const score = ansInfo[1];
|
||||
const baseInfo = {
|
||||
subtaskId,
|
||||
id: caseId,
|
||||
time: 0,
|
||||
memory: 0,
|
||||
};
|
||||
if (typeof ansInfo[0] === 'string') {
|
||||
if (ansInfo[0].trim() === (answers[key] as any).trim()) {
|
||||
totalScore += score;
|
||||
totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED);
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
score,
|
||||
message: 'Correct',
|
||||
},
|
||||
}, +caseId);
|
||||
} else {
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Incorrect',
|
||||
},
|
||||
}, +caseId);
|
||||
}
|
||||
} else {
|
||||
const stdAns = new Set(ansInfo[0]);
|
||||
const ans = new Set(answers[key]);
|
||||
const correct = stdAns.size === ans.size && [...stdAns].every((x) => ans.has(x));
|
||||
const partialCorrect = [...stdAns].some((x) => ans.has(x)) && [...ans].every((x) => stdAns.has(x));
|
||||
if (correct) {
|
||||
totalScore += score;
|
||||
totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED);
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
score,
|
||||
message: 'Correct',
|
||||
},
|
||||
}, +caseId);
|
||||
} else if (partialCorrect) {
|
||||
totalScore += Math.floor(score / 2);
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: Math.floor(score / 2),
|
||||
message: 'Partially Correct',
|
||||
},
|
||||
}, +caseId);
|
||||
} else {
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Incorrect',
|
||||
},
|
||||
}, +caseId);
|
||||
}
|
||||
}
|
||||
totalScore += score;
|
||||
totalStatus = Math.max(status, totalStatus);
|
||||
next({
|
||||
status: totalStatus,
|
||||
progress: (100 * (+i + 1)) / config.outputs.length,
|
||||
case: {
|
||||
status, score, time: 0, memory: 0, message: '',
|
||||
},
|
||||
}, +i + 1);
|
||||
}
|
||||
return end({
|
||||
status: totalStatus, score: totalScore, time: 0, memory: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.13.19",
|
||||
"version": "3.14.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -170,7 +170,7 @@ export interface ProblemConfigFile {
|
||||
user_extra_files?: string[];
|
||||
judge_extra_files?: string[];
|
||||
detail?: boolean;
|
||||
outputs?: [string, number][];
|
||||
answers?: Record<string, [string | string[], number]>;
|
||||
redirect?: string;
|
||||
cases?: TestCaseConfig[];
|
||||
subtasks?: SubtaskConfig[];
|
||||
|
||||
@@ -17,6 +17,7 @@ import * as contest from './model/contest';
|
||||
import * as discussion from './model/discussion';
|
||||
import * as document from './model/document';
|
||||
import domain from './model/domain';
|
||||
import MessageModel from './model/message';
|
||||
import problem from './model/problem';
|
||||
import RecordModel from './model/record';
|
||||
import StorageModel from './model/storage';
|
||||
@@ -715,6 +716,74 @@ const scripts: UpgradeScript[] = [
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async function _62_63() {
|
||||
const _FRESH_INSTALL_IGNORE = 1;
|
||||
const uids = new Set<number>();
|
||||
await iterateAllDomain(async (ddoc) => {
|
||||
const pdocs = await problem.getMulti(ddoc._id, { config: /type: objective/ })
|
||||
.project({
|
||||
config: 1, content: 1, docId: 1, owner: 1,
|
||||
}).toArray();
|
||||
for (const pdoc of pdocs) {
|
||||
try {
|
||||
const config = yaml.load(pdoc.config as string) as any;
|
||||
if (config.type !== 'objective' || !config.outputs) continue;
|
||||
config.answers = {};
|
||||
let cnt = 0;
|
||||
for (const l of config.outputs) {
|
||||
cnt++;
|
||||
config.answers[cnt] = l;
|
||||
}
|
||||
|
||||
function processSingleLanguage(content: string) { // eslint-disable-line no-inner-declarations
|
||||
let text = '';
|
||||
try {
|
||||
let scnt = 0;
|
||||
const doc = yaml.load(content);
|
||||
if (!(doc instanceof Array)) return content;
|
||||
for (const s of doc) {
|
||||
scnt++;
|
||||
text += `${scnt}. ${s.desc}\n`;
|
||||
if (s.choices) {
|
||||
text += `{{ select(${scnt}) }}\n`;
|
||||
const isPrefixed = s.choices.every((c) => /^[A-Z]\./.test(c));
|
||||
let selectionId = 64;
|
||||
for (const c of s.choices) {
|
||||
selectionId++;
|
||||
text += `- ${isPrefixed ? c.replace(/^[A-Z]\./, '') : c}\n`;
|
||||
if (config.answers[scnt][0] === c) config.answers[scnt][0] = String.fromCharCode(selectionId);
|
||||
}
|
||||
} else text += `{{ input(${scnt}) }}\n`;
|
||||
text += '\n';
|
||||
}
|
||||
} catch (e) { console.error(e); return content; }
|
||||
return text;
|
||||
}
|
||||
|
||||
try {
|
||||
const langs = JSON.parse(pdoc.content);
|
||||
if (typeof langs === 'object' && !(langs instanceof Array)) {
|
||||
for (const lang in langs) {
|
||||
if (typeof langs[lang] !== 'string') continue;
|
||||
langs[lang] = processSingleLanguage(langs[lang]);
|
||||
}
|
||||
await problem.edit(ddoc._id, pdoc.docId, { content: JSON.stringify(langs) });
|
||||
}
|
||||
} catch (e) {
|
||||
const content = processSingleLanguage(pdoc.content);
|
||||
await problem.edit(ddoc._id, pdoc.docId, { content });
|
||||
}
|
||||
uids.add(pdoc.owner);
|
||||
delete config.outputs;
|
||||
await problem.addTestdata(ddoc._id, pdoc.docId, 'config.yaml', Buffer.from(yaml.dump(config)));
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
});
|
||||
for (const uid of uids) {
|
||||
await MessageModel.send(1, uid, '我们更新了客观题的配置格式,已有题目已自动转换,查看文档获得更多信息。', MessageModel.FLAG_UNREAD);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
];
|
||||
|
||||
export default scripts;
|
||||
@@ -70,9 +70,16 @@ const problemConfigSchema = {
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputs: {
|
||||
type: 'array',
|
||||
minLength: 1,
|
||||
answers: {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'^\\d+(-\\d+)?$': {
|
||||
type: 'array',
|
||||
minLength: 2,
|
||||
maxLength: 2,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import { sortBy } from 'lodash';
|
||||
import request from 'vj/utils/request';
|
||||
import Notification from 'vj/components/notification';
|
||||
|
||||
export default class ObjectiveContainer extends React.PureComponent {
|
||||
constructor(args) {
|
||||
super(args);
|
||||
this.state = {};
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.setState({ [ev.target.name]: ev.target.value });
|
||||
}
|
||||
|
||||
onSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
const entries = sortBy(Object.entries(this.state).map((i) => [+i[0], i[1]]));
|
||||
const total = entries[entries.length - 1][0];
|
||||
const ans = new Array(total).fill('');
|
||||
entries.forEach((i) => ans[i[0]] = i[1]); // eslint-disable-line
|
||||
request
|
||||
.post(this.props.target, {
|
||||
lang: '_',
|
||||
code: ans.join('\n'),
|
||||
})
|
||||
.then((res) => {
|
||||
window.location.href = res.url;
|
||||
})
|
||||
.catch((e) => {
|
||||
Notification.error(e.message);
|
||||
});
|
||||
}
|
||||
|
||||
Textbox(args, name) {
|
||||
return (
|
||||
<label htmlFor={`textbox${name}`}>
|
||||
{name + 1}. {args.desc.split('\n').map((i, index) => {
|
||||
if (index) return <><br />{i}</>;
|
||||
return i;
|
||||
})}
|
||||
<div name={`form_item_${name}`} className="textbox-container">
|
||||
<input type="text" name={name} id={`textbox${name}`} className="textbox" onChange={this.onChange}></input>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
Radio(args, name) {
|
||||
return (
|
||||
<>
|
||||
{name + 1}. {args.desc.split('\n').map((i, index) => {
|
||||
if (index) return <><br />{i}</>;
|
||||
return i;
|
||||
})}
|
||||
{args.choices.map((i) => (
|
||||
<label className="radiobox" htmlFor={`radio${name}${i}`} key={i}>
|
||||
<input type="radio" name={name} id={`radio${name}${i}`} value={i} onChange={this.onChange} /> {i} <br />
|
||||
</label>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{this.props.panel.map((i, name) => (
|
||||
<div className="row" key={i}>
|
||||
<div className="medium-7 columns form__item end">
|
||||
{i.choices ? this.Radio(i, name) : this.Textbox(i, name)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{document.getElementsByClassName('nav__item--round').length
|
||||
? <input type="submit" disabled className="button rounded primary" value="登录后提交" />
|
||||
: <input type="submit" className="button rounded primary" value="提交" />}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
||||
}
|
||||
});
|
||||
if (formatConfig.type === 'objective') {
|
||||
Object.keys(formatConfig).filter((i) => !['type', 'outputs'].includes(i)).forEach((i) => delete formatConfig[i]);
|
||||
formatConfig.outputs = formatConfig.outputs || [];
|
||||
Object.keys(formatConfig).filter((i) => !['type', 'answers'].includes(i)).forEach((i) => delete formatConfig[i]);
|
||||
formatConfig.answers = config.answers || {};
|
||||
}
|
||||
Object.keys(formatConfig).filter((i) => i.startsWith('__')).forEach((i) => delete formatConfig[i]);
|
||||
return formatConfig;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.37.25",
|
||||
"version": "4.38.0",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import yaml from 'js-yaml';
|
||||
import ReactDOM, { createRoot } from 'react-dom/client';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import React from 'react';
|
||||
import { getScoreColor } from '@hydrooj/utils/lib/status';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
@@ -7,6 +7,9 @@ import { downloadProblemSet } from 'vj/components/zipDownloader';
|
||||
import loadReactRedux from 'vj/utils/loadReactRedux';
|
||||
import delay from 'vj/utils/delay';
|
||||
import pjax from 'vj/utils/pjax';
|
||||
import request from 'vj/utils/request';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
class ProblemPageExtender {
|
||||
constructor() {
|
||||
@@ -173,32 +176,75 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
}
|
||||
|
||||
async function loadObjective() {
|
||||
try {
|
||||
let s = '';
|
||||
try {
|
||||
s = JSON.parse(UiContext.pdoc.content);
|
||||
} catch {
|
||||
s = UiContext.pdoc.content;
|
||||
$('.outer-loader-container').show();
|
||||
const ans = {};
|
||||
let cnt = 0;
|
||||
const reg = /{{ (input|select|multiselect)\(\d+(-\d+)?\) }}/g;
|
||||
$('.problem-content .typo').children().each((i, e) => {
|
||||
if (e.tagName === 'PRE' && !e.children[0].className.includes('#input')) return;
|
||||
const questions = [];
|
||||
let q;
|
||||
while (q = reg.exec(e.innerText)) questions.push(q); // eslint-disable-line no-cond-assign
|
||||
for (const [info, type] of questions) {
|
||||
cnt++;
|
||||
const id = info.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2');
|
||||
if (type === 'input') {
|
||||
$(e).html($(e).html().replace(info, tpl`
|
||||
<div class="objective_${id} medium-3" style="display: inline-block;">
|
||||
<input type="text" name="${id}" class="textbox objective-input">
|
||||
</div>
|
||||
`));
|
||||
} else {
|
||||
if ($(e).next()[0].tagName !== 'UL') {
|
||||
cnt--;
|
||||
return;
|
||||
}
|
||||
$(e).html($(e).html().replace(info, ''));
|
||||
$(e).next('ul').children().each((j, ele) => {
|
||||
$(ele).after(tpl`
|
||||
<div class="objective_${id} radiobox">
|
||||
<input type="${type === 'select' ? 'radio' : 'checkbox'}" name="${id}" class="objective-input" value="${String.fromCharCode(65 + j)}">
|
||||
${String.fromCharCode(65 + j)}. ${ele.innerHTML}
|
||||
</div>
|
||||
`);
|
||||
$(ele).remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (typeof s === 'object') {
|
||||
const langs = Object.keys(s);
|
||||
const f = langs.filter((i) => i.startsWith(UserContext.viewLang));
|
||||
if (s[UserContext.viewLang]) s = s[UserContext.viewLang];
|
||||
else if (f.length) s = s[f[0]];
|
||||
else s = s[langs[0]];
|
||||
}
|
||||
const props = yaml.load(s);
|
||||
if (!(props instanceof Array)) return;
|
||||
$('.outer-loader-container').show();
|
||||
const { default: Objective } = await import('vj/components/objective-question/index');
|
||||
|
||||
ReactDOM.createRoot($('.problem-content').get(0)).render(
|
||||
<div className="section__body typo">
|
||||
<Objective panel={props} target={UiContext.postSubmitUrl}></Objective>
|
||||
</div>,
|
||||
);
|
||||
$('.outer-loader-container').hide();
|
||||
} catch (e) { }
|
||||
});
|
||||
if (cnt) {
|
||||
$('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length
|
||||
? `<input type="submit" disabled class="button rounded primary" value="${i18n('Submit')}" />`
|
||||
: `<input type="submit" class="button rounded primary" value="${i18n('Login to Submit')}" />`);
|
||||
$('input.objective-input[type!=checkbox]').on('input', (e) => {
|
||||
ans[e.target.name] = e.target.value;
|
||||
});
|
||||
$('input.objective-input[type=checkbox]').on('input', (e) => {
|
||||
if (e.target.checked) {
|
||||
if (ans[e.target.name] === undefined) ans[e.target.name] = [];
|
||||
ans[e.target.name].push(e.target.value);
|
||||
} else {
|
||||
ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value);
|
||||
}
|
||||
});
|
||||
$('input[type="submit"]').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
request
|
||||
.post(UiContext.postSubmitUrl, {
|
||||
lang: '_',
|
||||
code: yaml.dump(ans),
|
||||
})
|
||||
.then((res) => {
|
||||
window.location.href = res.url;
|
||||
})
|
||||
.catch((err) => {
|
||||
Notification.error(err.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
$('.scratchpad--hide').hide();
|
||||
$('.non-scratchpad--hide').hide();
|
||||
$('.outer-loader-container').hide();
|
||||
}
|
||||
|
||||
async function initChart() {
|
||||
|
||||
+16
-14
@@ -7,21 +7,24 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
judge_extra_files: [],
|
||||
user_extra_files: [],
|
||||
};
|
||||
if (cfg.checker) {
|
||||
if (!cfg.checker.includes('.')) {
|
||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
||||
if (cfg.type === 'objective') {
|
||||
if (cfg.checker || cfg.interactor) throw new Error('You cannot use checker or interactor for objective questions');
|
||||
} else {
|
||||
if (cfg.checker) {
|
||||
if (!cfg.checker.includes('.')) {
|
||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
||||
}
|
||||
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||
}
|
||||
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
||||
['judge', 'user'].forEach((n) => {
|
||||
const conf = cfg[`${n}_extra_files`];
|
||||
if (!conf) return;
|
||||
if (conf instanceof Array) {
|
||||
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
|
||||
} else throw new Error(`Invalid ${n}_extra_files config.`);
|
||||
});
|
||||
}
|
||||
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
||||
['judge', 'user'].forEach((n) => {
|
||||
const conf = cfg[`${n}_extra_files`];
|
||||
if (!conf) return;
|
||||
if (conf instanceof Array) {
|
||||
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
|
||||
} else throw new Error(`Invalid ${n}_extra_files config.`);
|
||||
});
|
||||
if (cfg.outputs) config.type = cfg.type || 'objective';
|
||||
if (cfg.cases?.length) {
|
||||
config.subtasks = [{
|
||||
cases: cfg.cases,
|
||||
@@ -30,7 +33,6 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
}
|
||||
if (cfg.time) config.time = parseTimeMS(cfg.time);
|
||||
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
||||
if (config.type === 'objective' && !cfg.outputs?.length) throw new Error('outputs config not found');
|
||||
return Object.assign(cfg, config);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/utils",
|
||||
"version": "1.2.7",
|
||||
"version": "1.3.0",
|
||||
"description": "hydrooj utils",
|
||||
"main": "package.json",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
Reference in new issue
Block a user