Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b8e46c2
ui-next: inject args into vite html
renbaoshuo Dec 25, 2025
666d9eb
core: improved RendererContext typing
renbaoshuo Dec 25, 2025
6374eff
core: allow inject route renderer
renbaoshuo Dec 25, 2025
4a58aeb
ui-next: update import for esbuild plugin to @chialab/esbuild-plugin-…
renbaoshuo Dec 25, 2025
fc17748
ui-next: add hydroPlugins to dynamically load route plugins
renbaoshuo Dec 25, 2025
54462c8
ui-next: update injection marker to maintain script integrity
renbaoshuo Dec 25, 2025
cb12669
ui-next: drop @vitejs/plugin-react-swc
renbaoshuo Dec 26, 2025
9d1eb9a
ui-next: serialize injected data correctly
renbaoshuo Dec 26, 2025
d19354d
*: update devcontainer config
renbaoshuo Dec 28, 2025
3a00690
ui-next: refactor addon ui patch resolving
renbaoshuo Mar 21, 2026
d65bbfb
ui-next: enable for all pages
renbaoshuo Mar 22, 2026
ecf27da
ui-next: add component registry
renbaoshuo Mar 22, 2026
31a75b7
ui-next: page data context
renbaoshuo Mar 24, 2026
e089ec6
ui-next: refactor code structure
renbaoshuo Mar 24, 2026
b61aeb3
Merge remote-tracking branch 'upstream/master' into ui-next
renbaoshuo Mar 24, 2026
aa357f9
Merge remote-tracking branch 'upstream/master' into ui-next
renbaoshuo Apr 20, 2026
cfd9c49
ui-next: inject route map
renbaoshuo Apr 21, 2026
1a43be0
workspace: include ui-next package in eslint configuration
renbaoshuo Apr 21, 2026
18606de
ui-next: simplify registry code
renbaoshuo Apr 21, 2026
ae30742
ui-next: add missing return statements in hydroPlugins function
renbaoshuo Apr 21, 2026
6cd4975
ui-next: format index.ts
renbaoshuo Apr 21, 2026
f597d38
ui-next: better page data context
renbaoshuo Apr 21, 2026
7d41aa3
ui-next: url builder
renbaoshuo Apr 21, 2026
a8cf9b8
ui-next: restructure registry context
renbaoshuo Apr 21, 2026
2bc1e62
Merge remote-tracking branch 'upstream/master' into ui-next
renbaoshuo Apr 21, 2026
b67697b
framework: fix typing
renbaoshuo Apr 22, 2026
8788895
ui-next: fix typing
renbaoshuo Apr 22, 2026
dc0339c
framework: improve typing
renbaoshuo Apr 22, 2026
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
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"remoteUser": "root",
"forwardPorts": [
2333,
3010,
8000
],
"features": {
Expand All @@ -32,4 +33,4 @@
}
},
"postCreateCommand": "git submodule update --init && yarn install && npx hydrooj cli system set server.port 2333 && npx hydrooj cli user create root@hydro.local root rootroot 2 && npx hydrooj cli user setSuperAdmin 2"
}
}
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,17 @@
},
"files.associations": {
"*.html": "nunjucks"
},
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
}
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const config = react({
'**/{public,frontend}/**/*.{ts,tsx,page.js}',
'**/plugins/**/*.page.{ts,js,tsx,jsx}',
'packages/ui-default/**/*.{ts,tsx,js,jsx}',
'packages/ui-next/**/*.{ts,tsx,js,jsx}',
],

languageOptions: {
Expand Down
40 changes: 25 additions & 15 deletions framework/framework/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,19 @@ export type KoaContext = Koa.Context & {
holdFiles: (string | File)[];
};

interface RendererContext {
handler: HandlerCommon;
UserContext: UserModel;
url: HandlerCommon['url'];
_: HandlerCommon['translate'];
}
export interface TextRenderer {
output: 'html' | 'json' | 'text';
render: (name: string, args: Record<string, any>, context: Record<string, any>) => string | Promise<string>;
render: (name: string, args: Record<string, any>, context: RendererContext) => string | Promise<string>;
}
export interface BinaryRenderer {
output: 'binary';
render: (name: string, args: Record<string, any>, context: Record<string, any>) => Buffer | Promise<Buffer>;
render: (name: string, args: Record<string, any>, context: RendererContext) => Buffer | Promise<Buffer>;
}
export type Renderer = (BinaryRenderer | TextRenderer) & {
name: string;
Expand Down Expand Up @@ -306,7 +312,12 @@ export class NotFoundHandler extends Handler {
all() { }
}

function executeMiddlewareStack(context: any, middlewares: { name: string, func: Function }[]) {
export interface LayerEntry {
name: string;
func: (ctx: any, next: () => Promise<void>) => any;
}

function executeMiddlewareStack(context: any, middlewares: LayerEntry[]): Promise<void> {
let index = -1;
context.__timers ||= {};
function dispatch(i) {
Expand Down Expand Up @@ -344,9 +355,10 @@ export class WebService extends Service<never> {

private registry: Record<string, any> = Object.create(null);
private registrationCount = Counter();
private serverLayers = [];
private handlerLayers = [];
private wsLayers = [];
public routeMap: Record<string, string> = Object.create(null);
private serverLayers: LayerEntry[] = [];
private handlerLayers: LayerEntry[] = [];
private wsLayers: LayerEntry[] = [];
private captureAllRoutes = Object.create(null);
private customDefaultContext: CordisContext;
private activeHandlers: Map<Handler, { start: number, name: string }> = new Map();
Expand Down Expand Up @@ -712,6 +724,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
}
this.registry[name] = HandlerClass;
this.registrationCount[name]++;
this.routeMap[routeName] = path as string;

const Checker = (args) => {
let perm: bigint;
Expand Down Expand Up @@ -764,6 +777,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
this.ctx.effect(() => () => {
this.registrationCount[name]--;
if (!this.registrationCount[name]) delete this.registry[name];
delete this.routeMap[routeName];
dispose();
});
}
Expand Down Expand Up @@ -794,10 +808,6 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);

// eslint-disable-next-line ts/naming-convention
public Route(name: string, path: string, RouteHandler: typeof Handler, ...permPrivChecker) {
// if (name === 'contest_scoreboard') {
// console.log('+++', this.ctx);
// console.log(this.ctx.scoreboard);
// }
return this.register('route', name, path, RouteHandler, ...permPrivChecker);
}

Expand All @@ -806,7 +816,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
return this.register('conn', name, path, RouteHandler, ...permPrivChecker);
}

private registerLayer(name: 'serverLayers' | 'handlerLayers' | 'wsLayers', layer: any) {
private registerLayer(name: 'serverLayers' | 'handlerLayers' | 'wsLayers', layer: LayerEntry) {
this.ctx.effect(() => {
this[name].push(layer);
return () => {
Expand All @@ -815,19 +825,19 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
});
}

public addServerLayer(name: string, func: any) {
public addServerLayer(name: LayerEntry['name'], func: LayerEntry['func']) {
return this.registerLayer('serverLayers', { name, func });
}

public addHandlerLayer(name: string, func: any) {
public addHandlerLayer(name: LayerEntry['name'], func: LayerEntry['func']) {
return this.registerLayer('handlerLayers', { name, func });
}

public addWSLayer(name: string, func: any) {
public addWSLayer(name: LayerEntry['name'], func: LayerEntry['func']) {
return this.registerLayer('wsLayers', { name, func });
}

public addLayer(name: string, layer: any) {
public addLayer(name: LayerEntry['name'], layer: LayerEntry['func']) {
this.addHandlerLayer(name, layer);
this.addWSLayer(name, layer);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/hydrojudge/vendor/testlib
Submodule testlib updated 1 files
+51 −4 testlib.h
1 change: 1 addition & 0 deletions packages/hydrooj/src/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export * from '@hydrooj/framework/validator';
export * as StorageService from './service/storage';
export { EventMap } from './service/bus';
export { db, pwsh };
export { getNodes } from './lib/ui';

// to load services into to context
export { } from './handler/contest';
8 changes: 8 additions & 0 deletions packages/ui-next/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Context
export { type PageData, usePageData } from './src/context/pageData';

// Hooks
export { useUrl } from './src/hooks/useUrl';

// Registry
export type { ComponentName, RegistryContext } from './src/registry';
1 change: 1 addition & 0 deletions packages/ui-next/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hydro</title>
<!-- __HYDRO_INJECTION__DO_NOT_REMOVE_THIS__ -->
</head>
<body>
<div id="root"></div>
Expand Down
149 changes: 97 additions & 52 deletions packages/ui-next/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,104 @@
import fs from 'fs';
import path from 'path';
import importMetaUrlPlugin from '@codingame/esbuild-import-meta-url-plugin';
import react from '@vitejs/plugin-react-swc';
import c2k from 'koa2-connect';
import { createServer } from 'vite';
import { } from '@hydrooj/framework';
import { serializer } from '@hydrooj/framework';
import { Context } from 'hydrooj';
import importMetaUrlPlugin from '@chialab/esbuild-plugin-meta-url';
import react from '@vitejs/plugin-react';
import c2k from 'koa2-connect/ts';
import { createServer, type Plugin } from 'vite';
import type { PageData } from './src/context/pageData';

const INJECT_MARKER = '<!-- __HYDRO_INJECTION__DO_NOT_REMOVE_THIS__ -->';
const buildInject = (str: string) => `<script id="__HYDRO_INJECTION__" type="application/json">${str}</script>`;

function hydroPlugins(): Plugin {
const virtualModuleId = 'virtual:hydro-plugins';
const resolvedVirtualModuleId = `\0${virtualModuleId}`;

return {
name: 'hydro-plugins',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return undefined;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const entries: string[] = [];
for (const addon of Object.values(global.addons)) {
const uiEntry = path.resolve(addon, 'ui', 'index.ts');
if (fs.existsSync(uiEntry)) entries.push(uiEntry);
}
if (!entries.length) return 'export default [];';
const imports = entries.map((e, i) => `import * as plugin${i} from '${e}';`).join('\n');
const exports = `export default [${entries.map((_, i) => `plugin${i}`).join(', ')}];`;
return `${imports}\n${exports}`;
}
return undefined;
},
};
}

export async function apply(ctx: Context) {
if (process.env.HYDRO_CLI) return;
const vite = await createServer({
server: {
middlewareMode: true,
hmr: {
port: 3010,
},
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
allowedHosts: ['beta.hydro.ac'],
},
appType: 'custom',
root: __dirname,
base: '/',
plugins: [react()],
optimizeDeps: {
esbuildOptions: {
plugins: [
// @ts-ignore
importMetaUrlPlugin,
],
},
},
worker: {
format: 'es',
},
});
const middleware = c2k(vite.middlewares);
const capture = ['/@vite/', '/src/', '/node_modules/', '/@react-refresh', '/@fs'];
for (const route of capture) {
ctx.server.addCaptureRoute(route, middleware);
}
const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8');
ctx.server.registerRenderer('next', {
name: 'next',
accept: ['main.html'],
output: 'html',
asFallback: false,
priority: 100,
render: async (name, args, context) => await vite.transformIndexHtml(context.handler.context.req.url, html),
});
if (process.env.HYDRO_CLI) return;
// 现在只是开发环境的实现,生产环境的实现还未完成
const vite = await createServer({
server: {
middlewareMode: true,
hmr: {
port: 3010,
},
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
// allowedHosts: ['beta.hydro.ac'],
},
appType: 'custom',
root: __dirname,
base: '/',
plugins: [react(), hydroPlugins()],
optimizeDeps: {
esbuildOptions: {
plugins: [
// @ts-ignore
importMetaUrlPlugin(),
],
},
},
worker: {
format: 'es',
},
});
const middleware = c2k(vite.middlewares);
const capture = ['/@vite/', '/src/', '/node_modules/', '/@react-refresh', '/@fs', '/@id/'];
for (const route of capture) {
ctx.server.addCaptureRoute(route, middleware);
}
const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8');
ctx.server.registerRenderer('next', {
name: 'next',
accept: [],
output: 'html',
asFallback: true,
priority: 100,
async render(name, args, context) {
const data: PageData = {
HYDRO_INJECTED: true,
name,
args,
url: context.handler.context.req.url!,
routeMap: ctx.server.routeMap,
};
const serialized = JSON.stringify(data, serializer(false, context.handler));
const htmlToRender = html.replace(INJECT_MARKER, buildInject(serialized));
return await vite.transformIndexHtml(context.handler.context.req.url!, htmlToRender);
},
});

// eslint-disable-next-line consistent-return
return async () => {
await vite.close().catch((e) => console.error(e));
};
// eslint-disable-next-line consistent-return
return async () => {
await vite.close().catch((e) => console.error(e));
};
}
13 changes: 8 additions & 5 deletions packages/ui-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
"name": "@hydrooj/ui-next",
"version": "0.0.0",
"type": "module",
"main": "index.ts",
"types": "api.ts",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@vitejs/plugin-react": "^6.0.1",
"react": "^19.2.5",
"react-dom": "^19.2.5"
"@chialab/esbuild-plugin-meta-url": "^0.19.0",
"@vitejs/plugin-react": "^5.1.2",
"koa2-connect": "^1.0.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.3.0",
"vite": "^8.0.8"
"vite": "^7.3.0"
}
}
13 changes: 9 additions & 4 deletions packages/ui-next/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
function App() {
return <div></div>;
}
import { usePageData } from './context/pageData';
import { Component } from './registry';

export default App;
const AppInner = Component('page:app', () => {
const data = usePageData();

return <div>app, page:{data.name}</div>;
});

export default AppInner;
Loading
Loading