diff --git a/packages/ui-next/api.ts b/packages/ui-next/api.ts index 71757047b..f7499b65f 100644 --- a/packages/ui-next/api.ts +++ b/packages/ui-next/api.ts @@ -7,4 +7,9 @@ export { type RouterState, useNavigate, useRouterState } from './src/context/rou export { useUrl } from './src/hooks/use-url'; // Registry -export type { ComponentName, RegistryContext } from './src/registry'; +export type { + Interceptor, InterceptorEntry, InterceptorOptions, + PluginAPI, PluginDefinition, + SlotName, +} from './src/registry'; +export { defineSlot } from './src/registry'; diff --git a/packages/ui-next/src/app.tsx b/packages/ui-next/src/app.tsx index c47277cc7..9aa6f484e 100644 --- a/packages/ui-next/src/app.tsx +++ b/packages/ui-next/src/app.tsx @@ -1,8 +1,8 @@ import { Link } from './components/link'; import { usePageData } from './context/page-data'; -import { Component } from './registry'; +import { before, defineSlot } from './registry'; -const AppInner = Component('page:app', () => { +const AppInner = defineSlot('page:app', () => { const data = usePageData(); return ( @@ -15,4 +15,13 @@ const AppInner = Component('page:app', () => { ); }); +before('page:app', () => { + console.log('before app'); + return ( + <> +
>( - 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 }) => (
+ name: SlotName,
+ interceptor: Interceptor ,
+ opts?: InterceptorOptions,
+): () => void {
+ return store.addInterceptor(name, interceptor, opts);
+}
+
+/** default priority: -100 */
+export function before = Record ,
+ opts?: InterceptorOptions,
+): () => void {
+ return intercept (
+ name,
+ (props, next) => (
+ <>
+ = Record ,
+ opts?: InterceptorOptions,
+): () => void {
+ return intercept (
+ name,
+ (props, next) => (
+ <>
+ {next()}
+ (
+ name: SlotName,
+ patcher: (props: P) => Partial ,
+ opts?: InterceptorOptions,
+): () => void {
+ return intercept (
+ name,
+ (props, next) => next(patcher(props)),
+ { priority: -50, ...opts },
+ );
+}
+
+/** default priority: 0 */
+export function replace = Record ,
+ opts?: { id?: string, priority?: number },
+): () => void {
+ return intercept (
+ name,
+ (props, _next) => = Record (
+ name,
+ (props, next) => >(
+ interceptors: InterceptorEntry [],
+ DefaultComp: React.FC ,
+): (props: P) => React.ReactNode {
+ let pipeline: (props: P) => React.ReactNode = (props) => >(
+ name: SlotName,
+ DefaultComp: React.FC ,
+): React.FC {
+ store.setDefault(name, DefaultComp);
+
+ const subscribeSlot = (cb: () => void) => store.subscribe(name, cb);
+ const getSnapshot = () => store.getVersion(name);
+
+ const SlotComponent: React.FC = (props) => {
+ const version = useSyncExternalStore(subscribeSlot, getSnapshot);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const chain = useMemo(() => buildChain(store.getInterceptors(name), store.getDefault(name)!), [version]);
+
+ return <>{chain(props)}>;
+ };
+
+ SlotComponent.displayName = `Slot(${name})`;
+ return SlotComponent;
+}
diff --git a/packages/ui-next/src/registry/store.ts b/packages/ui-next/src/registry/store.ts
new file mode 100644
index 000000000..5838af9b7
--- /dev/null
+++ b/packages/ui-next/src/registry/store.ts
@@ -0,0 +1,94 @@
+import type { InterceptorEntry, InterceptorOptions, SlotName } from './types';
+
+type Listener = () => void;
+
+interface RegistryState {
+ interceptors: Record (
+ name: SlotName,
+ interceptor: (props: P, next: (overrideProps?: Partial ) => React.ReactNode) => React.ReactNode,
+ opts?: InterceptorOptions,
+ ): () => void {
+ const entry: InterceptorEntry = {
+ id: opts?.id ?? genId(),
+ priority: opts?.priority ?? 0,
+ interceptor,
+ };
+
+ const list = (state.interceptors[name] ??= []);
+ const idx = list.findIndex((e) => e.id === entry.id);
+ if (idx !== -1) list[idx] = entry;
+ else list.push(entry);
+
+ list.sort((a, b) => a.priority - b.priority);
+ bumpVersion(name);
+
+ return () => {
+ const arr = state.interceptors[name];
+ if (!arr) return;
+ const i = arr.findIndex((e) => e.id === entry.id);
+ if (i !== -1) {
+ arr.splice(i, 1);
+ bumpVersion(name);
+ }
+ };
+ }
+
+ function getInterceptors(name: SlotName): InterceptorEntry[] {
+ return state.interceptors[name] ?? [];
+ }
+
+ function setDefault(name: SlotName, comp: React.FC = (
+ props: P,
+ next: (overrideProps?: Partial ) => React.ReactNode,
+) => React.ReactNode;
+
+export interface InterceptorOptions {
+ id?: string;
+ /** 越小越靠外,越先执行 */
+ priority?: number;
+}
+
+export interface InterceptorEntry {
+ id: string;
+ /** 越小越靠外,越先执行 */
+ priority: number;
+ interceptor: Interceptor ;
+}
diff --git a/packages/ui-next/src/vite-env.d.ts b/packages/ui-next/src/vite-env.d.ts
index 617da4f3f..4fc67de75 100644
--- a/packages/ui-next/src/vite-env.d.ts
+++ b/packages/ui-next/src/vite-env.d.ts
@@ -1,7 +1,7 @@
///