Skip to content
Merged

ui-next #1138

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
58b173a
ui-next: inject args into vite html
renbaoshuo Dec 25, 2025
e2a07e6
core: improved RendererContext typing
renbaoshuo Dec 25, 2025
f2a5a17
ui-next: add hydroPlugins to dynamically load route plugins
renbaoshuo Dec 25, 2025
9d5bff2
ui-next: serialize injected data correctly
renbaoshuo Dec 26, 2025
562ff3a
*: update devcontainer config
renbaoshuo Dec 28, 2025
8f366a0
ui-next: refactor addon ui patch resolving
renbaoshuo Mar 21, 2026
8d7a48b
ui-next: enable for all pages
renbaoshuo Mar 22, 2026
5b20f42
ui-next: add component registry
renbaoshuo Mar 22, 2026
e147b17
ui-next: page data context
renbaoshuo Mar 24, 2026
0b72aea
ui-next: refactor code structure
renbaoshuo Mar 24, 2026
bfca73c
ui-next: inject route map
renbaoshuo Apr 21, 2026
c6b4d4f
workspace: include ui-next package in eslint configuration
renbaoshuo Apr 21, 2026
3575ad6
ui-next: add missing return statements in hydroPlugins function
renbaoshuo Apr 21, 2026
0d16157
ui-next: format index.ts
renbaoshuo Apr 21, 2026
2bc1f58
ui-next: better page data context
renbaoshuo Apr 21, 2026
950910f
ui-next: url builder
renbaoshuo Apr 21, 2026
f753d1b
ui-next: restructure registry context
renbaoshuo Apr 21, 2026
b5ec79d
ui-next: use route name
renbaoshuo Apr 24, 2026
b441356
framework: prepare for ui-next data fetching logics
renbaoshuo Apr 24, 2026
efc9294
ui-next: use kebab-case for file naming
renbaoshuo Apr 24, 2026
f71d641
ui-next: navigation
renbaoshuo Apr 24, 2026
15dd1fc
ui-next: registry hmr support
renbaoshuo Apr 24, 2026
6ce3025
framework: build routeMap from koa router stack
renbaoshuo Apr 24, 2026
4acb572
ui-next: disable vite clear terminal screen behavior
renbaoshuo Apr 25, 2026
979cda7
ui-next: refactor router
renbaoshuo Apr 26, 2026
b66bdfa
ui-next: fix indent for server-side files
renbaoshuo Apr 28, 2026
8bb0892
ui-next: remove _ui_next search param
renbaoshuo Apr 30, 2026
28f977f
ui-next: refactor component registry structure
renbaoshuo May 4, 2026
ad49272
ui-next: handle link navigation correctly
renbaoshuo May 5, 2026
504b963
ui-next: add sample plugin
renbaoshuo May 6, 2026
871b4c2
ui-next: endpoint support
renbaoshuo May 6, 2026
8232b89
ui-next: prod build
renbaoshuo May 8, 2026
50ed8a6
ui-next: error boundary
renbaoshuo May 9, 2026
8dc7a73
ui-next: catch installPlugin errors
renbaoshuo May 10, 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"
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ packages/**/*.js
packages/ui-default/public
packages/ui-default/misc/.iconfont
packages/ui-default/static/locale
packages/ui-next/public
plugins/
modules/

Expand Down
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: 0 additions & 1 deletion build/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ const UINextConfig = {
module: 'ESNext',
skipLibCheck: true,
allowSyntheticDefaultImports: true,
baseUrl: '.',
jsx: 'react-jsx',
outDir: path.join(baseOutDir, 'ui-next'),

Expand Down
6 changes: 6 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-len */
/* eslint-disable ts/naming-convention */
import path from 'node:path';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
import react from '@hydrooj/eslint-config';

Expand Down Expand Up @@ -67,6 +68,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/src/**/*.{ts,tsx,js,jsx}',
],

languageOptions: {
Expand Down Expand Up @@ -193,12 +195,16 @@ const config = react({
},
}, {
files: ['packages/ui-next/src/**/*.{ts,tsx}'],
plugins: {
'react-hooks': reactHooks,
},
languageOptions: {
globals: {
...globals.browser,
},
},
rules: {
...reactHooks.configs.recommended.rules,
'style/indent': ['warn', 2],
'style/indent-binary-ops': ['warn', 2],
},
Expand Down
15 changes: 11 additions & 4 deletions framework/framework/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,20 @@ export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
response.type = 'application/json';
} else if (
request.json || response.redirect
|| request.query.noTemplate || !response.template) {
|| request.query.noTemplate || !response.template // no template, send raw data
) {
// Send raw data
try {
if (typeof response.body === 'object' && request.headers['x-hydro-inject']) {
if (request.headers['x-hydro-inject']) {
const inject = request.headers['x-hydro-inject'].toString().toLowerCase().split(',').map((i) => i.trim());
if (inject.includes('uicontext')) response.body.UiContext = UiContext;
if (inject.includes('usercontext')) response.body.UserContext = user;
if (inject.includes('pagename')) {
ctx.set('x-hydro-page', ctx._matchedRouteName || '');
}
if (response.body !== null && typeof response.body === 'object') {
if (inject.includes('uicontext')) response.body.UiContext = UiContext;
if (inject.includes('usercontext')) response.body.UserContext = user;
if (inject.includes('routemap')) response.body.routeMap = handler.ctx.server.routeMap;
}
}
response.body = JSON.stringify(response.body, serializer(false, handler));
} catch (e) {
Expand Down
55 changes: 40 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,9 @@ export class WebService extends Service<never> {

private registry: Record<string, any> = Object.create(null);
private registrationCount = Counter();
private serverLayers = [];
private handlerLayers = [];
private wsLayers = [];
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 All @@ -355,6 +366,22 @@ export class WebService extends Service<never> {
server = koa;
router = router;
HandlerCommon = HandlerCommon;
private _routeMap: Record<string, string> = Object.create(null);

get routeMap(): Record<string, string> {
return this._routeMap;
}

private rebuildRouteMap() {
const map: Record<string, string> = Object.create(null);
for (const layer of this.router.stack) {
if (layer.name && typeof layer.path === 'string') {
map[layer.name] = layer.path;
}
}
this._routeMap = map;
}

Handler = Handler;
ConnectionHandler = ConnectionHandler;

Expand Down Expand Up @@ -759,12 +786,14 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
}
}
const dispose = router.disposeLastOp;
this.rebuildRouteMap();
// @ts-ignore
this.ctx.parallel(`handler/register/${name}`, HandlerClass);
this.ctx.effect(() => () => {
this.registrationCount[name]--;
if (!this.registrationCount[name]) delete this.registry[name];
dispose();
this.rebuildRouteMap();
});
}

Expand Down Expand Up @@ -794,10 +823,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 +831,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 +840,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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build:ui:dev:https": "node packages/ui-default/build --iconfont && node --trace-deprecation packages/ui-default/build --dev --https",
"build:ui:production": "cross-env-shell NODE_OPTIONS=--max_old_space_size=8192 \"node packages/ui-default/build --iconfont && node packages/ui-default/build --production\"",
"build:ui:production:webpack": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node packages/ui-default/build --production",
"build:ui-next": "tsc -p tsconfig.ui-next.json --noEmit && yarn workspace @hydrooj/ui-next vite build",
Comment thread
renbaoshuo marked this conversation as resolved.
"test": "node -r @hydrooj/register packages/common/tests/subtask.spec.ts && node test/entry.js",
"benchmark": "cross-env BENCHMARK=true node test/entry.js",
"oxlint": "cd framework/eslint-config && node -r @hydrooj/register build.ts && cd ../../ && oxlint --config framework/eslint-config/.oxlintrc.json",
Expand Down
6 changes: 6 additions & 0 deletions packages/ui-next-plugin-sample/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@hydrooj/ui-next-plugin-sample",
"dependencies": {
"@hydrooj/ui-next": "workspace:*"
}
}
5 changes: 5 additions & 0 deletions packages/ui-next-plugin-sample/ui/before.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function BeforeComponent() {
console.log('before app');
// throw new Error('test error boundary in before interceptor');
return <div>before app via @hydrooj/ui-next-plugin-sample</div>;
}
6 changes: 6 additions & 0 deletions packages/ui-next-plugin-sample/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { PluginAPI } from '@hydrooj/ui-next';
import BeforeComponent from './before';

export function setup(api: PluginAPI) {
api.before('page:app', BeforeComponent);
}
1 change: 1 addition & 0 deletions packages/ui-next/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/api';
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
Loading
Loading