mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
ui-next: page registration (#1159)
This commit is contained in:
1 parent
66a8a0ab9e
commit
bbd7fbc852
28 files changed
+238
-98
No files matched your search
+2
-2
@@ -49,8 +49,8 @@ packages/ui-default/public
|
|||||||
packages/ui-default/misc/.iconfont
|
packages/ui-default/misc/.iconfont
|
||||||
packages/ui-default/static/locale
|
packages/ui-default/static/locale
|
||||||
packages/ui-next/public
|
packages/ui-next/public
|
||||||
plugins/
|
/plugins/
|
||||||
modules/
|
/modules/
|
||||||
|
|
||||||
# Data files
|
# Data files
|
||||||
*.mmdb
|
*.mmdb
|
||||||
|
|||||||
@@ -159,9 +159,6 @@ const UINextConfig = {
|
|||||||
'@/*': [
|
'@/*': [
|
||||||
'./packages/ui-next/src/*',
|
'./packages/ui-next/src/*',
|
||||||
],
|
],
|
||||||
'vj/*': [
|
|
||||||
'./packages/ui-default/*',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Whitespace-only changes.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@hydrooj/ui-next-plugin",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"author": "Baoshuo <i@baoshuo.ren>",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"hydro": {
|
||||||
|
"cli": false
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hydrooj/ui-next": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { PluginAPI } from '@hydrooj/ui-next';
|
||||||
|
|
||||||
|
export function setup(api: PluginAPI) {
|
||||||
|
api.before('page:app', () => {
|
||||||
|
console.log('before app');
|
||||||
|
// throw new Error('test error boundary in before interceptor');
|
||||||
|
return <div>before app via @hydrooj/ui-next-plugin-sample</div>;
|
||||||
|
});
|
||||||
|
}
|
||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"packages/*",
|
"packages/*",
|
||||||
"framework/*",
|
"framework/*",
|
||||||
"plugins/*",
|
"plugins/*",
|
||||||
"modules/*"
|
"modules/*",
|
||||||
|
"examples/plugins/*"
|
||||||
],
|
],
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@hydrooj/ui-next-plugin-sample",
|
|
||||||
"dependencies": {
|
|
||||||
"@hydrooj/ui-next": "workspace:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { PluginAPI } from '@hydrooj/ui-next';
|
|
||||||
import BeforeComponent from './before';
|
|
||||||
|
|
||||||
export function setup(api: PluginAPI) {
|
|
||||||
api.before('page:app', BeforeComponent);
|
|
||||||
}
|
|
||||||
+37
-29
@@ -1,7 +1,6 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import c2k from 'koa2-connect/ts';
|
import c2k from 'koa2-connect/ts';
|
||||||
import { createServer, type Plugin } from 'vite';
|
import { createServer, type Plugin } from 'vite';
|
||||||
@@ -28,6 +27,20 @@ const PENDING_HTML = `<html>
|
|||||||
const INJECT_MARKER = '<!-- __HYDRO_INJECTION__DO_NOT_REMOVE_THIS__ -->';
|
const INJECT_MARKER = '<!-- __HYDRO_INJECTION__DO_NOT_REMOVE_THIS__ -->';
|
||||||
const buildInject = (data: string) => `<script id="__HYDRO_INJECTION__" type="application/json">${data}</script>`;
|
const buildInject = (data: string) => `<script id="__HYDRO_INJECTION__" type="application/json">${data}</script>`;
|
||||||
|
|
||||||
|
function getAddonEntries(): Record<string, string> {
|
||||||
|
const entries: Record<string, string> = {};
|
||||||
|
for (const [name, addon] of Object.entries(global.addons)) {
|
||||||
|
const uiEntry = ['ui/index.ts', 'ui/index.tsx', 'ui/index.js', 'ui/index.jsx']
|
||||||
|
.map((f) => path.resolve(addon as string, f))
|
||||||
|
.find((f) => fs.existsSync(f));
|
||||||
|
if (uiEntry) {
|
||||||
|
logger.info('UI entry for addon %s: %s', name, uiEntry);
|
||||||
|
entries[name] = uiEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
function hydroPlugins(): Plugin {
|
function hydroPlugins(): Plugin {
|
||||||
const virtualModuleId = 'virtual:hydro-plugins';
|
const virtualModuleId = 'virtual:hydro-plugins';
|
||||||
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
||||||
@@ -42,14 +55,12 @@ function hydroPlugins(): Plugin {
|
|||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === resolvedVirtualModuleId) {
|
if (id === resolvedVirtualModuleId) {
|
||||||
const entries: string[] = [];
|
const entries = getAddonEntries();
|
||||||
for (const addon of Object.values(global.addons)) {
|
if (!Object.keys(entries).length) return 'export default [];';
|
||||||
const uiEntry = path.resolve(addon, 'ui', 'index.ts');
|
const imports = Object.entries(entries).map(([_, e], i) => `import * as plugin${i} from '${e}';`).join('\n');
|
||||||
if (fs.existsSync(uiEntry)) entries.push(uiEntry);
|
const exports = `export default [${Object.entries(entries).map(([addon, _], i) => {
|
||||||
}
|
return `{ name: '${addon}', ...plugin${i} }`;
|
||||||
if (!entries.length) return 'export default [];';
|
}).join(', ')}];`;
|
||||||
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 `${imports}\n${exports}`;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -105,12 +116,9 @@ class UiNextConstantHandler extends Handler {
|
|||||||
export async function buildPlugins() {
|
export async function buildPlugins() {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
const entries: string[] = [];
|
const entries = getAddonEntries();
|
||||||
for (const addon of Object.values(global.addons)) {
|
|
||||||
const uiEntry = path.resolve(addon as string, 'ui', 'index.ts');
|
if (!Object.keys(entries).length) {
|
||||||
if (fs.existsSync(uiEntry)) entries.push(uiEntry);
|
|
||||||
}
|
|
||||||
if (!entries.length) {
|
|
||||||
vfs['plugins.js'] = 'window.__hydroPlugins = [];';
|
vfs['plugins.js'] = 'window.__hydroPlugins = [];';
|
||||||
hashes['plugins.js'] = '00000000';
|
hashes['plugins.js'] = '00000000';
|
||||||
logger.info('No plugins to build');
|
logger.info('No plugins to build');
|
||||||
@@ -121,11 +129,8 @@ export async function buildPlugins() {
|
|||||||
const result = await esbuild.build({
|
const result = await esbuild.build({
|
||||||
stdin: {
|
stdin: {
|
||||||
contents: [
|
contents: [
|
||||||
...entries.map((e, i) => `import * as plugin${i} from '${e}';`),
|
...Object.entries(entries).map(([_, e], i) => `import * as plugin${i} from '${e}';`),
|
||||||
`window.__hydroPlugins = [${entries.map((e, i) => {
|
`window.__hydroPlugins = [${Object.entries(entries).map(([n], i) => `{ name: '${n}', ...plugin${i} }`).join(', ')}];`,
|
||||||
const addonName = path.basename(path.resolve(e, '..', '..'));
|
|
||||||
return `{ name: '${addonName}', ...plugin${i} }`;
|
|
||||||
}).join(', ')}];`,
|
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
resolveDir: process.cwd(),
|
resolveDir: process.cwd(),
|
||||||
loader: 'ts',
|
loader: 'ts',
|
||||||
@@ -155,7 +160,7 @@ export async function apply(ctx: Context) {
|
|||||||
|
|
||||||
if (process.env.DEV) {
|
if (process.env.DEV) {
|
||||||
const vite = await createServer({
|
const vite = await createServer({
|
||||||
configFile: false,
|
root: __dirname,
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
middlewareMode: true,
|
middlewareMode: true,
|
||||||
@@ -168,12 +173,7 @@ export async function apply(ctx: Context) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appType: 'custom',
|
appType: 'custom',
|
||||||
root: __dirname,
|
plugins: [hydroPlugins()],
|
||||||
base: '/',
|
|
||||||
plugins: [react(), hydroPlugins()],
|
|
||||||
worker: {
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const middleware = c2k(vite.middlewares);
|
const middleware = c2k(vite.middlewares);
|
||||||
const capture = ['/@vite/', '/src/', '/node_modules/', '/@react-refresh', '/@fs', '/@id/'];
|
const capture = ['/@vite/', '/src/', '/node_modules/', '/@react-refresh', '/@fs', '/@id/'];
|
||||||
@@ -191,7 +191,11 @@ export async function apply(ctx: Context) {
|
|||||||
const serialized = JSON.stringify({
|
const serialized = JSON.stringify({
|
||||||
HYDRO_INJECTED: true,
|
HYDRO_INJECTED: true,
|
||||||
name: context.handler.context._matchedRouteName,
|
name: context.handler.context._matchedRouteName,
|
||||||
args,
|
args: {
|
||||||
|
UserContext: context.UserContext,
|
||||||
|
UiContext: context.handler.UiContext,
|
||||||
|
...args,
|
||||||
|
},
|
||||||
url: context.handler.context.req.url!,
|
url: context.handler.context.req.url!,
|
||||||
route_map: ctx.server.routeMap,
|
route_map: ctx.server.routeMap,
|
||||||
endpoint: ctx.setting.get('server.url') || undefined,
|
endpoint: ctx.setting.get('server.url') || undefined,
|
||||||
@@ -220,7 +224,11 @@ export async function apply(ctx: Context) {
|
|||||||
const serialized = JSON.stringify({
|
const serialized = JSON.stringify({
|
||||||
HYDRO_INJECTED: true,
|
HYDRO_INJECTED: true,
|
||||||
name: context.handler.context._matchedRouteName,
|
name: context.handler.context._matchedRouteName,
|
||||||
args,
|
args: {
|
||||||
|
UserContext: context.UserContext,
|
||||||
|
UiContext: context.handler.UiContext,
|
||||||
|
...args,
|
||||||
|
},
|
||||||
url: context.handler.context.req.url!,
|
url: context.handler.context.req.url!,
|
||||||
route_map: ctx.server.routeMap,
|
route_map: ctx.server.routeMap,
|
||||||
endpoint: ctx.setting.get('server.url') || undefined,
|
endpoint: ctx.setting.get('server.url') || undefined,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export { Link, type LinkProps } from './components/link';
|
|||||||
// Context
|
// Context
|
||||||
export { type PageData, usePageData } from './context/page-data';
|
export { type PageData, usePageData } from './context/page-data';
|
||||||
export { type RouterState, useNavigate, useRouterState } from './context/router';
|
export { type RouterState, useNavigate, useRouterState } from './context/router';
|
||||||
export { useUrl } from './hooks/use-url';
|
export { useBuildUrl } from './hooks/use-build-url';
|
||||||
|
|
||||||
// Registry
|
// Registry
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
import { Link } from './components/link';
|
import { Suspense, useMemo, useSyncExternalStore } from 'react';
|
||||||
import { usePageData } from './context/page-data';
|
import { usePageData } from './context/page-data';
|
||||||
import { defineSlot } from './registry';
|
import { defineSlot, SlotName } from './registry';
|
||||||
|
import { SlotErrorBoundary } from './registry/error-boundary';
|
||||||
|
import { store } from './registry/store';
|
||||||
|
|
||||||
const AppInner = defineSlot('page:app', () => {
|
const App = defineSlot('app:root', () => {
|
||||||
const data = usePageData();
|
const { name } = usePageData();
|
||||||
|
const slotName: SlotName = `page:${name}`;
|
||||||
|
|
||||||
|
const [subscribe, getSnapshot] = useMemo(() => [
|
||||||
|
(cb: () => void) => store.subscribe(slotName, cb),
|
||||||
|
() => store.getVersion(slotName),
|
||||||
|
], [slotName]);
|
||||||
|
|
||||||
|
useSyncExternalStore(subscribe, getSnapshot);
|
||||||
|
|
||||||
|
const Comp = store.getDefault(slotName);
|
||||||
|
|
||||||
|
if (!Comp) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Page not found: <code>{name}</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SlotErrorBoundary slotName={slotName} label="renderer">
|
||||||
<div>app, page:{data.name}</div>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<div>
|
<Comp />
|
||||||
<Link to="homepage">homepage</Link> <Link to="problem_main">problem_main</Link>
|
</Suspense>
|
||||||
</div>
|
</SlotErrorBoundary>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AppInner;
|
export default App;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { defineSlot } from '../registry';
|
||||||
|
|
||||||
|
const Layout = defineSlot('app:layout', ({ children }: React.PropsWithChildren) => <>{children}</>);
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useNavigate } from '../context/router';
|
import { useNavigate } from '../context/router';
|
||||||
import { useUrl } from '../hooks/use-url';
|
import { type UrlParams, useBuildUrl } from '../hooks/use-build-url';
|
||||||
|
|
||||||
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
||||||
/** Pre-built href. Use this or `to`, not both. */
|
/** Pre-built href. Use this or `to`, not both. */
|
||||||
@@ -8,15 +8,20 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
|
|||||||
/** Route name to resolve via the route map. */
|
/** Route name to resolve via the route map. */
|
||||||
to?: string;
|
to?: string;
|
||||||
/** Params for route resolution when `to` is given. */
|
/** Params for route resolution when `to` is given. */
|
||||||
params?: Record<string, string>;
|
params?: UrlParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isModifiedEvent(e: React.MouseEvent): boolean {
|
function isModifiedEvent(e: React.MouseEvent): boolean {
|
||||||
return e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
|
return e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Link: React.FC<React.PropsWithChildren<LinkProps>> = ({ href, to, params, onClick, target, download = false, children, ...rest }) => {
|
export const Link: React.FC<React.PropsWithChildren<LinkProps>> = ({
|
||||||
const buildUrl = useUrl();
|
href, to, params,
|
||||||
|
onClick, target, download = false,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const buildUrl = useBuildUrl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const resolvedHref = useMemo(() => (to ? buildUrl(to, params) : (href ?? '#')), [buildUrl, href, to, params]);
|
const resolvedHref = useMemo(() => (to ? buildUrl(to, params) : (href ?? '#')), [buildUrl, href, to, params]);
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { createContext, type ReactNode, useContext, useMemo, useState } from 're
|
|||||||
|
|
||||||
export interface PageData {
|
export interface PageData {
|
||||||
name: string;
|
name: string;
|
||||||
args: Record<string, any>;
|
args: {
|
||||||
|
UserContext: Record<string, any>;
|
||||||
|
UiContext: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,3 +44,11 @@ export function usePageData(): PageData {
|
|||||||
export function useSetPageData(): React.Dispatch<React.SetStateAction<PageData>> {
|
export function useSetPageData(): React.Dispatch<React.SetStateAction<PageData>> {
|
||||||
return usePageDataContext().setData;
|
return usePageDataContext().setData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUiContext(): PageData['args']['UiContext'] {
|
||||||
|
return usePageDataContext().data.args.UiContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserContext(): PageData['args']['UserContext'] {
|
||||||
|
return usePageDataContext().data.args.UserContext;
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ export const isInjected: boolean = !!injectionData.HYDRO_INJECTED;
|
|||||||
export const hydroDomains: string[] = injectionData.hydro_domains ?? [];
|
export const hydroDomains: string[] = injectionData.hydro_domains ?? [];
|
||||||
export const pluginsUrl: string | undefined = injectionData.plugins_url;
|
export const pluginsUrl: string | undefined = injectionData.plugins_url;
|
||||||
|
|
||||||
// routeMap as an external store for useSyncExternalStore, with HMR state preservation
|
|
||||||
interface RouteMapStore {
|
interface RouteMapStore {
|
||||||
_routeMap: Record<string, string>;
|
_routeMap: Record<string, string>;
|
||||||
_listeners: Set<() => void>;
|
_listeners: Set<() => void>;
|
||||||
@@ -65,6 +64,6 @@ export const endpointOrigins = new Set(endpoints.map((ep) => new URL(ep).origin)
|
|||||||
|
|
||||||
export const initialPage: PageData = {
|
export const initialPage: PageData = {
|
||||||
name: (injectionData.name as string) || '',
|
name: (injectionData.name as string) || '',
|
||||||
args: (injectionData.args as Record<string, any>) || {},
|
args: (injectionData.args as any) || {},
|
||||||
url: (injectionData.url as string) || (window.location.pathname + window.location.search),
|
url: (injectionData.url as string) || (window.location.pathname + window.location.search),
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { compile } from 'path-to-regexp';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useUiContext } from '@/context/page-data';
|
||||||
|
import { useRouteMap } from './use-route-map';
|
||||||
|
|
||||||
|
export interface UrlParams {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBuildUrl() {
|
||||||
|
const routeMap = useRouteMap();
|
||||||
|
const { domainId, domain } = useUiContext();
|
||||||
|
|
||||||
|
const getPrefix = useCallback((id?: string) => {
|
||||||
|
id ||= domainId;
|
||||||
|
const domainHost = Array.isArray(domain.host) ? domain.host : [domain.host];
|
||||||
|
const currentHost = window.location.host;
|
||||||
|
return id === (domainHost && domainHost.includes(currentHost) ? domainId : 'system') ? '' : `/d/${id}`;
|
||||||
|
}, [domainId, domain]);
|
||||||
|
|
||||||
|
return useCallback((name: string, params: UrlParams = {}, searchParams: Record<string, string> = {}): string => {
|
||||||
|
const pattern = routeMap[name];
|
||||||
|
if (!pattern) {
|
||||||
|
console.warn(`[Hydro] Unknown route: ${name}`);
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { domainId: paramDomainId, ...routeParams } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prefix = getPrefix(paramDomainId);
|
||||||
|
const path = compile(pattern)(routeParams);
|
||||||
|
const search = new URLSearchParams(searchParams).toString();
|
||||||
|
if (prefix) return `${prefix}${path}${search ? `?${search}` : ''}`;
|
||||||
|
return `${path}${search ? `?${search}` : ''}`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Hydro] Failed to build URL for route "${name}":`, e);
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
}, [routeMap, getPrefix]);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { compile } from 'path-to-regexp';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useRouteMap } from './use-route-map';
|
|
||||||
|
|
||||||
function buildUrl(pattern: string, params: Record<string, string> = {}): string {
|
|
||||||
try {
|
|
||||||
return compile(pattern)(params);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`[Hydro] Failed to build URL for pattern "${pattern}":`, err);
|
|
||||||
return '#';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUrl() {
|
|
||||||
const routeMap = useRouteMap();
|
|
||||||
|
|
||||||
return useCallback((name: string, params: Record<string, string> = {}): string => {
|
|
||||||
const pattern = routeMap[name];
|
|
||||||
if (!pattern) {
|
|
||||||
console.warn(`[Hydro] Unknown route: ${name}`);
|
|
||||||
return '#';
|
|
||||||
}
|
|
||||||
return buildUrl(pattern, params);
|
|
||||||
}, [routeMap]);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './pages';
|
||||||
|
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Link } from '../components/link';
|
||||||
|
|
||||||
|
export default function Homepage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>homepage</div>
|
||||||
|
<Link to="problem_main">problem_main</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Layout({ children }: React.PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
<div>layout test</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { registerPage } from '../registry/page';
|
||||||
|
|
||||||
|
registerPage('homepage', () => import('./homepage'));
|
||||||
|
registerPage('problem_main', () => import('./problem_main'));
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Link } from '../components/link';
|
||||||
|
|
||||||
|
export default function ProblemMain() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>problem_main</div>
|
||||||
|
<Link to="homepage">homepage</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { SlotErrorBoundary } from './error-boundary';
|
export { SlotErrorBoundary } from './error-boundary';
|
||||||
export { after, before, intercept, patch, replace, wrap } from './interceptors';
|
export { after, before, intercept, patch, replace, wrap } from './interceptors';
|
||||||
|
export { registerPage } from './page';
|
||||||
export { installPlugin } from './plugin';
|
export { installPlugin } from './plugin';
|
||||||
export type { PluginAPI, PluginDefinition } from './plugin';
|
export type { PluginAPI, PluginDefinition } from './plugin';
|
||||||
export { defineSlot } from './slot';
|
export { defineSlot } from './slot';
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { lazy } from 'react';
|
||||||
|
import Layout from '../components/layout';
|
||||||
|
import { store } from './store';
|
||||||
|
|
||||||
|
interface PageModule<P = any> {
|
||||||
|
default: React.ComponentType<P>;
|
||||||
|
Layout?: React.ComponentType<React.PropsWithChildren>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageLoader<P = any> = () => Promise<PageModule<P>>;
|
||||||
|
|
||||||
|
export function registerPage<P = any>(name: string, loader: PageLoader<P>) {
|
||||||
|
store.setDefault(`page:${name}`, lazy(() =>
|
||||||
|
loader().then((mod) => {
|
||||||
|
const PageComp = mod.default;
|
||||||
|
const LayoutComp = mod.Layout || Layout;
|
||||||
|
return {
|
||||||
|
default(props: React.PropsWithChildren<P>) {
|
||||||
|
return (
|
||||||
|
<LayoutComp>
|
||||||
|
<PageComp {...props} />
|
||||||
|
</LayoutComp>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { after, before, intercept, patch, replace, wrap } from './interceptors';
|
import { after, before, intercept, patch, replace, wrap } from './interceptors';
|
||||||
|
import { registerPage } from './page';
|
||||||
|
|
||||||
export interface PluginAPI {
|
export interface PluginAPI {
|
||||||
intercept: typeof intercept;
|
intercept: typeof intercept;
|
||||||
@@ -7,10 +8,11 @@ export interface PluginAPI {
|
|||||||
patch: typeof patch;
|
patch: typeof patch;
|
||||||
replace: typeof replace;
|
replace: typeof replace;
|
||||||
wrap: typeof wrap;
|
wrap: typeof wrap;
|
||||||
|
registerPage: typeof registerPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPluginAPI(): PluginAPI {
|
export function createPluginAPI(): PluginAPI {
|
||||||
return { intercept, before, after, patch, replace, wrap };
|
return { intercept, before, after, patch, replace, wrap, registerPage };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDefinition {
|
export interface PluginDefinition {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ function createRegistryStore() {
|
|||||||
|
|
||||||
function setDefault(name: SlotName, comp: React.FC<any>) {
|
function setDefault(name: SlotName, comp: React.FC<any>) {
|
||||||
state.defaults[name] = comp;
|
state.defaults[name] = comp;
|
||||||
|
bumpVersion(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefault(name: SlotName): React.FC<any> | undefined {
|
function getDefault(name: SlotName): React.FC<any> | undefined {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type Scope = 'page' | 'component' | 'layout' | string;
|
type Scope = 'app' | 'page' | 'component' | string;
|
||||||
export type SlotName = `${Scope}:${string}`;
|
export type SlotName = `${Scope}:${string}`;
|
||||||
|
|
||||||
export type Interceptor<P = any> = (
|
export type Interceptor<P = any> = (
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import path from 'path';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
base: '/',
|
base: '/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
publicDir: 'pub',
|
publicDir: 'pub',
|
||||||
build: {
|
build: {
|
||||||
outDir: 'public',
|
outDir: 'public',
|
||||||
|
|||||||
Reference in new issue
Block a user