From 1a7b08e56e6a6b31ae4eb76938940548a2b08542 Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 9 Mar 2025 16:05:02 +0800 Subject: [PATCH] core&ui: remove Hydro.service.bus, Hydro.ui.template --- packages/hydrooj/src/entry/common.ts | 34 --- packages/hydrooj/src/entry/worker.ts | 5 +- packages/hydrooj/src/init.ts | 6 +- packages/hydrooj/src/interface.ts | 7 - packages/hydrooj/src/service/bus.ts | 1 - packages/hydrooj/src/service/server.ts | 6 - packages/ui-default/backendlib/template.ts | 309 +++++++++++++-------- packages/ui-default/index.ts | 3 +- 8 files changed, 193 insertions(+), 178 deletions(-) diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts index 2148deb37..51fa3f89a 100644 --- a/packages/hydrooj/src/entry/common.ts +++ b/packages/hydrooj/src/entry/common.ts @@ -14,17 +14,6 @@ import { unwrapExports } from '../utils'; const logger = new Logger('common'); -async function getFiles(folder: string, base = ''): Promise { - const files = []; - const f = await fs.readdir(folder); - for (const i of f) { - if ((await fs.stat(path.join(folder, i))).isDirectory()) { - files.push(...await getFiles(path.join(folder, i), path.join(base, i))); - } else files.push(path.join(base, i)); - } - return files.map((item) => item.replace(/\\/gmi, '/')); -} - function locateFile(basePath: string, filenames: string[]) { for (const i of filenames) { const p = path.resolve(basePath, i); @@ -136,26 +125,3 @@ export async function setting(pending: string[], fail: string[], modelSetting: t } } } - -export async function template(pending: string[], fail: string[]) { - for (const i of pending) { - const p = locateFile(i, ['template', 'templates']); - if (p && (await fs.stat(p)).isDirectory() && !fail.includes(i)) { - try { - const files = await getFiles(p); - for (const file of files) { - const l = path.resolve(p, file); - if (file.endsWith('.tsx')) global.Hydro.ui.template[file] = require(l); - global.Hydro.ui.template[file] = await fs.readFile(l, 'utf-8'); - if (process.env.DEV) global.Hydro.ui.template[`${file}.source`] = l; - } - logger.info('Template init: %s', i); - } catch (e) { - fail.push(i); - app.injectUI('Notification', 'Template load fail: {0}', { args: [i], type: 'warn' }, PRIV.PRIV_VIEW_SYSTEM_NOTIFICATION); - logger.error('Template Load Fail: %s', i); - logger.error(e); - } - } - } -} diff --git a/packages/hydrooj/src/entry/worker.ts b/packages/hydrooj/src/entry/worker.ts index 6c148671b..91e43261a 100644 --- a/packages/hydrooj/src/entry/worker.ts +++ b/packages/hydrooj/src/entry/worker.ts @@ -30,10 +30,7 @@ export async function apply(ctx: Context) { } const pending = global.addons; const fail = []; - await Promise.all([ - locale(pending, fail), - template(pending, fail), - ]); + await locale(pending, fail); await ctx.plugin(MongoService, load() || {}); await ctx.plugin(ConfigService); const modelSystem = require('../model/system'); diff --git a/packages/hydrooj/src/init.ts b/packages/hydrooj/src/init.ts index a31c020d1..f56f4d230 100644 --- a/packages/hydrooj/src/init.ts +++ b/packages/hydrooj/src/init.ts @@ -9,8 +9,6 @@ if (!global.Hydro) { hydrooj: require('hydrooj/package.json').version, }, // @ts-ignore - service: {}, - // @ts-ignore model: {}, script: {}, // @ts-ignore @@ -22,9 +20,7 @@ if (!global.Hydro) { }, }), // @ts-ignore - ui: { - template: {}, - }, + ui: {}, // @ts-ignore error: {}, locales: {}, diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 8bf9e477c..9496fff48 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -596,11 +596,6 @@ export interface Model { rp: typeof import('./script/rating').RpTypes, } -export interface HydroService { - /** @deprecated */ - bus: Context, -} - export interface GeoIP { provider: string, lookup: (ip: string, locale?: string) => any, @@ -629,7 +624,6 @@ export interface Lib extends Record { export type UIInjectableFields = 'ProblemAdd' | 'Notification' | 'Nav' | 'UserDropdown' | 'DomainManage' | 'ControlPanel'; export interface UI { - template: Record, nodes: Record, getNodes: typeof import('./lib/ui').getNodes, inject: typeof import('./lib/ui').inject, @@ -650,7 +644,6 @@ export interface HydroGlobal { version: Record; model: Model; script: Record; - service: HydroService; lib: Lib; module: { [K in keyof ModuleInterfaces]: Record }; ui: UI; diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index a3ef6081a..d679ac8fc 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -115,5 +115,4 @@ export function apply(ctx: Context) { export default app; -global.Hydro.service.bus = app as any; global.bus = app; diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 7d261f31b..1ae484e90 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -238,12 +238,6 @@ export async function apply(ctx: Context) { h.translate = h.translate.bind(h); h.url = h.url.bind(h); h.ctx = h.ctx.extend({ domain: h.domain }); - h.renderHTML = ((orig) => function (name: string, args: Record) { - const s = name.split('.'); - let templateName = `${s[0]}.${args.domainId}.${s[1]}`; - if (!global.Hydro.ui.template[templateName]) templateName = name; - return orig(templateName, args); - })(h.renderHTML).bind(h); }); on('handler/create/http', async (h) => { if (!argv.options.benchmark && !h.notUsage) await h.limitRate('global', 5, 100); diff --git a/packages/ui-default/backendlib/template.ts b/packages/ui-default/backendlib/template.ts index 18ea48ee7..c78b6db81 100644 --- a/packages/ui-default/backendlib/template.ts +++ b/packages/ui-default/backendlib/template.ts @@ -2,7 +2,7 @@ import * as status from '@hydrooj/common/status'; import { findFileSync } from '@hydrooj/utils/lib/utils'; import { convert } from 'html-to-text'; import { - avatar, Context, fs, PERM, PRIV, STATUS, yaml, + avatar, Context, fs, PERM, PRIV, Service, STATUS, yaml, } from 'hydrooj'; import jsesc from 'jsesc'; import nunjucks from 'nunjucks'; @@ -16,48 +16,47 @@ let { template } = argv.options; if (!template || typeof template !== 'string') template = findFileSync('@hydrooj/ui-default/templates'); else template = findFileSync(template); -class Loader extends nunjucks.Loader { - getSource(name) { - const src = global.Hydro.ui.template[name]; - const ref = global.Hydro.ui.template[`${name}.source`]; - if (!process.env.DEV) { - if (!src) throw new Error(`Cannot get template ${name}`); - return { - src, - path: name, - noCache: false, - }; - } - let fullpath = null; - const p = path.resolve(template, name); - if (fs.existsSync(p)) fullpath = p; - if (!fullpath && ref && fs.existsSync(ref)) fullpath = ref; - if (!fullpath) { - if (src) { - return { - src, - path: name, - noCache: true, - }; - } - throw new Error(`Cannot get template ${name}`); - } - return { - src: fs.readFileSync(fullpath, 'utf-8'), - path: fullpath, - noCache: true, - }; - } -} - const replacer = (k, v) => { if (k.startsWith('_') && k !== '_id') return undefined; if (typeof v === 'bigint') return `BigInt::${v.toString()}`; return v; }; +async function getFiles(folder: string, base = ''): Promise { + const files: string[] = []; + const f = await fs.readdir(folder); + for (const i of f) { + if ((await fs.stat(path.join(folder, i))).isDirectory()) { + files.push(...await getFiles(path.join(folder, i), path.join(base, i))); + } else files.push(path.join(base, i)); + } + return files.map((item) => item.replace(/\\/gmi, '/')); +} + +function locateFile(basePath: string, filenames: string[]) { + for (const i of filenames) { + const p = path.resolve(basePath, i); + if (fs.existsSync(p)) return p; + } + return null; +} + +// @ts-ignore +nunjucks.runtime.memberLookup = function memberLookup(obj, val) { + if ((obj || {})._original) obj = obj._original; + if (obj === undefined || obj === null) return undefined; + if (typeof obj[val] === 'function') { + const fn = function (...args) { + return obj[val].call(obj, ...args); + }; + fn._original = obj[val]; + return fn; + } + return obj[val]; +}; + class Nunjucks extends nunjucks.Environment { - constructor() { + constructor(Loader: any) { super(new Loader(), { autoescape: true, trimBlocks: true }); this.addFilter('await', async (promise, callback) => { try { @@ -117,94 +116,164 @@ class Nunjucks extends nunjucks.Environment { console.log(self); return self; }); + + // eslint-disable-next-line no-eval + this.addGlobal('eval', eval); + this.addGlobal('Date', Date); + this.addGlobal('Object', Object); + this.addGlobal('String', String); + this.addGlobal('Array', Array); + this.addGlobal('Math', Math); + this.addGlobal('process', process); + this.addGlobal('global', global); + this.addGlobal('typeof', (o) => typeof o); + this.addGlobal('instanceof', (a, b) => a instanceof b); + this.addGlobal('paginate', misc.paginate); + this.addGlobal('size', misc.size); + this.addGlobal('utils', { status }); + this.addGlobal('avatarUrl', avatar); + this.addGlobal('formatSeconds', misc.formatSeconds); + this.addGlobal('lib', global.Hydro.lib); + this.addGlobal('model', global.Hydro.model); + this.addGlobal('ui', global.Hydro.ui); + this.addGlobal('isIE', (str) => { + if (!str) return false; + if (['MSIE', 'rv:11.0'].some((i) => str.includes(i))) return true; + if (str.includes('Chrome/') && +str.split('Chrome/')[1].split('.')[0] < 60) return true; + return false; + }); + this.addGlobal('set', (obj, key, val) => { + if (val !== undefined) obj[key] = val; + else Object.assign(obj, key); + return ''; + }); + + const platformIconMap = { 'mac os': 'mac' }; + const supportedPlatformIcons = ['android', 'chromeos', 'ios', 'linux', 'mac', 'mobile', 'windows']; + this.addGlobal('platformIcon', (platform) => { + const key = platformIconMap[platform?.toLowerCase()] || platform?.toLowerCase(); + if (supportedPlatformIcons.includes(key)) return key; + return 'unknown'; + }); } } -// @ts-ignore -nunjucks.runtime.memberLookup = function memberLookup(obj, val) { - if ((obj || {})._original) obj = obj._original; - if (obj === undefined || obj === null) return undefined; - if (typeof obj[val] === 'function') { - const fn = function (...args) { - return obj[val].call(obj, ...args); - }; - fn._original = obj[val]; - return fn; + +export class TemplateService extends Service { + static inject = ['server']; + + registry: Record = {}; + + constructor(public ctx: Context) { + super(ctx, 'template'); + const that = this; + ctx.on('handler/create', async (h) => { + h.user = h.context.HydroContext.user as any; + h.domain = h.context.HydroContext.domain as any; + h.translate = h.translate.bind(h); + h.url = h.url.bind(h); + h.ctx = h.ctx.extend({ domain: h.domain }); + h.renderHTML = ((orig) => function (name: string, args: Record) { + const s = name.split('.'); + let templateName = `${s[0]}.${args.domainId}.${s[1]}`; + if (!that.registry[templateName]) templateName = name; + return orig(templateName, args); + })(h.renderHTML).bind(h); + }); + + class Loader extends nunjucks.Loader { + getSource(name) { + const src = that.registry[name]; + const ref = that.registry[`${name}.source`]; + if (!process.env.DEV) { + if (!src) throw new Error(`Cannot get template ${name}`); + return { + src, + path: name, + noCache: false, + }; + } + let fullpath: string | null = null; + const p = path.resolve(template, name); + if (fs.existsSync(p)) fullpath = p; + if (!fullpath && ref && fs.existsSync(ref)) fullpath = ref; + if (!fullpath) { + if (src) { + return { + src, + path: name, + noCache: true, + }; + } + throw new Error(`Cannot get template ${name}`); + } + return { + src: fs.readFileSync(fullpath, 'utf-8'), + path: fullpath, + noCache: true, + }; + } + } + + const env = new Nunjucks(Loader); + env.addGlobal('findSubModule', (prefix) => Object.keys(that.registry).filter((n) => n.startsWith(prefix))); + env.addGlobal('templateExists', (name) => !!that.registry[name]); + + const render = (name: string, state: any) => new Promise((resolve, reject) => { + const start = Date.now(); + env.render(name, { + page_name: name.split('.')[0], + ...state, + formatJudgeTexts: (texts) => texts.map((text) => { + if (typeof text === 'string') return text; + return state._(text.message).format(...text.params || []) + ((process.env.DEV && text.stack) ? `\n${text.stack}` : ''); + }).join('\n'), + datetimeSpan: (arg0, arg1, arg2) => misc.datetimeSpan(arg0, arg1, arg2, state.handler.user?.timeZone), + ctx: state.handler?.ctx, + perm: PERM, + PRIV, + STATUS, + UiContext: state.handler?.UiContext || {}, + }, (err, res) => { + const end = Date.now(); + if (end - start > 5000) console.error(`Render of ${name} took ${end - start}ms`); + if (err) reject(err); + else resolve(res || ''); + }); + }); + + ctx.server.registerRenderer('ui-default', { + name: 'ui-default', + output: 'html', + accept: [], + asFallback: true, + priority: 1, + render: (name, args, context) => render(name, { ...context, ...args }), + }); } - return obj[val]; -}; -const env = new Nunjucks(); -// eslint-disable-next-line no-eval -env.addGlobal('eval', eval); -env.addGlobal('Date', Date); -env.addGlobal('Object', Object); -env.addGlobal('String', String); -env.addGlobal('Array', Array); -env.addGlobal('Math', Math); -env.addGlobal('process', process); -env.addGlobal('global', global); -env.addGlobal('typeof', (o) => typeof o); -env.addGlobal('instanceof', (a, b) => a instanceof b); -env.addGlobal('paginate', misc.paginate); -env.addGlobal('size', misc.size); -env.addGlobal('utils', { status }); -env.addGlobal('avatarUrl', avatar); -env.addGlobal('formatSeconds', misc.formatSeconds); -env.addGlobal('lib', global.Hydro.lib); -env.addGlobal('model', global.Hydro.model); -env.addGlobal('ui', global.Hydro.ui); -env.addGlobal('isIE', (str) => { - if (!str) return false; - if (['MSIE', 'rv:11.0'].some((i) => str.includes(i))) return true; - if (str.includes('Chrome/') && +str.split('Chrome/')[1].split('.')[0] < 60) return true; - return false; -}); -env.addGlobal('set', (obj, key, val) => { - if (val !== undefined) obj[key] = val; - else Object.assign(obj, key); - return ''; -}); -env.addGlobal('findSubModule', (prefix) => Object.keys(global.Hydro.ui.template).filter((n) => n.startsWith(prefix))); -env.addGlobal('templateExists', (name) => !!global.Hydro.ui.template[name]); -const platformIconMap = { 'mac os': 'mac' }; -const supportedPlatformIcons = ['android', 'chromeos', 'ios', 'linux', 'mac', 'mobile', 'windows']; -env.addGlobal('platformIcon', (platform) => { - const key = platformIconMap[platform?.toLowerCase()] || platform?.toLowerCase(); - if (supportedPlatformIcons.includes(key)) return key; - return 'unknown'; -}); + async [Service.setup]() { + const pending = global.addons; + const logger = this.ctx.logger('template'); + for (const i of pending) { + const p = locateFile(i, ['template', 'templates']); + if (p && (await fs.stat(p)).isDirectory()) { + try { + const files = await getFiles(p); + for (const file of files) { + const l = path.resolve(p, file); + this.registry[file] = await fs.readFile(l, 'utf-8'); + if (process.env.DEV) this.registry[`${file}.source`] = l; + } + logger.info('Template init: %s', i); + } catch (e) { + this.ctx.injectUI('Notification', 'Template load fail: {0}', { args: [i], type: 'warn' }, PRIV.PRIV_VIEW_SYSTEM_NOTIFICATION); + logger.error('Template Load Fail: %s', i); + logger.error(e); + } + } + } + } +} -const render = (name: string, state: any) => new Promise((resolve, reject) => { - const start = Date.now(); - env.render(name, { - page_name: name.split('.')[0], - ...state, - formatJudgeTexts: (texts) => texts.map((text) => { - if (typeof text === 'string') return text; - return state._(text.message).format(...text.params || []) + ((process.env.DEV && text.stack) ? `\n${text.stack}` : ''); - }).join('\n'), - datetimeSpan: (arg0, arg1, arg2) => misc.datetimeSpan(arg0, arg1, arg2, state.handler.user?.timeZone), - ctx: state.handler?.ctx, - perm: PERM, - PRIV, - STATUS, - UiContext: state.handler?.UiContext || {}, - }, (err, res) => { - const end = Date.now(); - if (end - start > 5000) console.error(`Render of ${name} took ${end - start}ms`); - if (err) reject(err); - else resolve(res); - }); -}); - -export const inject = ['server']; export async function apply(ctx: Context) { - ctx.server.registerRenderer('ui-default', { - name: 'ui-default', - output: 'html', - accept: [], - asFallback: true, - priority: 1, - render: (name, args, context) => render(name, { ...context, ...args }), - }); } diff --git a/packages/ui-default/index.ts b/packages/ui-default/index.ts index 62eb5b77b..aea578f8e 100644 --- a/packages/ui-default/index.ts +++ b/packages/ui-default/index.ts @@ -5,6 +5,7 @@ import { } from 'hydrooj'; import convert from 'schemastery-jsonschema'; import markdown from './backendlib/markdown'; +import { TemplateService } from './backendlib/template'; class WikiHelpHandler extends Handler { noCheckPermView = true; @@ -164,6 +165,6 @@ export function apply(ctx: Context) { domains: SystemModel.get('ui-default.domains') || [], }; }); - ctx.plugin(require('./backendlib/template')); + ctx.plugin(TemplateService); ctx.plugin(require('./backendlib/builder')); }