From 5b20f42de7ec4a9ce2f28d6a4f6c55aee2f24be0 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Sun, 22 Mar 2026 21:37:31 +0800 Subject: [PATCH] ui-next: add component registry --- packages/ui-next/api.ts | 2 + packages/ui-next/index.ts | 3 +- packages/ui-next/package.json | 2 + packages/ui-next/src/App.tsx | 8 ++-- packages/ui-next/src/context.ts | 16 +++++++ packages/ui-next/src/main.tsx | 15 ++++-- packages/ui-next/src/registry.tsx | 76 ++++++++++++++++++++++++++++++ packages/ui-next/src/vite-env.d.ts | 6 +++ 8 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 packages/ui-next/api.ts create mode 100644 packages/ui-next/src/context.ts create mode 100644 packages/ui-next/src/registry.tsx diff --git a/packages/ui-next/api.ts b/packages/ui-next/api.ts new file mode 100644 index 000000000..930999c04 --- /dev/null +++ b/packages/ui-next/api.ts @@ -0,0 +1,2 @@ +export type { Context } from './src/context'; +export type { ComponentName } from './src/registry'; diff --git a/packages/ui-next/index.ts b/packages/ui-next/index.ts index c3577227e..7611e4d5b 100644 --- a/packages/ui-next/index.ts +++ b/packages/ui-next/index.ts @@ -29,7 +29,7 @@ function hydroPlugins(): Plugin { if (fs.existsSync(uiEntry)) entries.push(uiEntry); } if (!entries.length) return 'export default [];'; - const imports = entries.map((e, i) => `import plugin${i} from '${e}';`).join('\n'); + 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}`; } @@ -39,6 +39,7 @@ function hydroPlugins(): Plugin { export async function apply(ctx: Context) { if (process.env.HYDRO_CLI) return; + // 现在只是开发环境的实现,生产环境的实现还未完成 const vite = await createServer({ server: { middlewareMode: true, diff --git a/packages/ui-next/package.json b/packages/ui-next/package.json index 30d7d39b8..179b82a56 100644 --- a/packages/ui-next/package.json +++ b/packages/ui-next/package.json @@ -2,6 +2,8 @@ "name": "@hydrooj/ui-next", "version": "0.0.0", "type": "module", + "main": "index.ts", + "types": "api.ts", "scripts": { "dev": "vite", "build": "tsc -b && vite build", diff --git a/packages/ui-next/src/App.tsx b/packages/ui-next/src/App.tsx index 6f74d94b5..b0fcd4e9e 100644 --- a/packages/ui-next/src/App.tsx +++ b/packages/ui-next/src/App.tsx @@ -1,5 +1,5 @@ -function App() { - return
; -} +import { Component } from './registry'; -export default App; +const AppInner = Component('page:app', () =>
app
); + +export default AppInner; diff --git a/packages/ui-next/src/context.ts b/packages/ui-next/src/context.ts new file mode 100644 index 000000000..eae2b5951 --- /dev/null +++ b/packages/ui-next/src/context.ts @@ -0,0 +1,16 @@ +import { + after, before, Component, patch, +} from './registry'; + +export interface Context { + before: typeof before; + after: typeof after; + patch: typeof patch; + register: typeof Component; +} + +export function createContext(): Context { + return { + before, after, patch, register: Component, + }; +} diff --git a/packages/ui-next/src/main.tsx b/packages/ui-next/src/main.tsx index 8b9bc3ccd..7c8beca82 100644 --- a/packages/ui-next/src/main.tsx +++ b/packages/ui-next/src/main.tsx @@ -1,7 +1,16 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import plugins from 'virtual:hydro-plugins'; import App from './App'; +import { createContext } from './context'; -createRoot(document.getElementById('root')!).render( - -); +const ctx = createContext(); +for (const plugin of plugins) { + plugin.apply(ctx); +} + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/packages/ui-next/src/registry.tsx b/packages/ui-next/src/registry.tsx new file mode 100644 index 000000000..99105f9ba --- /dev/null +++ b/packages/ui-next/src/registry.tsx @@ -0,0 +1,76 @@ +/* eslint-disable react-refresh/only-export-components */ + +import React from 'react'; + +type Scope = 'page' | 'component'; +export type ComponentName = `${Scope}:${string}`; + +interface BeforeAfterEntry { + id: string; + Comp: React.FC; +} + +interface PatchEntry { + id: string; + cb: (props: any) => any; +} + +const registry = { + components: {} as Record>, + before: {} as Record, + after: {} as Record, + patch: {} as Record, +}; + +let idCounter = 0; +function genId() { return `__auto_${++idCounter}`; } + +function upsert(list: T[], entry: T) { + const idx = list.findIndex((e) => e.id === entry.id); + if (idx !== -1) list[idx] = entry; + else list.push(entry); +} + +export function Component

>( + name: ComponentName, + Inner: React.FC

, +): React.FC

{ + if (registry.components[name]) { + console.warn(`[Registry] Component "${name}" is being re-registered.`); + } + const Wrapped: React.FC

= (props) => { + let patched = props; + for (const { cb } of registry.patch[name] ?? []) { + try { + patched = cb(patched); + } catch (e) { + console.error(`[Registry] patch error in "${name}":`, e); + } + } + return ( + <> + {(registry.before[name] ?? []).map(({ id, Comp }) => )} + + {(registry.after[name] ?? []).map(({ id, Comp }) => )} + + ); + }; + Wrapped.displayName = `Registry(${name})`; + registry.components[name] = Wrapped; + return Wrapped; +} + +export function before(name: ComponentName, Comp: React.FC, id?: string) { + const list = registry.before[name] ??= []; + upsert(list, { id: id ?? genId(), Comp }); +} + +export function after(name: ComponentName, Comp: React.FC, id?: string) { + const list = registry.after[name] ??= []; + upsert(list, { id: id ?? genId(), Comp }); +} + +export function patch(name: ComponentName, cb: (props: any) => any, id?: string) { + const list = registry.patch[name] ??= []; + upsert(list, { id: id ?? genId(), cb }); +} diff --git a/packages/ui-next/src/vite-env.d.ts b/packages/ui-next/src/vite-env.d.ts index 11f02fe2a..617da4f3f 100644 --- a/packages/ui-next/src/vite-env.d.ts +++ b/packages/ui-next/src/vite-env.d.ts @@ -1 +1,7 @@ /// + +declare module 'virtual:hydro-plugins' { + import type { RegistryContext } from './registry'; + const plugins: Array<{ apply: (ctx: RegistryContext) => void }>; + export default plugins; +}