@bbki.ng/site 5.6.7 → 5.8.0
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.
- package/CHANGELOG.md +12 -0
- package/index.html +1 -12
- package/package.json +1 -1
- package/src/app/app.tsx +46 -0
- package/src/{blog → app}/components/BaseLayout.tsx +2 -2
- package/src/{blog/pages → app/components}/cover/index.tsx +12 -13
- package/src/app/constants/index.ts +1 -0
- package/src/{blog → app}/context/bbcontext.tsx +2 -2
- package/src/{blog/hooks/use_loading.ts → app/hooks/use_global_loading.ts} +1 -1
- package/src/{blog → app}/hooks/use_paths.ts +14 -12
- package/src/{blog → app}/hooks/use_plugin_entries.ts +1 -3
- package/src/{blog → app}/index.tsx +2 -2
- package/src/{blog → app}/main.css +3 -3
- package/src/{blog → app}/swr.tsx +1 -1
- package/src/{blog → app}/utils/index.ts +1 -1
- package/src/core/components/SlotComp.tsx +2 -3
- package/src/core/hooks/useMiddlewareTransData.ts +12 -7
- package/src/core/hooks/useSlotComp.ts +8 -7
- package/src/core/hooks/use_plugins.ts +28 -15
- package/src/core/{pluginManager.ts → plugin-system/pluginManager.ts} +26 -43
- package/src/core/{pluginManifestService.ts → plugin-system/pluginManifestService.ts} +4 -4
- package/src/core/plugin-system/pluginStore.ts +190 -0
- package/src/core/{registry.ts → plugin-system/registry.ts} +1 -3
- package/src/core/plugin-system/services/coreService.ts +56 -0
- package/src/core/plugin-system/services/systemUIService.ts +159 -0
- package/src/core/shared-service/contract/ICoreService.ts +23 -0
- package/src/core/shared-service/contract/IUIService.ts +49 -0
- package/src/core/shared-service/service-proxy.ts +26 -0
- package/src/core/shared-service/service-registry.ts +52 -0
- package/src/index.tsx +3 -3
- package/src/plugins/blog/components/BlogSlotCom.tsx +27 -0
- package/src/plugins/blog/components/app.tsx +15 -0
- package/src/{blog → plugins/blog}/components/article/index.tsx +2 -2
- package/src/plugins/blog/constants/index.ts +1 -0
- package/src/plugins/blog/context/index.ts +7 -0
- package/src/plugins/blog/hooks/useMiddlewareTransData.ts +79 -0
- package/src/{blog → plugins/blog}/hooks/use_blog_scroll_pos_restoration.ts +4 -6
- package/src/plugins/blog/hooks/use_blog_slot_com.ts +27 -0
- package/src/{blog → plugins/blog}/hooks/use_posts.ts +11 -11
- package/src/plugins/blog/index.ts +61 -0
- package/src/{blog → plugins/blog}/pages/extensions/txt/article.tsx +5 -5
- package/src/{blog → plugins/blog}/pages/extensions/txt/index.tsx +6 -3
- package/src/plugins/blog/services/BlogUIService.ts +120 -0
- package/src/plugins/blog/services/IBlogUIService.ts +31 -0
- package/src/plugins/extra-cd/index.ts +13 -2
- package/src/plugins/extra-entry/index.ts +8 -4
- package/src/plugins/fx/index.ts +18 -5
- package/src/plugins/manifest.ts +8 -6
- package/src/plugins/now/hooks/use_streaming.ts +3 -1
- package/src/plugins/now/index.ts +17 -6
- package/src/plugins/sticker/const.ts +2 -2
- package/src/plugins/sticker/index.ts +18 -3
- package/src/plugins/store/components/storePage.tsx +15 -4
- package/src/plugins/store/context/index.ts +1 -0
- package/src/plugins/store/index.ts +18 -7
- package/src/plugins/xwy/index.ts +24 -19
- package/src/plugins/xwy/types/index.ts +0 -18
- package/src/types/hostApi.ts +3 -33
- package/src/types/plugin.ts +2 -0
- package/src/utils/index.tsx +12 -44
- package/tsconfig.json +0 -1
- package/vite.config.js +1 -1
- package/src/blog/app.tsx +0 -52
- package/src/blog/constants/index.ts +0 -1
- package/src/blog/constants/routes.ts +0 -20
- package/src/blog/hooks/index.ts +0 -2
- package/src/blog/hooks/use_blog_context.ts +0 -14
- package/src/blog/hooks/use_mouse_position.ts +0 -17
- package/src/blog/hooks/use_role.ts +0 -14
- package/src/blog/hooks/use_sticker.ts +0 -11
- package/src/blog/pages/index.tsx +0 -2
- package/src/blog/pages/login/index.tsx +0 -24
- package/src/blog/types/blog-context.ts +0 -6
- package/src/blog/types/font.ts +0 -4
- package/src/blog/types/glsl.d.ts +0 -8
- package/src/core/pluginStore.ts +0 -70
- /package/src/{blog/pages → app/components}/bot/index.tsx +0 -0
- /package/src/{blog → app}/context/global_loading_state_provider.tsx +0 -0
- /package/src/{blog → app}/context/global_routes_provider.tsx +0 -0
- /package/src/{blog → app}/hooks/use_pathname.ts +0 -0
- /package/src/{blog → app}/utils/fingerprints.ts +0 -0
- /package/src/core/{bbplugin.ts → plugin-system/bbplugin.ts} +0 -0
- /package/src/{blog → plugins/blog}/components/index.tsx +0 -0
- /package/src/{blog/types → types}/path.ts +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
2
|
+
|
|
3
|
+
import { pluginManager } from './pluginManager';
|
|
4
|
+
import { PluginManifestService } from './pluginManifestService';
|
|
5
|
+
|
|
6
|
+
export class PluginStore {
|
|
7
|
+
private static instance: PluginStore;
|
|
8
|
+
|
|
9
|
+
private installedSet: Set<PluginID> = new Set();
|
|
10
|
+
|
|
11
|
+
private constructor() {
|
|
12
|
+
this.parse();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private checkDep = (id: PluginID): boolean => {
|
|
16
|
+
const manifest = PluginManifestService.getInstance().getPlugin(id);
|
|
17
|
+
if (!manifest) {
|
|
18
|
+
console.warn(`[PluginStore] Manifest not found for plugin: ${id}`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!manifest.dependencies || manifest.dependencies.length === 0) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const depId of manifest.dependencies) {
|
|
27
|
+
if (!this.isPluginInstalled(depId)) {
|
|
28
|
+
console.warn(`[PluginStore] Dependency ${depId} for plugin ${id} is not installed`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
private parse = () => {
|
|
37
|
+
this.installedSet.clear();
|
|
38
|
+
const installedPluginsStr = localStorage.getItem('installed_plugins');
|
|
39
|
+
if (installedPluginsStr) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = JSON.parse(installedPluginsStr) as Array<PluginID>;
|
|
42
|
+
entries.forEach(id => this.installedSet.add(id));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('Failed to parse installed plugins from localStorage', e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
this.ensureRightOrder(this.installedSet);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Failed to ensure correct plugin load order:', error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return this.installedSet;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
private buildDependencyGraph = (ids: Set<PluginID>): Map<PluginID, PluginID[]> => {
|
|
58
|
+
const graph = new Map<PluginID, PluginID[]>();
|
|
59
|
+
const manifest = PluginManifestService.getInstance().getAllPlugins();
|
|
60
|
+
|
|
61
|
+
ids.forEach(id => {
|
|
62
|
+
graph.set(id, []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
manifest.forEach(plugin => {
|
|
66
|
+
if (ids.has(plugin.id)) {
|
|
67
|
+
const deps = plugin.dependencies || [];
|
|
68
|
+
deps.forEach(dep => {
|
|
69
|
+
if (ids.has(dep)) {
|
|
70
|
+
if (!graph.has(dep)) {
|
|
71
|
+
graph.set(dep, []);
|
|
72
|
+
}
|
|
73
|
+
graph.get(dep)!.push(plugin.id);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return graph;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private topologicalSortKahn = (graph: Map<PluginID, PluginID[]>): PluginID[] => {
|
|
83
|
+
const inDegree = new Map<PluginID, number>();
|
|
84
|
+
graph.forEach((deps, plugin) => {
|
|
85
|
+
if (!inDegree.has(plugin)) {
|
|
86
|
+
inDegree.set(plugin, 0);
|
|
87
|
+
}
|
|
88
|
+
deps.forEach(dep => {
|
|
89
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const queue: PluginID[] = [];
|
|
94
|
+
inDegree.forEach((degree, plugin) => {
|
|
95
|
+
if (degree === 0) {
|
|
96
|
+
queue.push(plugin);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const loadOrder: PluginID[] = [];
|
|
101
|
+
while (queue.length > 0) {
|
|
102
|
+
const current = queue.shift()!;
|
|
103
|
+
loadOrder.push(current);
|
|
104
|
+
|
|
105
|
+
const deps = graph.get(current) || [];
|
|
106
|
+
deps.forEach(dep => {
|
|
107
|
+
inDegree.set(dep, inDegree.get(dep)! - 1);
|
|
108
|
+
if (inDegree.get(dep) === 0) {
|
|
109
|
+
queue.push(dep);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (loadOrder.length !== graph.size) {
|
|
115
|
+
throw new Error('Circular dependency detected among plugins');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return loadOrder;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
private ensureRightOrder = (ids: Set<PluginID>) => {
|
|
122
|
+
const graph = this.buildDependencyGraph(ids);
|
|
123
|
+
const loadOrder = this.topologicalSortKahn(graph);
|
|
124
|
+
|
|
125
|
+
// write back to installedSet in the correct order without create a new Set to preserve reference
|
|
126
|
+
if (loadOrder.length === ids.size) {
|
|
127
|
+
this.installedSet.clear();
|
|
128
|
+
loadOrder.forEach(id => this.installedSet.add(id));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private stringify = () => {
|
|
133
|
+
localStorage.setItem('installed_plugins', JSON.stringify(Array.from(this.installedSet)));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
getInstalledPlugins: () => Set<PluginID> = () => {
|
|
137
|
+
return this.parse();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
async getAllPlugins(): Promise<Array<IPluginStoreEntry>> {
|
|
141
|
+
const installedPlugins = this.getInstalledPlugins();
|
|
142
|
+
await PluginManifestService.getInstance().fetch();
|
|
143
|
+
const manifest = PluginManifestService.getInstance().getAllPlugins();
|
|
144
|
+
return manifest.map(plugin => ({
|
|
145
|
+
...plugin,
|
|
146
|
+
enabled: installedPlugins.has(plugin.id),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isPluginInstalled: (id: PluginID) => boolean = id => {
|
|
151
|
+
return this.getInstalledPlugins().has(id);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
getPlugin: (id: PluginID) => IPluginStoreEntry | undefined = id => {
|
|
155
|
+
const manifest = PluginManifestService.getInstance().getPlugin(id);
|
|
156
|
+
if (!manifest) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...manifest,
|
|
162
|
+
enabled: this.isPluginInstalled(id),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
installPlugin: (id: PluginID) => Promise<void> = async id => {
|
|
167
|
+
if (!this.checkDep(id)) {
|
|
168
|
+
// throw new Error(`Cannot install plugin ${id} due to missing dependencies`);
|
|
169
|
+
console.warn(`Cannot install plugin ${id} due to missing dependencies`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await pluginManager.loadPlugin(id);
|
|
174
|
+
this.installedSet.add(id);
|
|
175
|
+
this.stringify();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
disablePlugin: (id: PluginID) => Promise<void> = async id => {
|
|
179
|
+
await pluginManager.disablePlugin(id);
|
|
180
|
+
this.installedSet.delete(id);
|
|
181
|
+
this.stringify();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
static getInstance() {
|
|
185
|
+
if (!PluginStore.instance) {
|
|
186
|
+
PluginStore.instance = new PluginStore();
|
|
187
|
+
}
|
|
188
|
+
return PluginStore.instance;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
4
4
|
|
|
5
|
-
import { createEventBus } from '
|
|
5
|
+
import { createEventBus } from '../utils/eventBus';
|
|
6
6
|
|
|
7
7
|
export interface ISlotEntry {
|
|
8
8
|
id: string;
|
|
@@ -120,5 +120,3 @@ export class Registry {
|
|
|
120
120
|
return result;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
export const registry = new Registry();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Fetcher } from 'swr';
|
|
2
|
+
|
|
3
|
+
import { getStableDeviceId } from '#/app/utils/fingerprints';
|
|
4
|
+
import { cfApiFetcher } from '#/app/utils';
|
|
5
|
+
import { PluginEvents, PluginID } from '#/types/plugin';
|
|
6
|
+
|
|
7
|
+
import { ICoreService } from '../../shared-service/contract/ICoreService';
|
|
8
|
+
import { createEventBus } from '../../utils/eventBus';
|
|
9
|
+
|
|
10
|
+
export class CoreService implements ICoreService {
|
|
11
|
+
private static instance: CoreService;
|
|
12
|
+
|
|
13
|
+
private constructor() {}
|
|
14
|
+
|
|
15
|
+
private bus = createEventBus<PluginEvents>();
|
|
16
|
+
|
|
17
|
+
getDeviceId = getStableDeviceId;
|
|
18
|
+
|
|
19
|
+
getVersionHash() {
|
|
20
|
+
const hashStr: string = typeof GLOBAL_COMMIT_HASH === 'string' ? GLOBAL_COMMIT_HASH : '0000000';
|
|
21
|
+
return hashStr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setLoading(id: PluginID, loading: boolean): void {
|
|
25
|
+
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
|
|
29
|
+
return this.bus.on('plugin:loading:changed', listener);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
subscribePluginUninstall(listener: (payload: PluginEvents['plugin:uninstall']) => void) {
|
|
33
|
+
return this.bus.on('plugin:uninstall', listener);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
notifyPluginUninstall(id: PluginID): void {
|
|
37
|
+
this.bus.emit('plugin:uninstall', id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
subscribeLoadingChange(listener: (payload: boolean) => void): () => void {
|
|
41
|
+
return this.bus.on('site:loading:changed', listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setSiteLoading(loading: boolean) {
|
|
45
|
+
this.bus.emit('site:loading:changed', loading);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fetch: Fetcher = cfApiFetcher;
|
|
49
|
+
|
|
50
|
+
static getInstance(): CoreService {
|
|
51
|
+
if (!CoreService.instance) {
|
|
52
|
+
CoreService.instance = new CoreService();
|
|
53
|
+
}
|
|
54
|
+
return CoreService.instance;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PathRouteProps } from 'react-router-dom';
|
|
3
|
+
import { LinkProps } from '@bbki.ng/ui';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ISystemUIService,
|
|
7
|
+
SystemDataHookPoint,
|
|
8
|
+
SystemSlotName,
|
|
9
|
+
} from '#/core/shared-service/contract/IUIService';
|
|
10
|
+
import { IPluginEntry, PluginID } from '#/types/plugin';
|
|
11
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
12
|
+
import { createEventBus } from '#/core/utils/eventBus';
|
|
13
|
+
import { buildEntrySlotCom } from '#/utils';
|
|
14
|
+
|
|
15
|
+
import { IMiddlewareEntry, ISlotEntry } from '../registry';
|
|
16
|
+
|
|
17
|
+
type RegistryEvents = {
|
|
18
|
+
'system:slots:changed': void;
|
|
19
|
+
'system:middleware:changed': SystemDataHookPoint;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class SystemUIService implements ISystemUIService {
|
|
23
|
+
private static instance: SystemUIService;
|
|
24
|
+
|
|
25
|
+
private slots = new Map<SystemSlotName, Array<ISlotEntry>>();
|
|
26
|
+
private middlewares = new Map<SystemDataHookPoint, IMiddlewareEntry<unknown>[]>();
|
|
27
|
+
|
|
28
|
+
private bus = createEventBus<RegistryEvents>();
|
|
29
|
+
|
|
30
|
+
private constructor() {}
|
|
31
|
+
|
|
32
|
+
subscribeSlotChange(listener: () => void) {
|
|
33
|
+
return this.bus.on('system:slots:changed', listener);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerMiddleware: <T>(
|
|
37
|
+
hookPoint: SystemDataHookPoint,
|
|
38
|
+
fn: (data: T) => T | Promise<T>,
|
|
39
|
+
pluginId: PluginID,
|
|
40
|
+
weight?: number
|
|
41
|
+
) => void = <T>(
|
|
42
|
+
hookPoint: SystemDataHookPoint,
|
|
43
|
+
fn: (data: T) => T | Promise<T>,
|
|
44
|
+
pluginId: PluginID,
|
|
45
|
+
weight = 0
|
|
46
|
+
) => {
|
|
47
|
+
const existing = this.middlewares.get(hookPoint) || [];
|
|
48
|
+
|
|
49
|
+
const newEntry: IMiddlewareEntry<T> = {
|
|
50
|
+
id: `${pluginId}-${hookPoint}-middleware`,
|
|
51
|
+
pluginId,
|
|
52
|
+
fn,
|
|
53
|
+
weight,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const updated = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
|
|
57
|
+
(a, b) => (b.weight || 0) - (a.weight || 0)
|
|
58
|
+
);
|
|
59
|
+
this.middlewares.set(hookPoint, updated);
|
|
60
|
+
|
|
61
|
+
this.bus.emit('system:middleware:changed', hookPoint);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
static getInstance(): SystemUIService {
|
|
65
|
+
if (!SystemUIService.instance) {
|
|
66
|
+
SystemUIService.instance = new SystemUIService();
|
|
67
|
+
}
|
|
68
|
+
return SystemUIService.instance;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
registerPluginEntry: (entry: IPluginEntry, id: PluginID) => void = (entry, id) => {
|
|
72
|
+
this.registerMiddleware(
|
|
73
|
+
'extendedRoutes',
|
|
74
|
+
(routes: Array<Omit<PathRouteProps, 'element'>>) => {
|
|
75
|
+
return [
|
|
76
|
+
...routes,
|
|
77
|
+
{
|
|
78
|
+
path: entry.path,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
},
|
|
82
|
+
id,
|
|
83
|
+
10
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
this.registerSlot('route', buildEntrySlotCom(entry), id);
|
|
87
|
+
|
|
88
|
+
if (!entry.label) return;
|
|
89
|
+
|
|
90
|
+
this.registerMiddleware(
|
|
91
|
+
'transformCoverEntry',
|
|
92
|
+
(entries: Array<LinkProps>) => {
|
|
93
|
+
return [
|
|
94
|
+
...entries,
|
|
95
|
+
{
|
|
96
|
+
to: entry.path,
|
|
97
|
+
children: entry.label,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
},
|
|
101
|
+
id,
|
|
102
|
+
10
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
registerSlot: (
|
|
107
|
+
slotName: SystemSlotName,
|
|
108
|
+
component: React.ComponentType<IComPropsRegisteredToSlot>,
|
|
109
|
+
pluginId: PluginID,
|
|
110
|
+
weight?: number
|
|
111
|
+
) => void = (slotName, component, pluginId, weight = 0) => {
|
|
112
|
+
const existing = this.slots.get(slotName) || [];
|
|
113
|
+
|
|
114
|
+
const newEntry: ISlotEntry = {
|
|
115
|
+
id: `${pluginId}-${component.name || 'comp'}`,
|
|
116
|
+
pluginId,
|
|
117
|
+
component,
|
|
118
|
+
weight,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 插入并按权重排序(权重大的在前)
|
|
122
|
+
const updated = [...existing, newEntry].sort((a, b) => b.weight - a.weight);
|
|
123
|
+
this.slots.set(slotName, updated);
|
|
124
|
+
|
|
125
|
+
this.bus.emit('system:slots:changed', undefined);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
unregisterAllByPluginId(pluginId: string) {
|
|
129
|
+
this.slots.forEach((entries, slotName) => {
|
|
130
|
+
const filtered = entries.filter(entry => entry.pluginId !== pluginId);
|
|
131
|
+
this.slots.set(slotName, filtered);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.middlewares.forEach((entries, point) => {
|
|
135
|
+
const filtered = entries.filter(entry => entry.pluginId !== pluginId);
|
|
136
|
+
this.middlewares.set(point, filtered);
|
|
137
|
+
this.bus.emit('system:middleware:changed', point);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.bus.emit('system:slots:changed', undefined);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getComponents(slotName: SystemSlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
|
|
144
|
+
return (this.slots.get(slotName) || []).map(entry => entry.component);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
subscribeMiddlewareChange(listener: (point: SystemDataHookPoint) => void) {
|
|
148
|
+
return this.bus.on('system:middleware:changed', listener);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async runMiddlewares<T>(point: SystemDataHookPoint, data: T): Promise<T> {
|
|
152
|
+
const fns = (this.middlewares.get(point) || []).map(entry => entry.fn);
|
|
153
|
+
let result = data;
|
|
154
|
+
for (const fn of fns) {
|
|
155
|
+
result = (await fn(result)) as T;
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Fetcher } from 'swr';
|
|
2
|
+
|
|
3
|
+
import { FingerprintData } from '#/app/utils/fingerprints';
|
|
4
|
+
import { PluginID } from '#/types/plugin';
|
|
5
|
+
|
|
6
|
+
declare module '#/core/shared-service/service-registry' {
|
|
7
|
+
interface ServiceIdentifierMap {
|
|
8
|
+
'core:baseService': ICoreService;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ICoreService {
|
|
13
|
+
getVersionHash(): Promise<string> | string;
|
|
14
|
+
getDeviceId(): Promise<{ id: string; fp: FingerprintData }>;
|
|
15
|
+
|
|
16
|
+
setLoading(id: string, loading: boolean): void;
|
|
17
|
+
subscribeLoadingChange(listener: (payload: boolean) => void): () => void;
|
|
18
|
+
|
|
19
|
+
subscribePluginUninstall(listener: (payload: PluginID) => void): () => void;
|
|
20
|
+
notifyPluginUninstall(id: PluginID): void;
|
|
21
|
+
|
|
22
|
+
fetch: Fetcher;
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import { IPluginEntry, PluginID } from '#/types/plugin';
|
|
4
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
+
|
|
6
|
+
declare module '#/core/shared-service/service-registry' {
|
|
7
|
+
interface ServiceIdentifierMap {
|
|
8
|
+
'core:uiService': ISystemUIService;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IBaseUIService<T extends string, K extends string> {
|
|
13
|
+
registerMiddleware: <S>(
|
|
14
|
+
hookPoint: K,
|
|
15
|
+
fn: (data: S) => Promise<S> | S,
|
|
16
|
+
pluginId: PluginID,
|
|
17
|
+
weight?: number
|
|
18
|
+
) => void;
|
|
19
|
+
|
|
20
|
+
registerSlot: (
|
|
21
|
+
slotName: T,
|
|
22
|
+
component: React.ComponentType<IComPropsRegisteredToSlot>,
|
|
23
|
+
pluginId: PluginID,
|
|
24
|
+
weight?: number
|
|
25
|
+
) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SystemSlotName = 'leftCol' | 'rightCol' | 'logo' | 'route' | 'pageFooter';
|
|
29
|
+
|
|
30
|
+
export type SystemDataHookPoint =
|
|
31
|
+
| 'extendedRoutes'
|
|
32
|
+
| 'transformBreadcrumbPath'
|
|
33
|
+
| 'transformCoverEntry';
|
|
34
|
+
|
|
35
|
+
export interface ISystemUIService extends IBaseUIService<SystemSlotName, SystemDataHookPoint> {
|
|
36
|
+
registerMiddleware: <S>(
|
|
37
|
+
hookPoint: SystemDataHookPoint,
|
|
38
|
+
fn: (data: S) => S | Promise<S>,
|
|
39
|
+
pluginId: PluginID,
|
|
40
|
+
weight?: number
|
|
41
|
+
) => void;
|
|
42
|
+
registerSlot: (
|
|
43
|
+
slotName: SystemSlotName,
|
|
44
|
+
component: React.ComponentType<IComPropsRegisteredToSlot>,
|
|
45
|
+
pluginId: PluginID,
|
|
46
|
+
weight?: number
|
|
47
|
+
) => void;
|
|
48
|
+
registerPluginEntry: (entry: IPluginEntry, id: PluginID) => void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ServiceIdentifierMap, ServiceRegistry } from '#/core/shared-service/service-registry';
|
|
2
|
+
|
|
3
|
+
// core/ServiceProxy.ts
|
|
4
|
+
export function createServiceProxy<T extends object, K extends keyof ServiceIdentifierMap>(
|
|
5
|
+
serviceId: K,
|
|
6
|
+
container: ServiceRegistry
|
|
7
|
+
): T {
|
|
8
|
+
return new Proxy({} as T, {
|
|
9
|
+
get(target, prop, receiver) {
|
|
10
|
+
const actualService = container.getRawInstance(serviceId);
|
|
11
|
+
|
|
12
|
+
if (!actualService) {
|
|
13
|
+
return (..._: unknown[]) => {
|
|
14
|
+
console.warn(
|
|
15
|
+
`[System] Service ${serviceId} is currently unavailable. Call to ${String(prop)} ignored.`
|
|
16
|
+
);
|
|
17
|
+
return undefined; // 或者返回一个待定 Promise
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 3. 正常转发调用
|
|
22
|
+
const value = Reflect.get(actualService, prop, receiver);
|
|
23
|
+
return typeof value === 'function' ? value.bind(actualService) : value;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createServiceProxy } from './service-proxy';
|
|
2
|
+
|
|
3
|
+
export interface ServiceIdentifierMap {}
|
|
4
|
+
|
|
5
|
+
export class ServiceRegistry {
|
|
6
|
+
private services: Map<string, unknown> = new Map();
|
|
7
|
+
|
|
8
|
+
private constructor() {}
|
|
9
|
+
|
|
10
|
+
private static instance: ServiceRegistry;
|
|
11
|
+
|
|
12
|
+
static getInstance(): ServiceRegistry {
|
|
13
|
+
if (!ServiceRegistry.instance) {
|
|
14
|
+
ServiceRegistry.instance = new ServiceRegistry();
|
|
15
|
+
}
|
|
16
|
+
return ServiceRegistry.instance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
register<T extends keyof ServiceIdentifierMap>(name: T, service: ServiceIdentifierMap[T]): void {
|
|
20
|
+
if (this.services.has(name)) {
|
|
21
|
+
console.warn(`Service with name ${name} is already registered. It will be overwritten.`);
|
|
22
|
+
}
|
|
23
|
+
this.services.set(name, service);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getRawInstance<T extends keyof ServiceIdentifierMap>(
|
|
27
|
+
name: T
|
|
28
|
+
): ServiceIdentifierMap[T] | undefined {
|
|
29
|
+
const target = this.services.get(name);
|
|
30
|
+
if (!target) {
|
|
31
|
+
console.warn(`Service with name ${name} is not registered.`);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return target as ServiceIdentifierMap[T];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get<T extends keyof ServiceIdentifierMap>(name: T): ServiceIdentifierMap[T] | undefined {
|
|
39
|
+
// const target = this.services.get(name);
|
|
40
|
+
// if (!target) {
|
|
41
|
+
// console.warn(`Service with name ${name} is not registered.`);
|
|
42
|
+
// return undefined;
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
// return target as ServiceIdentifierMap[T];
|
|
46
|
+
return createServiceProxy(name, this) as ServiceIdentifierMap[T];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
unregister<T extends keyof ServiceIdentifierMap>(name: T): void {
|
|
50
|
+
this.services.delete(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderApp } from './app';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const appContainer = document.getElementById('app') as Element;
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
RenderApp(appContainer);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useBlogSlotComp } from '../hooks/use_blog_slot_com';
|
|
4
|
+
import { BlogSlotName } from '../services/IBlogUIService';
|
|
5
|
+
|
|
6
|
+
export interface ISlotProps {
|
|
7
|
+
name: BlogSlotName;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
|
|
10
|
+
placeholder?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Slot: React.FC<ISlotProps> = ({ name, data, placeholder }) => {
|
|
14
|
+
const components = useBlogSlotComp(name);
|
|
15
|
+
|
|
16
|
+
if (components.length === 0) {
|
|
17
|
+
return <>{placeholder}</>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
{components.map((Component, index) => (
|
|
23
|
+
<Component key={index} data={data} />
|
|
24
|
+
))}
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useRoutes } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import ArticlePage from '#/plugins/blog/pages/extensions/txt/article';
|
|
5
|
+
import Txt from '#/plugins/blog/pages/extensions/txt';
|
|
6
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
7
|
+
|
|
8
|
+
export const BlogPageApp = (_: IComPropsRegisteredToSlot) => {
|
|
9
|
+
const element = useRoutes([
|
|
10
|
+
{ path: '/', element: <Txt /> },
|
|
11
|
+
{ path: '/:title', element: <ArticlePage /> },
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
return <>{element}</>;
|
|
15
|
+
};
|
|
@@ -2,7 +2,7 @@ import React, { ReactElement, ReactNode } from 'react';
|
|
|
2
2
|
import { Article, Link } from '@bbki.ng/ui';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
|
|
5
|
-
import { Slot } from '
|
|
5
|
+
import { Slot } from '../BlogSlotCom';
|
|
6
6
|
|
|
7
7
|
export type ArticlePageProps = {
|
|
8
8
|
tags?: string[];
|
|
@@ -27,7 +27,7 @@ export const ArticlePage = (props: ArticlePageProps) => {
|
|
|
27
27
|
return (
|
|
28
28
|
<>
|
|
29
29
|
<Article
|
|
30
|
-
title={<Slot name="articleTitle" data={title} placeholder={<>{title}</>} />}
|
|
30
|
+
title={<Slot name="blog:articleTitle" data={title} placeholder={<>{title}</>} />}
|
|
31
31
|
date={props.date}
|
|
32
32
|
description={description}
|
|
33
33
|
className={`${props.className || ''}`}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLUGIN_NAME = 'blog';
|