diff --git a/packages/ui-next/index.ts b/packages/ui-next/index.ts index 2f2dc72a0..40bfc7279 100644 --- a/packages/ui-next/index.ts +++ b/packages/ui-next/index.ts @@ -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'); @@ -112,6 +112,50 @@ function addFile(name: string, content: string) { hashes[name] = sha1(content).substring(0, 8); } +async function buildI18n() { + const localeList: Record = {}; + 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 = { ...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; + } + } 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; @@ -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) => ``); + 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, @@ -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); }, }); @@ -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: [], @@ -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); } } diff --git a/packages/ui-next/package.json b/packages/ui-next/package.json index dbfb8ffc6..9f3c5261c 100644 --- a/packages/ui-next/package.json +++ b/packages/ui-next/package.json @@ -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.8", "react-dom": "^19.2.8" },