Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 94 additions & 9 deletions packages/ui-next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import path from 'path';
import esbuild from 'esbuild';
import c2k from 'koa2-connect/ts';
import { createServer, type Plugin } from 'vite';
import { serializer } from '@hydrooj/framework';
import { HandlerCommon, serializer } from '@hydrooj/framework';
import {
Context, Handler, Logger,
NotFoundError, param, sha1, size, Types,
NotFoundError, param, SettingModel, sha1, size, Types,
} from 'hydrooj';

const logger = new Logger('ui-next');
Expand Down Expand Up @@ -112,6 +112,50 @@ function addFile(name: string, content: string) {
hashes[name] = sha1(content).substring(0, 8);
}

async function buildI18n() {
const localeList: Record<string, { name: string, flag: string }> = {};
for (const lang in global.Hydro.locales) {
if (!/^[a-zA-Z_]+$/.test(lang)) continue;
if (!global.Hydro.locales[lang].__interface) continue;
addFile(`lang-${lang}.js`, `window.HydroLocale=${JSON.stringify(global.Hydro.locales[lang][Symbol.for('iterate')])};`);
const id = global.Hydro.locales[lang].__id;
if (id) localeList[id] = { name: global.Hydro.locales[lang].__langname, flag: global.Hydro.locales[lang].__flag };
}
addFile('locale-list.js', `window.HydroLocaleList=${JSON.stringify(localeList)};`);
}

async function buildCodeLangs() {
addFile('code-langs.js', `window.HydroCodeLangs=${JSON.stringify(SettingModel.langs)};`);
}

async function buildVersions() {
const versions: Record<string, string> = { ...global.Hydro.version };
try {
const { simpleGit } = require('simple-git') as typeof import('simple-git');
const fetchAddonVersion = async (name: string, addonPath: string) => {
try {
const git = simpleGit(addonPath);
const [log, status] = await Promise.all([git.log(), git.status()]);
if (log.all.length > 0) {
let hash = log.all[0].hash.substring(0, 7);
if (!status.isClean()) hash += '-dirty';
versions[name] = versions[name] ? `${versions[name]}-${hash}` : hash;
Comment on lines +135 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby context
git ls-files packages/ui-next/index.ts packages/ui-next/package.json package.json
wc -l packages/ui-next/index.ts
sed -n '110,170p' packages/ui-next/index.ts

# Find simple-git usage/version in the repo
rg -n "simple-git|git\.log\(" packages/ui-next package.json packages -g '!**/dist/**' -g '!**/build/**'

Repository: hydro-dev/Hydro

Length of output: 3811


🌐 Web query:

simple-git log maxCount option documentation

💡 Result:

In the simple-git library, the maxCount option within the git log method is used to limit the number of commits returned [1][2]. Specifying options.maxCount is functionally equivalent to passing the --max-count option directly to the underlying git log command [3][4]. You can use it by passing an options object to the log method, for example: git.log({ maxCount: 10 }); This configuration will instruct simple-git to retrieve only the most recent 10 commits [1][2]. The log method also supports various other options, including from and to for defining commit ranges, file for filtering by path, and custom formatting options [1][4].

Citations:


Bound the git log query to one commit. git.log() reads the full history for each addon, but only the latest hash is used here. Use maxCount: 1 to avoid unnecessary startup and rebuild work.

Proposed fix
-                const [log, status] = await Promise.all([git.log(), git.status()]);
+                const [log, status] = await Promise.all([git.log({ maxCount: 1 }), git.status()]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchAddonVersion = async (name: string, addonPath: string) => {
try {
const git = simpleGit(addonPath);
const [log, status] = await Promise.all([git.log(), git.status()]);
if (log.all.length > 0) {
let hash = log.all[0].hash.substring(0, 7);
if (!status.isClean()) hash += '-dirty';
versions[name] = versions[name] ? `${versions[name]}-${hash}` : hash;
const fetchAddonVersion = async (name: string, addonPath: string) => {
try {
const git = simpleGit(addonPath);
const [log, status] = await Promise.all([git.log({ maxCount: 1 }), git.status()]);
if (log.all.length > 0) {
let hash = log.all[0].hash.substring(0, 7);
if (!status.isClean()) hash += '-dirty';
versions[name] = versions[name] ? `${versions[name]}-${hash}` : hash;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui-next/index.ts` around lines 135 - 142, Update fetchAddonVersion
to request only the latest commit by passing maxCount: 1 to git.log(), while
preserving the existing hash and dirty-status version construction.

}
} catch (e) {
logger.debug('Could not get git hash for addon %s: %o', name, e);
}
};
await Promise.all(
Object.entries(global.addons)
.filter(([name]) => name !== 'hydrooj') // already handled in loader.ts
.map(([name, addonPath]) => fetchAddonVersion(name, addonPath as string)),
);
} catch (e) {
logger.debug('simple-git not available: %o', e);
}
addFile('versions.js', `window.HydroVersions=${JSON.stringify(versions)};`);
}

class UiNextConstantHandler extends Handler {
noCheckPermView = true;

Expand Down Expand Up @@ -219,10 +263,32 @@ export async function buildPlugins() {
}
}

const HASH_FALLBACK = '00000000';

const getViewLang = (handler: HandlerCommon) => handler.user?.viewLang || handler.session?.viewLang || 'zh';

const injectedScripts = (resolve: (name: string) => string, viewLang: string) => [
'code-langs.js',
'locale-list.js',
`lang-${viewLang}.js`,
'versions.js',
].map((name) => `<script src="${resolve(name)}"></script>`);

export async function apply(ctx: Context) {
if (process.env.HYDRO_CLI) return;

ctx.Route('ui_next_constants', '/plugins/:version/:name', UiNextConstantHandler);

if (process.env.DEV) {
ctx.on('app/started', async () => {
await buildI18n();
await buildCodeLangs();
await buildVersions();
});
ctx.on('app/i18n/update', buildI18n);
ctx.on('system/setting-loaded', buildCodeLangs);
ctx.on('system/setting', buildCodeLangs);

const vite = await createServer({
root: __dirname,
clearScreen: false,
Expand Down Expand Up @@ -265,7 +331,13 @@ export async function apply(ctx: Context) {
route_map: ctx.server.routeMap,
endpoint: ctx.setting.get('server.url') || undefined,
}, serializer(false, context.handler));
const htmlToRender = html.replace(INJECT_MARKER, buildInject(serialized));
const ts = Date.now();
const devAssetUrl = (name: string) => `/plugins/0/${name}?_=${ts}`;
const injectHtml = [
buildInject(serialized),
...injectedScripts(devAssetUrl, getViewLang(context.handler)),
].join('\n');
const htmlToRender = html.replace(INJECT_MARKER, injectHtml);
return await vite.transformIndexHtml(context.handler.context.req.url!, htmlToRender);
},
});
Expand All @@ -275,7 +347,14 @@ export async function apply(ctx: Context) {
await vite.close().catch((e) => console.error(e));
};
} else {
ctx.Route('ui_next_constants', '/plugins/:version/:name', UiNextConstantHandler);
const build = async () => {
await buildPlugins();
await buildI18n();
await buildCodeLangs();
await buildVersions();
};
ctx.on('app/started', build);

ctx.server.registerRenderer('next', {
name: 'next',
accept: [],
Expand All @@ -298,19 +377,25 @@ export async function apply(ctx: Context) {
url: context.handler.context.req.url!,
route_map: ctx.server.routeMap,
endpoint: ctx.setting.get('server.url') || undefined,
plugins_url: `/plugins/${hashes['plugins.js'] || '00000000'}/plugins.js`,
plugins_url: `/plugins/${hashes['plugins.js'] || HASH_FALLBACK}/plugins.js`,
}, serializer(false, context.handler));
return html.replace(INJECT_MARKER, buildInject(serialized));
const prodAssetUrl = (name: string) => `/plugins/${hashes[name] || HASH_FALLBACK}/${name}`;
const injectHtml = [
buildInject(serialized),
...injectedScripts(prodAssetUrl, getViewLang(context.handler)),
].join('\n');
return html.replace(INJECT_MARKER, injectHtml);
},
});
ctx.on('app/started', buildPlugins);
const debouncedBuild = ctx.debounce(buildPlugins, 2000);
const debouncedBuild = ctx.debounce(build, 2000);
const triggerHotUpdate = (filePath?: string) => {
if (filePath && !filePath.includes('/ui/')) return;
debouncedBuild();
};
ctx.on('app/watch/change', triggerHotUpdate);
ctx.on('app/watch/unlink', triggerHotUpdate);
ctx.on('system/setting', () => debouncedBuild());
ctx.on('system/setting-loaded', buildCodeLangs);
ctx.on('system/setting', debouncedBuild);
ctx.on('app/i18n/update', debouncedBuild);
}
}
1 change: 1 addition & 0 deletions packages/ui-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"esbuild": "0.25.2",
"is-relative-url": "^4.1.0",
"path-to-regexp": "^8.4.2",
"simple-git": "^3.36.0",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
Expand Down
Loading