diff --git a/framework/eslint-config/package.json b/framework/eslint-config/package.json index 2a0163589..f5b0ac147 100644 --- a/framework/eslint-config/package.json +++ b/framework/eslint-config/package.json @@ -4,12 +4,12 @@ "license": "MIT", "main": "base.mjs", "dependencies": { - "@eslint-react/eslint-plugin": "^1.37.3", + "@eslint-react/eslint-plugin": "^1.38.2", "@eslint/compat": "^1.2.7", "@eslint/eslintrc": "^3.3.1", "@stylistic/eslint-plugin": "^4.2.0", - "@typescript-eslint/eslint-plugin": "^8.27.0", - "@typescript-eslint/parser": "^8.27.0", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-de-morgan": "^1.2.1", "eslint-plugin-github": "^6.0.0", diff --git a/package.json b/package.json index 2cea346e6..bb2ca18b0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/chai": "^5.2.1", "@types/cross-spawn": "^6.0.6", "@types/node": "22.13.8", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.0", "autocannon": "^8.0.0", "cac": "^6.7.14", "chai": "^5.2.0", @@ -44,7 +44,7 @@ "cross-spawn": "^7.0.6", "esbuild": "0.25.0", "eslint": "^9.23.0", - "eslint-import-resolver-typescript": "^4.2.2", + "eslint-import-resolver-typescript": "^4.2.5", "eslint-import-resolver-webpack": "^0.13.10", "fs-extra": "^11.3.0", "globals": "^16.0.0", @@ -53,7 +53,7 @@ "mongodb-memory-server": "10.1.4", "nyc": "^17.1.0", "ora": "^8.2.0", - "oxlint": "^0.16.2", + "oxlint": "^0.16.3", "package-json": "^10.0.1", "semver": "^7.7.1", "simple-git": "^3.27.0", diff --git a/packages/components/frontend/ConfigEditor.tsx b/packages/components/frontend/ConfigEditor.tsx index 5070c1e9c..d0fa52e1d 100644 --- a/packages/components/frontend/ConfigEditor.tsx +++ b/packages/components/frontend/ConfigEditor.tsx @@ -42,9 +42,16 @@ function MonacoContainer({ editor.layout(); }, [size]); React.useEffect(() => { - if (!editor) return; + if (!editor || !model) return; + const decoration = editor.createDecorationsCollection(); + const updateDecoration = () => { + const found = model.findMatches("'[hidden]'", true, false, true, '', true).map((i) => i.range); + decoration.set(found.map((range) => ({ range, options: { inlineClassName: 'decoration-hide' } }))); + }; + updateDecoration(); const disposable = editor.onDidChangeModelContent(() => { const val = editor.getValue({ lineEnding: '\n', preserveBOM: false }); + updateDecoration(); try { const loaded = yaml.load(val); schema(loaded); @@ -55,7 +62,7 @@ function MonacoContainer({ } }); return () => disposable.dispose(); // eslint-disable-line - }, [editor, setValue, setError]); + }, [editor, model, setValue, setError]); React.useEffect(() => { if (!editorRef.current || loading) return; setLoading(true); diff --git a/packages/components/frontend/Dom.tsx b/packages/components/frontend/Dom.tsx new file mode 100644 index 000000000..ed56c9c22 --- /dev/null +++ b/packages/components/frontend/Dom.tsx @@ -0,0 +1,14 @@ +import { omit } from 'lodash'; +import React from 'react'; + +export default function DomComponent(props: React.HTMLAttributes & { childDom: HTMLElement }) { + const [dom, setDom] = React.useState(null); + React.useEffect(() => { + if (!dom) return; + dom.appendChild(props.childDom); + return () => { // eslint-disable-line consistent-return + dom.removeChild(props.childDom); + }; + }, [dom]); + return
; +} diff --git a/packages/components/frontend/provider.tsx b/packages/components/frontend/provider.tsx index 13324c3f2..5f02d1dc0 100644 --- a/packages/components/frontend/provider.tsx +++ b/packages/components/frontend/provider.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ToastContainer } from 'react-toastify'; +import { Slide, ToastContainer } from 'react-toastify'; interface C { i18n: (key: string, ...args: any[]) => string; @@ -19,14 +19,15 @@ export default function ComponentsProvider(props: { children: React.ReactNode } {props.children} ; diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 2986b2e67..959abb0c5 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -20,7 +20,7 @@ "preferUnplugged": true, "license": "AGPL-3.0-or-later", "devDependencies": { - "@types/adm-zip": "^0.4.34", + "@types/adm-zip": "^0.5.7", "@types/shell-quote": "^1.7.5", "@types/ws": "^8.18.0" } diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 22c997f63..b64ca8f58 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "@babel/code-frame": "^7.26.2", - "@types/adm-zip": "^0.4.34", + "@types/adm-zip": "^0.5.7", "@types/babel__code-frame": "^7.0.6", "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", @@ -70,7 +70,7 @@ "@types/nodemailer": "^6.4.17", "@types/notp": "^2.0.5", "@types/saslprep": "^1.0.3", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.0", "@types/superagent": "^8.1.9", "chokidar": "^4.0.3", "moment": "^2.30.1" diff --git a/packages/hydrooj/src/entry/cli.ts b/packages/hydrooj/src/entry/cli.ts index 1d3915e37..de2ccdbf6 100644 --- a/packages/hydrooj/src/entry/cli.ts +++ b/packages/hydrooj/src/entry/cli.ts @@ -117,15 +117,17 @@ export async function load(ctx: Context) { resolve(c); }); }); - await ctx.loader.reloadPlugin(require.resolve('../service/storage'), 'file'); + await Promise.all([ + ctx.loader.reloadPlugin(require.resolve('../service/storage'), 'file'), + ctx.loader.reloadPlugin(require.resolve('../service/worker'), 'worker'), + ctx.loader.reloadPlugin(require.resolve('../service/server'), 'server'), + ]); require('../lib/index'); await Promise.all([ service(pending, fail, ctx), ]); - ctx.plugin(require('../service/worker')); await builtinModel(ctx); await model(pending, fail, ctx); - ctx.plugin(require('../service/server')); ctx = await new Promise((resolve) => { ctx.inject(['server', 'setting', 'worker'], (c) => { resolve(c); diff --git a/packages/hydrooj/src/loader.ts b/packages/hydrooj/src/loader.ts index 4c48bc916..fef271f57 100644 --- a/packages/hydrooj/src/loader.ts +++ b/packages/hydrooj/src/loader.ts @@ -110,9 +110,12 @@ export class Loader extends Service { if (!plugin) return; const config = await this.resolveConfig(plugin, configScope); let fork = this.state[key]; + const displayPath = key.includes('node_modules') + ? key.split('node_modules').pop() + : path.relative(process.cwd(), key); logger.info( `%s plugin %c${configScope ? ' with scope %c' : ''}`, - fork ? 'reload' : 'apply', key.split('node_modules').pop(), configScope, + fork ? 'reload' : 'apply', displayPath, configScope, ); if (fork) { fork.update(config); diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 27345b1cd..b4c089bc0 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -5,7 +5,7 @@ import path from 'path'; import AdmZip from 'adm-zip'; import fs from 'fs-extra'; import yaml from 'js-yaml'; -import { pick } from 'lodash'; +import { keyBy, pick } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; import type { Readable } from 'stream'; import { ProblemConfigFile, ProblemType } from '@hydrooj/common'; @@ -409,12 +409,7 @@ export class ProblemModel { const psdocs = await ProblemModel.getMultiStatus( domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, ).toArray(); - const r: Record = {}; - for (const psdoc of psdocs) { - r[psdoc.docId] = psdoc; - r[`${psdoc.domainId}#${psdoc.docId}`] = psdoc; - } - return r; + return keyBy(psdocs, 'docId'); } static async updateStatus( @@ -468,7 +463,7 @@ export class ProblemModel { throw new ValidationError('zip', null, e.message); } await new Promise((resolve, reject) => { - zip.extractAllToAsync(tmpdir, true, (err) => { + zip.extractAllToAsync(tmpdir, true, false, (err) => { if (err) reject(err); resolve(null); }); diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index c12451625..f6b213fcb 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -252,7 +252,7 @@ export default class RecordModel { RecordModel.collStat.deleteMany({ _id: { $in: rids } }), task.deleteMany({ rid: { $in: rids } }), ]); - await RecordModel.collHistory.insertMany(rdocs.map((rdoc) => ({ + await RecordModel.collHistory.insertMany(rdocs.filter((rdoc) => rdoc.judgeAt).map((rdoc) => ({ ...pick(rdoc, [ 'compilerTexts', 'judgeTexts', 'testCases', 'subtasks', 'score', 'time', 'memory', 'status', 'judgeAt', 'judger', diff --git a/packages/hydrooj/src/model/storage.ts b/packages/hydrooj/src/model/storage.ts index a583fe8db..5b817b3b1 100644 --- a/packages/hydrooj/src/model/storage.ts +++ b/packages/hydrooj/src/model/storage.ts @@ -178,7 +178,7 @@ async function cleanFiles() { } } -export function apply(ctx: Context) { +export async function apply(ctx: Context) { ctx.inject(['worker'], (c) => { c.worker.addHandler('storage.prune', cleanFiles); }); @@ -190,24 +190,21 @@ export function apply(ctx: Context) { ]); await StorageModel.del(problemFiles.concat(contestFiles).concat(trainingFiles).map((i) => i.path)); }); - if (process.env.NODE_APP_INSTANCE !== '0') return; - ctx.on('ready', async () => { - await db.ensureIndexes( - StorageModel.coll, - { key: { path: 1 }, name: 'path' }, - { key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' }, - { key: { link: 1 }, sparse: true, name: 'link' }, - ); - if (!await ScheduleModel.count({ type: 'schedule', subType: 'storage.prune' })) { - await ScheduleModel.add({ - type: 'schedule', - subType: 'storage.prune', - executeAfter: moment().startOf('hour').toDate(), - interval: [1, 'hour'], - }); - } - }); + await db.ensureIndexes( + StorageModel.coll, + { key: { path: 1 }, name: 'path' }, + { key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' }, + { key: { link: 1 }, sparse: true, name: 'link' }, + ); + if (!await ScheduleModel.count({ type: 'schedule', subType: 'storage.prune' })) { + await ScheduleModel.add({ + type: 'schedule', + subType: 'storage.prune', + executeAfter: moment().startOf('hour').toDate(), + interval: [1, 'hour'], + }); + } } global.Hydro.model.storage = StorageModel; diff --git a/packages/hydrooj/src/service/db.ts b/packages/hydrooj/src/service/db.ts index 244e57d2b..2f49cc3ad 100644 --- a/packages/hydrooj/src/service/db.ts +++ b/packages/hydrooj/src/service/db.ts @@ -62,7 +62,7 @@ export class MongoService extends Service { yield () => { this.client.close(); }; this.db = this.client.db(url.database || 'hydro'); await bus.parallel('database/connect', this.db); - this.ctx.setInterval(() => this.fixExpireAfter(), Time.hour); + yield this.ctx.setInterval(() => this.fixExpireAfter(), Time.hour); } public collection(c: K) { diff --git a/packages/hydrooj/src/service/monitor.ts b/packages/hydrooj/src/service/monitor.ts index 2876d32e8..e6eab92a3 100644 --- a/packages/hydrooj/src/service/monitor.ts +++ b/packages/hydrooj/src/service/monitor.ts @@ -98,17 +98,14 @@ export async function updateJudge(args) { ); } -export function apply(ctx: Context) { +export async function apply(ctx: Context) { if (process.env.NODE_APP_INSTANCE !== '0') return; - ctx.on('app/started', async () => { - sysinfo.get().then((info) => { - coll.updateOne( - { mid: info.mid, type: 'server' }, - { $set: { ...info, updateAt: new Date(), type: 'server' } }, - { upsert: true }, - ); - feedback(); - setInterval(update, 1800 * 1000); - }); - }); + const info = await sysinfo.get(); + coll.updateOne( + { mid: info.mid, type: 'server' }, + { $set: { ...info, updateAt: new Date(), type: 'server' } }, + { upsert: true }, + ); + feedback(); + return ctx.setInterval(update, 1800 * 1000); // eslint-disable-line } diff --git a/packages/hydrooj/src/service/worker.ts b/packages/hydrooj/src/service/worker.ts index a6c7b078d..8144bac68 100644 --- a/packages/hydrooj/src/service/worker.ts +++ b/packages/hydrooj/src/service/worker.ts @@ -18,7 +18,14 @@ export default class WorkerService extends Service { constructor(ctx: Context) { super(ctx, 'worker'); + } + + async *[Context.init]() { this.consume(); + yield () => { + this.consuming = false; + return this.promise; + }; } async getFirst() { @@ -79,9 +86,4 @@ export default class WorkerService extends Service { }; }); } - - public async stop() { - this.consuming = false; - await this.promise; - } } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index e0a30e34b..5a378c2ef 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -22,8 +22,8 @@ "@fontsource/source-code-pro": "^5.2.5", "@fontsource/ubuntu-mono": "^5.2.5", "@hydrooj/utils": "workspace:^", - "@sentry/browser": "^9.9.0", - "@sentry/webpack-plugin": "^3.2.2", + "@sentry/browser": "^9.10.0", + "@sentry/webpack-plugin": "^3.2.3", "@simplewebauthn/browser": "13.1.0", "@svgr/webpack": "^8.1.0", "@types/html-to-text": "^9.0.4", @@ -67,7 +67,7 @@ "jquery.easing": "^1.4.1", "jquery.transit": "^0.9.12", "matchmedia-polyfill": "^0.3.2", - "md-editor-rt": "^5.4.4", + "md-editor-rt": "^5.4.5", "mini-css-extract-plugin": "^2.9.2", "moment": "^2.30.1", "monaco-editor": "0.52.2", @@ -118,7 +118,7 @@ "web-streams-polyfill": "^4.1.0", "webpack": "^5.98.0", "webpack-bundle-analyzer": "^4.10.2", - "webpack-dev-server": "^5.2.0", + "webpack-dev-server": "^5.2.1", "webpack-manifest-plugin": "^5.0.1", "webpackbar": "^7.0.0" }, diff --git a/packages/ui-default/templates/layout/html5.html b/packages/ui-default/templates/layout/html5.html index 01fdc69f3..243e7e8fd 100644 --- a/packages/ui-default/templates/layout/html5.html +++ b/packages/ui-default/templates/layout/html5.html @@ -48,8 +48,8 @@ {% endif %} {% set UiContext = Object.create(UiContext) %} @@ -65,8 +65,8 @@ {% block script %}{% endblock %} {% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %} {% endif %} diff --git a/packages/ui-next/package.json b/packages/ui-next/package.json index 634751f32..e6085b1d1 100644 --- a/packages/ui-next/package.json +++ b/packages/ui-next/package.json @@ -16,6 +16,6 @@ "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react-swc": "^3.8.1", - "vite": "^6.2.2" + "vite": "^6.2.3" } }