@bbki.ng/site 5.5.19 → 5.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/src/blog/app.tsx +18 -54
  4. package/src/blog/components/BaseLayout.tsx +55 -0
  5. package/src/blog/components/index.tsx +2 -4
  6. package/src/blog/context/global_loading_state_provider.tsx +3 -9
  7. package/src/blog/hooks/use_loading.ts +1 -1
  8. package/src/blog/hooks/use_plugin_entries.ts +48 -0
  9. package/src/blog/hooks/use_posts.ts +16 -9
  10. package/src/blog/index.tsx +1 -0
  11. package/src/blog/pages/cover/index.tsx +33 -24
  12. package/src/blog/pages/extensions/txt/article.tsx +6 -3
  13. package/src/blog/pages/extensions/txt/index.tsx +29 -25
  14. package/src/blog/swr.tsx +3 -2
  15. package/src/blog/types/path.ts +0 -7
  16. package/src/blog/utils/index.ts +0 -21
  17. package/src/core/context/createPluginCtx.tsx +12 -7
  18. package/src/core/hooks/index.ts +1 -1
  19. package/src/core/hooks/useMiddlewareTransData.ts +32 -46
  20. package/src/core/hooks/useSlotComp.ts +7 -3
  21. package/src/core/hooks/use_plugins.ts +16 -5
  22. package/src/core/pluginManager.ts +1 -1
  23. package/src/core/registry.ts +20 -0
  24. package/src/plugins/extra-entry/components/page.tsx +14 -0
  25. package/src/plugins/extra-entry/index.ts +32 -0
  26. package/src/plugins/manifest.ts +6 -0
  27. package/src/plugins/xwy/const/index.ts +1 -1
  28. package/src/types/plugin.ts +11 -1
  29. package/src/types/posts.ts +9 -0
  30. package/src/types/slots.ts +8 -2
  31. package/src/utils/index.tsx +52 -0
  32. package/src/blog/components/Auth.tsx +0 -13
  33. package/src/blog/components/DelayFadeIn/DelayFadeIn.tsx +0 -28
  34. package/src/blog/components/Spinner.tsx +0 -10
  35. package/src/blog/components/my_suspense.tsx +0 -11
  36. package/src/blog/components/share/share-btn.tsx +0 -28
  37. package/src/blog/components/share/share-icon.tsx +0 -19
  38. package/src/blog/hooks/use_font_loading.ts +0 -41
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.5.20
4
+
5
+ ### Patch Changes
6
+
7
+ - 6748bb3: fix null pointer exception
8
+
3
9
  ## 5.5.19
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.5.19",
3
+ "version": "5.5.20",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/blog/app.tsx CHANGED
@@ -1,75 +1,32 @@
1
- import React, { useContext } from 'react';
2
- import { Outlet, Route, Routes, useNavigate } from 'react-router-dom';
3
- import { Logo, Nav, NotFound, Page, Grid, ErrorBoundary, Container } from '@bbki.ng/ui';
1
+ import React from 'react';
2
+ import { Outlet, Route, Routes } from 'react-router-dom';
3
+ import { NotFound } from '@bbki.ng/ui';
4
4
 
5
- import { usePaths } from '@/hooks';
6
5
  import ArticlePage from '@/pages/extensions/txt/article';
7
6
  import Txt from '@/pages/extensions/txt';
8
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
9
7
  import { BotRedirect } from '@/pages/bot';
10
8
  import { BBContext } from '@/context/bbcontext';
11
9
  import { Slot } from '#/core/components/SlotComp';
12
10
  import { usePlugins } from '#/core/hooks/use_plugins';
13
11
  import { SWR } from '@/swr';
12
+ import type { PluginID } from '#/types/plugin';
14
13
 
15
14
  import { Cover, Streaming } from './pages';
15
+ import { usePluginEntries } from './hooks/use_plugin_entries';
16
+ import { BaseLayout } from './components/BaseLayout';
16
17
 
17
- const Layout = () => {
18
- const paths = usePaths();
19
- const { isLoading } = useContext(GlobalLoadingContext);
20
-
21
- const nav = useNavigate();
22
-
23
- const defaultLogo = (
24
- <Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
25
- );
26
-
27
- return (
28
- <Page
29
- nav={
30
- <Nav
31
- paths={paths}
32
- className="gradient-blur-cover select-none"
33
- loading={isLoading}
34
- customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
35
- style={{
36
- paddingTop: 'var(--safe-top)',
37
- transition: 'all .2s ease-in-out',
38
- }}
39
- />
40
- }
41
- main={
42
- <Grid
43
- leftAside={
44
- <div className="py-32 px-6">
45
- <Slot name="leftCol" data={paths} />
46
- </div>
47
- }
48
- rightAside={
49
- <div className="py-32 px-6">
50
- <Slot name="rightCol" data={paths} />
51
- </div>
52
- }
53
- >
54
- <Container className="py-32">
55
- <ErrorBoundary>
56
- <Outlet />
57
- </ErrorBoundary>
58
- </Container>
59
- </Grid>
60
- }
61
- />
62
- );
63
- };
18
+ const APP_PLUGIN_IDS: Array<PluginID> = ['sticker', 'xwy', 'extra-cd' /*'extra-entry'*/];
64
19
 
65
20
  export const App = () => {
66
- usePlugins(['sticker', 'xwy', 'extra-cd']);
21
+ usePlugins(APP_PLUGIN_IDS);
22
+
23
+ const pluginEntries = usePluginEntries();
67
24
 
68
25
  return (
69
26
  <SWR>
70
27
  <BBContext>
71
28
  <Routes>
72
- <Route path="/" element={<Layout />}>
29
+ <Route path="/" element={<BaseLayout />}>
73
30
  <Route index element={<Cover />} />
74
31
 
75
32
  <Route path="blog" element={<Outlet />}>
@@ -79,6 +36,13 @@ export const App = () => {
79
36
 
80
37
  <Route path="bot" element={<BotRedirect />} />
81
38
  <Route path="now" element={<Streaming />} />
39
+ {pluginEntries?.map(route => (
40
+ <Route
41
+ key={route.path}
42
+ path={route.path}
43
+ element={<Slot name="route" data={route} />}
44
+ />
45
+ ))}
82
46
  </Route>
83
47
  <Route path="*" element={<NotFound />} />
84
48
  </Routes>
@@ -0,0 +1,55 @@
1
+ import React, { useContext } from 'react';
2
+ import { useNavigate, Outlet } from 'react-router-dom';
3
+ import { Logo, Nav, Page, Grid, ErrorBoundary, Container } from '@bbki.ng/ui';
4
+
5
+ import { Slot } from '#/core/components/SlotComp';
6
+ import { usePaths } from '@/hooks';
7
+ import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
8
+
9
+ export const BaseLayout = () => {
10
+ const paths = usePaths();
11
+ const { isLoading } = useContext(GlobalLoadingContext);
12
+
13
+ const nav = useNavigate();
14
+
15
+ const defaultLogo = (
16
+ <Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
17
+ );
18
+
19
+ return (
20
+ <Page
21
+ nav={
22
+ <Nav
23
+ paths={paths}
24
+ className="gradient-blur-cover select-none"
25
+ loading={isLoading}
26
+ customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
27
+ style={{
28
+ paddingTop: 'var(--safe-top)',
29
+ transition: 'all .2s ease-in-out',
30
+ }}
31
+ />
32
+ }
33
+ main={
34
+ <Grid
35
+ leftAside={
36
+ <div className="py-32 px-6">
37
+ <Slot name="leftCol" data={paths} />
38
+ </div>
39
+ }
40
+ rightAside={
41
+ <div className="py-32 px-6">
42
+ <Slot name="rightCol" data={paths} />
43
+ </div>
44
+ }
45
+ >
46
+ <Container className="py-32">
47
+ <ErrorBoundary>
48
+ <Outlet />
49
+ </ErrorBoundary>
50
+ </Container>
51
+ </Grid>
52
+ }
53
+ />
54
+ );
55
+ };
@@ -1,9 +1,7 @@
1
1
  import React from 'react';
2
- import { LinkList } from '@bbki.ng/ui';
2
+ import { LinkList, LinkListProps } from '@bbki.ng/ui';
3
3
 
4
- export { MySuspense } from './my_suspense';
5
-
6
- export const CenterLinkList = (props: any) => {
4
+ export const CenterLinkList = (props: LinkListProps) => {
7
5
  return (
8
6
  <div className="flex justify-center relative h-full">
9
7
  <LinkList {...props} />
@@ -1,19 +1,15 @@
1
1
  import React, { createContext, ReactNode, useState, useCallback, useMemo } from 'react';
2
2
 
3
- import { useFontLoading } from '@/hooks/use_font_loading';
4
-
5
3
  type LoadingStates = Map<string, boolean>;
6
4
 
7
5
  type LoadingContext = {
8
6
  isLoading: boolean;
9
- isFontLoading: boolean;
10
7
  register: (id: string) => void;
11
8
  setLoading: (id: string, loading: boolean) => void;
12
9
  unregister: (id: string) => void;
13
10
  };
14
11
 
15
12
  export const GlobalLoadingContext = createContext<LoadingContext>({
16
- isFontLoading: false,
17
13
  isLoading: false,
18
14
  register: () => {},
19
15
  setLoading: () => {},
@@ -22,7 +18,6 @@ export const GlobalLoadingContext = createContext<LoadingContext>({
22
18
 
23
19
  export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
24
20
  const [loadingStates, setLoadingStates] = useState<LoadingStates>(new Map());
25
- const isFontLoading = useFontLoading();
26
21
 
27
22
  const register = useCallback((id: string) => {
28
23
  setLoadingStates(prev => {
@@ -49,18 +44,17 @@ export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
49
44
  }, []);
50
45
 
51
46
  const isLoading = useMemo(() => {
52
- return Array.from(loadingStates.values()).some(Boolean) || isFontLoading;
53
- }, [loadingStates, isFontLoading]);
47
+ return Array.from(loadingStates.values()).some(Boolean);
48
+ }, [loadingStates]);
54
49
 
55
50
  const contextValue = useMemo(
56
51
  () => ({
57
52
  isLoading,
58
- isFontLoading,
59
53
  register,
60
54
  setLoading,
61
55
  unregister,
62
56
  }),
63
- [isLoading, isFontLoading, register, setLoading, unregister]
57
+ [isLoading, register, setLoading, unregister]
64
58
  );
65
59
 
66
60
  return (
@@ -20,7 +20,7 @@ export function useGlobalLoading(id: string | undefined, loading: boolean | unde
20
20
  return () => {
21
21
  unregister(id);
22
22
  };
23
- }, [id, loading]);
23
+ }, [id, loading, register, setLoading, unregister]);
24
24
 
25
25
  return isLoading;
26
26
  }
@@ -0,0 +1,48 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { type PathRouteProps } from 'react-router-dom';
3
+
4
+ import { useMiddlewareRunner } from '#/core/hooks';
5
+
6
+ const protectedRoutesSet = new Set<string>([
7
+ '/bot',
8
+ '/now',
9
+ '/',
10
+ '/blog',
11
+ '/blog/:title',
12
+ 'default',
13
+ ]);
14
+
15
+ type PluginRoute = Omit<PathRouteProps, 'element'>;
16
+
17
+ export const usePluginEntries = () => {
18
+ const [fullRoutes, setFullRoutes] = useState<Array<PluginRoute>>([]);
19
+ const runRef = useRef<(input: Array<PluginRoute>) => Promise<Array<PluginRoute>>>(() =>
20
+ Promise.resolve([])
21
+ );
22
+
23
+ const onMiddlewareChange = useCallback(() => {
24
+ runRef.current([]).then(setFullRoutes);
25
+ }, []);
26
+
27
+ const { run } = useMiddlewareRunner<Array<PluginRoute>>({
28
+ hookPoint: 'extendedRoutes',
29
+ onMiddlewareChange,
30
+ });
31
+
32
+ runRef.current = run;
33
+
34
+ useEffect(() => {
35
+ run([]).then(setFullRoutes);
36
+ }, [run]);
37
+
38
+ const safeRoutes = fullRoutes?.filter(route => {
39
+ if ('path' in route) {
40
+ return !protectedRoutesSet.has(route.path);
41
+ }
42
+ return false;
43
+ }) as Array<PluginRoute>;
44
+
45
+ console.log('safe plugin routes:', safeRoutes);
46
+
47
+ return safeRoutes ?? [];
48
+ };
@@ -1,10 +1,11 @@
1
1
  import useSWR from 'swr';
2
- import { useMemo } from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
3
 
4
4
  import { useGlobalLoading } from '@/hooks';
5
5
  import { baseFetcher } from '@/utils';
6
6
  import { API_ENDPOINT } from '@/constants/routes';
7
- import { useMiddlewareTransData } from '#/core/hooks';
7
+ import { IPost } from '#/types/posts';
8
+ import { useMiddlewareRunner } from '#/core/hooks/useMiddlewareTransData';
8
9
 
9
10
  const isProd = true;
10
11
  const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
@@ -28,34 +29,40 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
28
29
  const baseTitleList: TitleListItem[] = useMemo(() => {
29
30
  if (!data || swrError) return [];
30
31
 
31
- return data.map((p: any) => ({
32
+ return data.map((p: IPost) => ({
32
33
  name: p.title,
33
34
  to: p.title,
34
35
  children: p.title,
35
36
  }));
36
37
  }, [data, swrError]);
37
38
 
39
+ const [fullTitleList, setFullTitleList] = useState<TitleListItem[]>(baseTitleList);
40
+
38
41
  // Use middleware hook to transform title list
39
42
  const {
40
- data: titleList,
41
43
  loading: isTransforming,
42
44
  error: transformError,
43
- } = useMiddlewareTransData({
45
+ run,
46
+ } = useMiddlewareRunner<TitleListItem[]>({
44
47
  hookPoint: 'transformTitleList',
45
- baseData: baseTitleList,
46
- immediate: baseTitleList.length > 0,
47
48
  });
48
49
 
50
+ useEffect(() => {
51
+ if (baseTitleList.length > 0) {
52
+ run(baseTitleList).then(setFullTitleList);
53
+ }
54
+ }, [baseTitleList, run]);
55
+
49
56
  const gLoading = useGlobalLoading('posts', isDataLoading || isTransforming);
50
57
 
51
58
  const posts =
52
59
  isDataLoading || name === '' || swrError || !data
53
60
  ? data
54
- : data.find((p: any) => p.title === name);
61
+ : data.find((p: IPost) => p.title === name);
55
62
 
56
63
  return {
57
64
  posts,
58
- titleList: titleList ?? [],
65
+ titleList: fullTitleList ?? [],
59
66
  isError: swrError || transformError,
60
67
  isLoading: gLoading,
61
68
  };
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import { BrowserRouter as Router } from 'react-router-dom';
4
+
4
5
  import App from './app';
5
6
  import './main.css';
6
7
 
@@ -1,31 +1,40 @@
1
- import React, { useContext } from 'react';
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { LinkProps } from '@bbki.ng/ui';
3
+
2
4
  import { CenterLinkList } from '@/components';
3
- import { GlobalRoutesContext } from '@/context/global_routes_provider';
5
+ import { useMiddlewareRunner } from '#/core/hooks';
6
+
7
+ export const Cover = (_: { className?: string }) => {
8
+ const baseEntries: Array<LinkProps> = useMemo(
9
+ () => [
10
+ {
11
+ to: '/blog',
12
+ children: 'cd ./blog',
13
+ },
14
+ {
15
+ to: '/now',
16
+ children: 'cd ./now',
17
+ },
18
+ ],
19
+ []
20
+ );
21
+
22
+ const [entries, setEntries] = React.useState<Array<LinkProps>>(baseEntries);
23
+
24
+ const { run } = useMiddlewareRunner<Array<LinkProps>>({
25
+ hookPoint: 'transformCoverEntry',
26
+ onMiddlewareChange: () => {
27
+ run(baseEntries).then(setEntries);
28
+ },
29
+ });
30
+
31
+ useEffect(() => {
32
+ run(baseEntries).then(setEntries);
33
+ }, [baseEntries, run]);
4
34
 
5
- export const Cover = (props: { className?: string }) => {
6
- const globalRouteCtx = useContext(GlobalRoutesContext);
7
- const routes = globalRouteCtx.globalRoutes;
8
- const pluginEntry = routes.length > 0 ? [{ to: '/plugins', name: 'cd ./plugins' }] : [];
9
35
  return (
10
36
  <>
11
- <CenterLinkList
12
- className="select-none"
13
- links={[
14
- {
15
- to: '/blog',
16
- name: 'cd ./blog',
17
- children: 'cd ./blog',
18
- },
19
- {
20
- to: '/now',
21
- name: 'cd ./now',
22
- children: 'cd ./now',
23
- },
24
- ...pluginEntry,
25
- ]}
26
- title=""
27
- // footer={<Version className="" />}
28
- />
37
+ <CenterLinkList className="select-none" links={entries} />
29
38
  </>
30
39
  );
31
40
  };
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
2
  import { NotFound } from '@bbki.ng/ui';
3
3
  import { useParams } from 'react-router-dom';
4
+
4
5
  import { usePosts } from '@/hooks/use_posts';
5
6
  import { ArticlePage } from '@/components/article';
6
7
  import { useBlogScrollReset } from '@/hooks/use_blog_scroll_pos_restoration';
7
8
 
8
- export default () => {
9
+ function TxtArticle() {
9
10
  const { title } = useParams();
10
11
  const { posts, isError, isLoading } = usePosts(title);
11
12
 
@@ -19,7 +20,7 @@ export default () => {
19
20
  return <NotFound />;
20
21
  }
21
22
 
22
- if (isLoading) {
23
+ if (isLoading || !posts) {
23
24
  return null;
24
25
  }
25
26
 
@@ -30,4 +31,6 @@ export default () => {
30
31
  <div dangerouslySetInnerHTML={{ __html: posts.content }} />
31
32
  </ArticlePage>
32
33
  );
33
- };
34
+ }
35
+
36
+ export default TxtArticle;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { LinkProps, Button } from '@bbki.ng/ui';
3
+
3
4
  import { usePosts } from '@/hooks/use_posts';
4
5
  import { CenterLinkList } from '@/components';
5
6
  import { useBlogScroll, useBlogScrollRestoration } from '@/hooks/use_blog_scroll_pos_restoration';
@@ -9,43 +10,46 @@ type TxtProps = {
9
10
  articleList?: LinkProps[];
10
11
  };
11
12
 
12
- export default (props: TxtProps) => {
13
- const { titleList, isLoading, isError } = usePosts();
13
+ const Txt = (props: TxtProps) => {
14
+ const { titleList, isError } = usePosts();
14
15
 
15
16
  useBlogScrollRestoration();
16
17
 
17
18
  const { gotoTop } = useBlogScroll();
18
19
 
19
- if (isLoading) {
20
- return null;
21
- }
22
-
23
20
  if (isError) {
24
- return <CenterLinkList links={props.articleList} />;
21
+ return <CenterLinkList links={props.articleList ?? titleList} />;
25
22
  }
26
23
 
24
+ const links = props.articleList ?? titleList;
25
+
27
26
  return (
28
27
  <CenterLinkList
29
- links={props.articleList || titleList}
30
- loading={isLoading}
28
+ links={links}
31
29
  footer={
32
- <Button onClick={gotoTop} className="mt-32">
33
- <svg
34
- data-testid="geist-icon"
35
- height="16"
36
- stroke-linejoin="round"
37
- viewBox="0 0 16 16"
38
- width="16"
39
- >
40
- <path
41
- fill-rule="evenodd"
42
- clip-rule="evenodd"
43
- d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
44
- fill="currentColor"
45
- ></path>
46
- </svg>
47
- </Button>
30
+ links.length > 0 ? (
31
+ <Button onClick={gotoTop} className="mt-32">
32
+ <svg
33
+ data-testid="geist-icon"
34
+ height="16"
35
+ strokeLinejoin="round"
36
+ viewBox="0 0 16 16"
37
+ width="16"
38
+ >
39
+ <path
40
+ fillRule="evenodd"
41
+ clipRule="evenodd"
42
+ d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
43
+ fill="currentColor"
44
+ ></path>
45
+ </svg>
46
+ </Button>
47
+ ) : null
48
48
  }
49
49
  />
50
50
  );
51
51
  };
52
+
53
+ Txt.displayName = 'Txt';
54
+
55
+ export default Txt;
package/src/blog/swr.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import React from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  import { SWRConfig } from 'swr';
3
+
3
4
  import { cfApiFetcher } from '@/utils';
4
5
 
5
- export const SWR = (props: { children: any }) => {
6
+ export const SWR = (props: { children: ReactNode }) => {
6
7
  return (
7
8
  <SWRConfig
8
9
  value={{
@@ -1,11 +1,4 @@
1
- import { FunctionComponent } from 'react';
2
-
3
1
  export interface pathObj {
4
2
  path?: string;
5
3
  name: string;
6
4
  }
7
-
8
- export interface compPathObj extends pathObj {
9
- component: FunctionComponent<any>;
10
- componentProps?: any;
11
- }
@@ -32,24 +32,3 @@ export const withBBApi =
32
32
  fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
33
33
 
34
34
  export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
35
-
36
- export const changeFont = (type: FontType) => {
37
- const rootDiv = document.getElementById('root');
38
- if (rootDiv == null) {
39
- return;
40
- }
41
-
42
- if (rootDiv.classList.contains(type)) {
43
- return;
44
- }
45
-
46
- // remove all font type class
47
- for (const fontType in FontType) {
48
- rootDiv.classList.remove(fontType);
49
- }
50
-
51
- rootDiv.classList.add(type);
52
-
53
- // save font type to local storage
54
- localStorage.setItem('font', type);
55
- };
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useContext, ComponentType } from 'react';
2
- import { IHostContext } from '#/types/hostApi';
2
+
3
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
3
4
 
4
5
  /**
5
6
  * 创建插件专属的 Context 工具集
@@ -33,15 +34,19 @@ export function createPluginCtx<T>() {
33
34
  * const Enhanced = withCtx(picker, BaseComp)(ctx);
34
35
  * ctx.api.registerSlot('slotName', Enhanced, pluginId);
35
36
  */
36
- const withCtx = <P extends { data?: any }>(
37
+ const withCtx = <P extends IComPropsRegisteredToSlot>(
37
38
  ctx: T,
38
39
  Component: React.ComponentType<P>
39
40
  ): ComponentType<P> => {
40
- return (props: P) => (
41
- <Ctx.Provider value={ctx}>
42
- <Component {...props} />
43
- </Ctx.Provider>
44
- );
41
+ const WrappedComponent: React.FC<P> = props => {
42
+ return (
43
+ <Ctx.Provider value={ctx}>
44
+ <Component {...props} />
45
+ </Ctx.Provider>
46
+ );
47
+ };
48
+
49
+ return WrappedComponent;
45
50
  };
46
51
 
47
52
  return {
@@ -1,5 +1,5 @@
1
1
  export { useSlotComp } from './useSlotComp';
2
- export { useMiddlewareTransData } from './useMiddlewareTransData';
2
+ export { useMiddlewareRunner } from './useMiddlewareTransData';
3
3
  export type {
4
4
  UseMiddlewareTransDataOptions,
5
5
  UseMiddlewareTransDataResult,
@@ -1,68 +1,54 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
 
3
3
  import { registry } from '#/core/registry';
4
4
  import type { HookPoint } from '#/types/slots';
5
5
 
6
- export interface UseMiddlewareTransDataOptions<T> {
6
+ export interface UseMiddlewareTransDataOptions {
7
7
  hookPoint: HookPoint;
8
- baseData: T;
9
- immediate?: boolean;
8
+ onMiddlewareChange?: () => void; // 通知外部,由外部决定是否 run
10
9
  }
11
10
 
12
11
  export interface UseMiddlewareTransDataResult<T> {
13
- data: T | undefined;
14
12
  loading: boolean;
15
13
  error: Error | null;
16
- run: () => Promise<T>;
14
+ run: (inputData: T) => Promise<T>;
17
15
  }
18
16
 
19
- export function useMiddlewareTransData<T>({
17
+ export function useMiddlewareRunner<T>({
20
18
  hookPoint,
21
- baseData,
22
- immediate = true,
23
- }: UseMiddlewareTransDataOptions<T>): UseMiddlewareTransDataResult<T> {
24
- const [data, setData] = useState<T | undefined>(undefined);
19
+ onMiddlewareChange,
20
+ }: UseMiddlewareTransDataOptions): UseMiddlewareTransDataResult<T> {
25
21
  const [loading, setLoading] = useState(false);
26
22
  const [error, setError] = useState<Error | null>(null);
27
23
 
28
- const immediateRef = useRef(immediate);
29
- immediateRef.current = immediate;
30
-
31
- const run = useCallback(async (): Promise<T> => {
32
- setLoading(true);
33
- setError(null);
34
-
35
- try {
36
- const result = await registry.runMiddleware(hookPoint, baseData);
37
- setData(result);
38
- return result;
39
- } catch (err) {
40
- const error = err instanceof Error ? err : new Error(String(err));
41
- setError(error);
42
- throw error;
43
- } finally {
44
- setLoading(false);
45
- }
46
- }, [hookPoint, baseData]);
47
-
24
+ const run = useCallback(
25
+ async (inputData: T) => {
26
+ setLoading(true);
27
+ setError(null);
28
+ try {
29
+ const result = await registry.runMiddleware(hookPoint, inputData);
30
+ return result;
31
+ } catch (err) {
32
+ const error = err instanceof Error ? err : new Error(String(err));
33
+ setError(error);
34
+ throw error;
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ },
39
+ [hookPoint]
40
+ );
41
+
42
+ // 仅通知,不执行
48
43
  useEffect(() => {
49
- if (!immediateRef.current) return;
50
-
51
- let cancelled = false;
52
-
53
- run()
54
- .then(result => {
55
- if (cancelled) return;
56
- setData(result);
57
- })
58
- .catch(() => {
59
- // 错误已在 run 中设置到 state
60
- });
44
+ const unsubscribe = registry.subscribeMiddleware(hookPoint, () => {
45
+ onMiddlewareChange?.();
46
+ });
61
47
 
62
48
  return () => {
63
- cancelled = true;
49
+ unsubscribe();
64
50
  };
65
- }, [hookPoint, baseData, run]);
51
+ }, [hookPoint, onMiddlewareChange]);
66
52
 
67
- return { data, loading, error, run };
53
+ return { loading, error, run };
68
54
  }
@@ -1,13 +1,17 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
 
3
- import { SlotName } from '#/types/slots';
3
+ import { IComPropsRegisteredToSlot, SlotName } from '#/types/slots';
4
4
 
5
5
  import { registry } from '../registry';
6
6
 
7
7
  export const useSlotComp = (slotName: SlotName) => {
8
- const [components, setComponents] = useState<React.ComponentType<any>[]>([]);
8
+ const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
9
+ () => registry.getComponents(slotName)
10
+ );
9
11
 
10
12
  useEffect(() => {
13
+ setComponents(registry.getComponents(slotName));
14
+
11
15
  const unsubscribe = registry.subscribe(() => {
12
16
  const comps = registry.getComponents(slotName);
13
17
  setComponents(comps);
@@ -16,7 +20,7 @@ export const useSlotComp = (slotName: SlotName) => {
16
20
  return () => {
17
21
  unsubscribe();
18
22
  };
19
- }, []);
23
+ }, [slotName]);
20
24
 
21
25
  return components;
22
26
  };
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
 
3
3
  import { pluginManager } from '#/core/pluginManager';
4
4
  import { PluginID } from '#/types/plugin';
@@ -13,11 +13,20 @@ import { PluginID } from '#/types/plugin';
13
13
  * usePlugins(['sticker', 'fontstyler']);
14
14
  */
15
15
  export const usePlugins = (pluginIds: Array<PluginID>) => {
16
+ const [done, setDone] = useState(false);
17
+
18
+ const loadAllPlugins = useCallback(async () => {
19
+ try {
20
+ await Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)));
21
+ setDone(true);
22
+ } catch (error) {
23
+ console.error('Error loading plugins:', error);
24
+ }
25
+ }, [pluginIds]);
26
+
16
27
  useEffect(() => {
17
28
  // 加载指定的插件
18
- pluginIds.forEach(id => {
19
- pluginManager.loadPlugin(id);
20
- });
29
+ loadAllPlugins();
21
30
 
22
31
  return () => {
23
32
  // 卸载所有指定的插件
@@ -25,5 +34,7 @@ export const usePlugins = (pluginIds: Array<PluginID>) => {
25
34
  pluginManager.disablePlugin(id);
26
35
  });
27
36
  };
28
- }, [pluginIds]);
37
+ }, [loadAllPlugins, pluginIds]);
38
+
39
+ return done;
29
40
  };
@@ -70,7 +70,7 @@ class PluginManager {
70
70
  const ctx = this.createHostContext();
71
71
 
72
72
  if (p.onInstall) {
73
- p.onInstall(ctx);
73
+ await p.onInstall(ctx);
74
74
  }
75
75
 
76
76
  this.activePlugins.set(pluginId, p);
@@ -20,6 +20,7 @@ export class Registry {
20
20
  private slots = new Map<SlotName, ISlotEntry[]>();
21
21
 
22
22
  private listeners = new Set<() => void>();
23
+ private middlewareListenerMap = new Map<HookPoint, Set<() => void>>();
23
24
 
24
25
  private middlewares = new Map<HookPoint, IMiddlewareEntry<unknown>[]>();
25
26
 
@@ -27,6 +28,22 @@ export class Registry {
27
28
  this.listeners.forEach(listener => listener());
28
29
  }
29
30
 
31
+ private broadcastMiddlewareChange(point: HookPoint) {
32
+ const listeners = this.middlewareListenerMap.get(point);
33
+ if (listeners) {
34
+ listeners.forEach(listener => listener());
35
+ }
36
+ }
37
+
38
+ subscribeMiddleware(hookPoint: HookPoint, listener: () => void) {
39
+ if (!this.middlewareListenerMap.has(hookPoint)) {
40
+ this.middlewareListenerMap.set(hookPoint, new Set());
41
+ }
42
+ const listeners = this.middlewareListenerMap.get(hookPoint)!;
43
+ listeners.add(listener);
44
+ return () => listeners.delete(listener);
45
+ }
46
+
30
47
  subscribe(listener: () => void) {
31
48
  this.listeners.add(listener);
32
49
  return () => this.listeners.delete(listener);
@@ -91,7 +108,10 @@ export class Registry {
91
108
  const newList = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
92
109
  (a, b) => (b.weight || 0) - (a.weight || 0)
93
110
  );
111
+
94
112
  this.middlewares.set(hookPoint, newList);
113
+
114
+ this.broadcastMiddlewareChange(hookPoint);
95
115
  }
96
116
 
97
117
  getComponents(slotName: SlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { PathRouteProps } from 'react-router-dom';
3
+
4
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
5
+
6
+ export const ExtendedRoutesPage = ({ data }: IComPropsRegisteredToSlot) => {
7
+ const route = data as Omit<PathRouteProps, 'element'>;
8
+
9
+ if (route.path === '/hello') {
10
+ return <div className="p-4 text-center">Hello from Extra Entry Plugin!</div>;
11
+ }
12
+
13
+ return <></>;
14
+ };
@@ -0,0 +1,32 @@
1
+ import { IHostContext } from '#/types/hostApi';
2
+ import { IPlugin } from '#/types/plugin';
3
+ import { buildEntryCreator } from '#/utils';
4
+
5
+ import { ExtendedRoutesPage } from './components/page';
6
+
7
+ export class ExtraEntryPlugin implements IPlugin {
8
+ id: string = 'extra-entry';
9
+ name: string = 'Extra Entry';
10
+ description?: string | undefined;
11
+ version: string = '1.0.0';
12
+ author?: string | undefined = 'bbki.ng';
13
+
14
+ onInstall?: ((ctx: IHostContext) => void | Promise<void>) | undefined = async (
15
+ ctx: IHostContext
16
+ ) => {
17
+ const entryCreator = buildEntryCreator(ctx);
18
+ entryCreator(
19
+ {
20
+ pageComponent: ExtendedRoutesPage,
21
+ path: '/hello',
22
+ label: 'cd ./hello',
23
+ },
24
+ this.id
25
+ );
26
+ };
27
+
28
+ onDisable?: (() => Promise<void> | void) | undefined;
29
+ onDestroy?: (() => void) | undefined;
30
+ }
31
+
32
+ export default new ExtraEntryPlugin();
@@ -12,6 +12,12 @@ export const PLUGIN_MANIFEST = [
12
12
  description:
13
13
  'Provides additional change directory functionalities for enhanced user experience.',
14
14
  },
15
+ {
16
+ name: 'extra-entry',
17
+ id: 'extra-entry',
18
+ version: '0.1.0',
19
+ description: 'Provides extra entries for cover and extended routes.',
20
+ },
15
21
  {
16
22
  name: 'xwy',
17
23
  id: 'xwy',
@@ -9,7 +9,7 @@ export const FontRules: Array<FontRule> = [
9
9
  src: '/fonts/xwy.woff2',
10
10
  format: 'woff2',
11
11
  },
12
- extraCls: 'text-2xl',
12
+ extraCls: 'text-2xl w-[113px]',
13
13
  variant: 'special',
14
14
  },
15
15
  ];
@@ -1,3 +1,7 @@
1
+ import type React from 'react';
2
+
3
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
4
+
1
5
  import { IHostContext } from './hostApi';
2
6
 
3
7
  export interface IPlugin {
@@ -12,4 +16,10 @@ export interface IPlugin {
12
16
  onDestroy?: () => void;
13
17
  }
14
18
 
15
- export type PluginID = 'sticker' | 'xwy' | 'extra-cd';
19
+ export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry';
20
+
21
+ export interface IPluginEntry {
22
+ path: string;
23
+ label?: string;
24
+ pageComponent: React.ComponentType<IComPropsRegisteredToSlot>;
25
+ }
@@ -5,3 +5,12 @@ export interface TitleListItem {
5
5
  className?: string;
6
6
  variant?: 'default' | 'special';
7
7
  }
8
+
9
+ export interface IPost {
10
+ id: string;
11
+ title: string;
12
+ content: string;
13
+ author: string;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
@@ -1,5 +1,11 @@
1
- export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow' | 'logo';
2
- export type HookPoint = 'filterPosts' | 'transformPostContent' | 'transformTitleList';
1
+ export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow' | 'logo' | 'route';
2
+
3
+ export type HookPoint =
4
+ | 'filterPosts'
5
+ | 'transformPostContent'
6
+ | 'transformTitleList'
7
+ | 'extendedRoutes'
8
+ | 'transformCoverEntry';
3
9
 
4
10
  export interface IComPropsRegisteredToSlot {
5
11
  data: unknown;
@@ -0,0 +1,52 @@
1
+ import { LinkProps, PathRouteProps } from 'react-router-dom';
2
+ import React from 'react';
3
+
4
+ import { IHostContext } from '#/types/hostApi';
5
+ import { IPluginEntry } from '#/types/plugin';
6
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
7
+
8
+ export const buildEntryCreator = (ctx: IHostContext) => (entry: IPluginEntry, id: string) => {
9
+ ctx.api.registerMiddleware(
10
+ 'extendedRoutes',
11
+ (routes: Array<Omit<PathRouteProps, 'element'>>) => {
12
+ return [
13
+ ...routes,
14
+ {
15
+ path: entry.path,
16
+ },
17
+ ];
18
+ },
19
+ id,
20
+ 10
21
+ );
22
+
23
+ ctx.api.registerSlot(
24
+ 'route',
25
+ (props: IComPropsRegisteredToSlot) => {
26
+ const route = props.data as Omit<PathRouteProps, 'element'>;
27
+ if (route.path === entry.path) {
28
+ const Com = entry.pageComponent;
29
+ return <Com data={props.data} />;
30
+ }
31
+ return null;
32
+ },
33
+ id
34
+ );
35
+
36
+ if (!entry.label) return;
37
+
38
+ ctx.api.registerMiddleware(
39
+ 'transformCoverEntry',
40
+ (entries: Array<LinkProps>) => {
41
+ return [
42
+ ...entries,
43
+ {
44
+ to: entry.path,
45
+ children: entry.label,
46
+ },
47
+ ];
48
+ },
49
+ id,
50
+ 10
51
+ );
52
+ };
@@ -1,13 +0,0 @@
1
- import { Role, useRole } from '@/hooks/use_role';
2
- import React from 'react';
3
- import { Navigate } from 'react-router-dom';
4
-
5
- export const Auth = (props: { children: any; shouldRedirect?: boolean; role?: Role[] }) => {
6
- const myRole = useRole();
7
-
8
- if (props.role && !props.role.includes(myRole)) {
9
- return props.shouldRedirect ? <Navigate to={'/login'} /> : null;
10
- }
11
-
12
- return props.children;
13
- };
@@ -1,28 +0,0 @@
1
- import React, { useEffect } from 'react';
2
- import cls from 'classnames';
3
-
4
- export type DelayFadeInProps = {
5
- children: any;
6
- delay: number;
7
- };
8
-
9
- export const DelayFadeIn = (props: DelayFadeInProps) => {
10
- const [show, setShow] = React.useState(false);
11
-
12
- useEffect(() => {
13
- const id = setTimeout(() => {
14
- setShow(true);
15
- }, props.delay);
16
-
17
- return () => {
18
- clearTimeout(id);
19
- };
20
- }, []);
21
-
22
- const className = cls('transition-opacity', {
23
- 'opacity-100': show,
24
- 'opacity-0': !show,
25
- });
26
-
27
- return <div className={className}>{props.children}</div>;
28
- };
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import { useGlobalLoading } from '@/hooks';
3
-
4
- export const Spinner = (props: { disableDotIndicator?: boolean }) => {
5
- const { disableDotIndicator } = props;
6
-
7
- useGlobalLoading('spinner', !disableDotIndicator);
8
-
9
- return <div className="h-full w-full grid place-items-center"></div>;
10
- };
@@ -1,11 +0,0 @@
1
- import React, { ReactElement, ReactNode, Suspense } from 'react';
2
- import { ErrorBoundary } from '@bbki.ng/ui';
3
- import { Spinner } from './Spinner';
4
-
5
- export const MySuspense = (props: { children: ReactNode; fallback?: ReactElement }) => {
6
- return (
7
- <ErrorBoundary>
8
- <Suspense fallback={<Spinner />}>{props.children}</Suspense>
9
- </ErrorBoundary>
10
- );
11
- };
@@ -1,28 +0,0 @@
1
- import React from 'react';
2
- import { Button } from '@bbki.ng/ui';
3
- import { ShareIcon } from './share-icon';
4
-
5
- export const ShareBtn = ({ shareInfo }: { shareInfo: ShareData }) => {
6
- const handleShare = async () => {
7
- try {
8
- await navigator.share(shareInfo);
9
- } catch (error) {
10
- const isAbortError = (error as Error).name === 'AbortError';
11
- if (isAbortError) {
12
- return;
13
- }
14
- console.error('Share failed:', (error as Error).message);
15
- }
16
- };
17
-
18
- return (
19
- <Button
20
- size="sm"
21
- className="text-gray-400 hover:text-gray-600 transition-colors ease-in duration-200"
22
- variant="ghost"
23
- onClick={handleShare}
24
- >
25
- <ShareIcon />
26
- </Button>
27
- );
28
- };
@@ -1,19 +0,0 @@
1
- import React from 'react';
2
-
3
- export const ShareIcon = () => (
4
- <svg
5
- data-testid="geist-icon"
6
- height="12"
7
- stroke-linejoin="round"
8
- // style="color:currentColor"
9
- viewBox="0 0 16 16"
10
- width="12"
11
- >
12
- <path
13
- fill-rule="evenodd"
14
- clip-rule="evenodd"
15
- d="M7.29289 1.39644C7.68342 1.00592 8.31658 1.00592 8.70711 1.39644L11.7803 4.46966L12.3107 4.99999L11.25 6.06065L10.7197 5.53032L8.75 3.56065V10.25V11H7.25V10.25V3.56065L5.28033 5.53032L4.75 6.06065L3.68934 4.99999L4.21967 4.46966L7.29289 1.39644ZM13.5 9.24999V13.5H2.5V9.24999V8.49999H1V9.24999V14C1 14.5523 1.44771 15 2 15H14C14.5523 15 15 14.5523 15 14V9.24999V8.49999H13.5V9.24999Z"
16
- fill="currentColor"
17
- ></path>
18
- </svg>
19
- );
@@ -1,41 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { changeFont } from '@/utils';
3
- import { FontType } from '@/types/font';
4
-
5
- export const useFontLoading = () => {
6
- const [isFontLoading, setIsFontLoading] = useState(false);
7
-
8
- const handleFontLoading = () => {
9
- setIsFontLoading(true);
10
- document.fonts.ready.then(() => {
11
- handleFontLoadingDone();
12
- });
13
- };
14
-
15
- const handleFontLoadingDone = () => {
16
- setIsFontLoading(false);
17
- // setTimeout(() => {
18
- // }, 500);
19
- };
20
-
21
- const handleFontLoadingError = () => {
22
- setIsFontLoading(false);
23
- changeFont(FontType.Mono);
24
- };
25
-
26
- useEffect(() => {
27
- document.fonts.onloadingerror = handleFontLoadingError;
28
- document.fonts.onloading = handleFontLoading;
29
-
30
- document.fonts.ready.then(() => {
31
- handleFontLoadingDone();
32
- });
33
-
34
- return () => {
35
- document.fonts.onloadingerror = null;
36
- document.fonts.onloading = null;
37
- };
38
- }, []);
39
-
40
- return isFontLoading;
41
- };