@nocobase/client-v2 2.0.0-alpha.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 (70) hide show
  1. package/LICENSE.txt +172 -0
  2. package/lib/Application.d.ts +124 -0
  3. package/lib/Application.js +489 -0
  4. package/lib/MockApplication.d.ts +16 -0
  5. package/lib/MockApplication.js +96 -0
  6. package/lib/Plugin.d.ts +33 -0
  7. package/lib/Plugin.js +89 -0
  8. package/lib/PluginManager.d.ts +46 -0
  9. package/lib/PluginManager.js +114 -0
  10. package/lib/PluginSettingsManager.d.ts +67 -0
  11. package/lib/PluginSettingsManager.js +148 -0
  12. package/lib/RouterManager.d.ts +61 -0
  13. package/lib/RouterManager.js +198 -0
  14. package/lib/WebSocketClient.d.ts +45 -0
  15. package/lib/WebSocketClient.js +217 -0
  16. package/lib/components/BlankComponent.d.ts +12 -0
  17. package/lib/components/BlankComponent.js +48 -0
  18. package/lib/components/MainComponent.d.ts +10 -0
  19. package/lib/components/MainComponent.js +54 -0
  20. package/lib/components/RouterBridge.d.ts +13 -0
  21. package/lib/components/RouterBridge.js +66 -0
  22. package/lib/components/RouterContextCleaner.d.ts +12 -0
  23. package/lib/components/RouterContextCleaner.js +61 -0
  24. package/lib/components/index.d.ts +10 -0
  25. package/lib/components/index.js +32 -0
  26. package/lib/context.d.ts +11 -0
  27. package/lib/context.js +38 -0
  28. package/lib/hooks/index.d.ts +11 -0
  29. package/lib/hooks/index.js +34 -0
  30. package/lib/hooks/useApp.d.ts +10 -0
  31. package/lib/hooks/useApp.js +41 -0
  32. package/lib/hooks/usePlugin.d.ts +11 -0
  33. package/lib/hooks/usePlugin.js +42 -0
  34. package/lib/hooks/useRouter.d.ts +9 -0
  35. package/lib/hooks/useRouter.js +41 -0
  36. package/lib/index.d.ts +14 -0
  37. package/lib/index.js +40 -0
  38. package/lib/utils/index.d.ts +11 -0
  39. package/lib/utils/index.js +79 -0
  40. package/lib/utils/remotePlugins.d.ts +44 -0
  41. package/lib/utils/remotePlugins.js +131 -0
  42. package/lib/utils/requirejs.d.ts +18 -0
  43. package/lib/utils/requirejs.js +1361 -0
  44. package/lib/utils/types.d.ts +330 -0
  45. package/lib/utils/types.js +28 -0
  46. package/package.json +16 -0
  47. package/src/Application.tsx +539 -0
  48. package/src/MockApplication.tsx +53 -0
  49. package/src/Plugin.ts +78 -0
  50. package/src/PluginManager.ts +114 -0
  51. package/src/PluginSettingsManager.ts +182 -0
  52. package/src/RouterManager.tsx +239 -0
  53. package/src/WebSocketClient.ts +220 -0
  54. package/src/__tests__/app.test.tsx +141 -0
  55. package/src/components/BlankComponent.tsx +12 -0
  56. package/src/components/MainComponent.tsx +20 -0
  57. package/src/components/RouterBridge.tsx +38 -0
  58. package/src/components/RouterContextCleaner.tsx +26 -0
  59. package/src/components/index.ts +11 -0
  60. package/src/context.ts +14 -0
  61. package/src/hooks/index.ts +12 -0
  62. package/src/hooks/useApp.ts +16 -0
  63. package/src/hooks/usePlugin.ts +17 -0
  64. package/src/hooks/useRouter.ts +15 -0
  65. package/src/index.ts +15 -0
  66. package/src/utils/index.tsx +48 -0
  67. package/src/utils/remotePlugins.ts +140 -0
  68. package/src/utils/requirejs.ts +2164 -0
  69. package/src/utils/types.ts +375 -0
  70. package/tsconfig.json +7 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { Application } from './Application';
11
+ import type { Plugin } from './Plugin';
12
+ import { getPlugins } from './utils/remotePlugins';
13
+
14
+ export type PluginOptions<T = any> = { name?: string; packageName?: string; config?: T };
15
+ export type PluginType<Opts = any> = typeof Plugin | [typeof Plugin<Opts>, PluginOptions<Opts>];
16
+ export type PluginData = {
17
+ name: string;
18
+ packageName: string;
19
+ version: string;
20
+ url: string;
21
+ type: 'local' | 'upload' | 'npm';
22
+ };
23
+
24
+ export class PluginManager {
25
+ protected pluginInstances: Map<typeof Plugin, Plugin> = new Map();
26
+ protected pluginsAliases: Record<string, Plugin> = {};
27
+ private initPlugins: Promise<void>;
28
+
29
+ constructor(
30
+ protected _plugins: PluginType[],
31
+ protected loadRemotePlugins: boolean,
32
+ protected app: Application,
33
+ ) {
34
+ this.app = app;
35
+ this.initPlugins = this.init(_plugins);
36
+ }
37
+
38
+ /**
39
+ * @internal
40
+ */
41
+ async init(_plugins: PluginType[]) {
42
+ await this.initStaticPlugins(_plugins);
43
+ if (this.loadRemotePlugins) {
44
+ await this.initRemotePlugins();
45
+ }
46
+ }
47
+
48
+ private async initStaticPlugins(_plugins: PluginType[] = []) {
49
+ for await (const plugin of _plugins) {
50
+ const pluginClass = Array.isArray(plugin) ? plugin[0] : plugin;
51
+ const opts = Array.isArray(plugin) ? plugin[1] : undefined;
52
+ await this.add(pluginClass, opts);
53
+ }
54
+ }
55
+
56
+ private async initRemotePlugins() {
57
+ const res = await this.app.apiClient.request({ url: 'pm:listEnabled' });
58
+ const pluginList: PluginData[] = res?.data?.data || [];
59
+ const plugins = await getPlugins({
60
+ requirejs: this.app.requirejs,
61
+ pluginData: pluginList,
62
+ devDynamicImport: this.app.devDynamicImport,
63
+ });
64
+ for await (const [name, pluginClass] of plugins) {
65
+ const info = pluginList.find((item) => item.name === name);
66
+ await this.add(pluginClass, info);
67
+ }
68
+ }
69
+
70
+ async add<T = any>(plugin: typeof Plugin, opts: PluginOptions<T> = {}) {
71
+ const instance = this.getInstance(plugin, opts);
72
+
73
+ this.pluginInstances.set(plugin, instance);
74
+
75
+ if (opts.name) {
76
+ this.pluginsAliases[opts.name] = instance;
77
+ }
78
+
79
+ if (opts.packageName) {
80
+ this.pluginsAliases[opts.packageName] = instance;
81
+ }
82
+
83
+ await instance.afterAdd();
84
+ }
85
+
86
+ get<T extends typeof Plugin>(PluginClass: T): InstanceType<T>;
87
+ get<T extends {}>(name: string): T;
88
+ get(nameOrPluginClass: any) {
89
+ if (typeof nameOrPluginClass === 'string') {
90
+ return this.pluginsAliases[nameOrPluginClass];
91
+ }
92
+ return this.pluginInstances.get(nameOrPluginClass.default || nameOrPluginClass);
93
+ }
94
+
95
+ private getInstance<T>(plugin: typeof Plugin, opts?: T) {
96
+ return new plugin(opts, this.app);
97
+ }
98
+
99
+ /**
100
+ * @internal
101
+ */
102
+ async load() {
103
+ await this.initPlugins;
104
+
105
+ for (const plugin of this.pluginInstances.values()) {
106
+ await plugin.beforeLoad();
107
+ }
108
+
109
+ for (const plugin of this.pluginInstances.values()) {
110
+ await plugin.load();
111
+ this.app.eventBus.dispatchEvent(new CustomEvent(`plugin:${plugin.options.name}:loaded`, { detail: plugin }));
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { set } from 'lodash';
11
+ import React from 'react';
12
+ import { Outlet } from 'react-router-dom';
13
+
14
+ import type { Application } from './Application';
15
+ import type { RouteType } from './RouterManager';
16
+
17
+ export const ADMIN_SETTINGS_KEY = 'admin.settings.';
18
+ export const ADMIN_SETTINGS_PATH = '/admin/settings/';
19
+ export const SNIPPET_PREFIX = 'pm.';
20
+
21
+ export interface PluginSettingOptions {
22
+ title: any;
23
+ /**
24
+ * @default Outlet
25
+ */
26
+ Component?: RouteType['Component'];
27
+ icon?: string;
28
+ /**
29
+ * sort, the smaller the number, the higher the priority
30
+ * @default 0
31
+ */
32
+ sort?: number;
33
+ aclSnippet?: string;
34
+ link?: string;
35
+ isTopLevel?: boolean;
36
+ isPinned?: boolean;
37
+ [index: string]: any;
38
+ }
39
+
40
+ export interface PluginSettingsPageType {
41
+ label?: string | React.ReactElement;
42
+ title: string | React.ReactElement;
43
+ link?: string;
44
+ key: string;
45
+ icon: any;
46
+ path: string;
47
+ sort?: number;
48
+ name?: string;
49
+ isAllow?: boolean;
50
+ topLevelName?: string;
51
+ aclSnippet: string;
52
+ children?: PluginSettingsPageType[];
53
+ [index: string]: any;
54
+ }
55
+
56
+ export class PluginSettingsManager {
57
+ protected settings: Record<string, PluginSettingOptions> = {};
58
+ protected aclSnippets: string[] = [];
59
+ public app: Application;
60
+ private cachedList = {};
61
+
62
+ constructor(_pluginSettings: Record<string, PluginSettingOptions>, app: Application) {
63
+ this.app = app;
64
+ Object.entries(_pluginSettings || {}).forEach(([name, pluginSettingOptions]) => {
65
+ this.add(name, pluginSettingOptions);
66
+ });
67
+ }
68
+
69
+ clearCache() {
70
+ this.cachedList = {};
71
+ }
72
+
73
+ setAclSnippets(aclSnippets: string[]) {
74
+ this.aclSnippets = aclSnippets;
75
+ }
76
+
77
+ getAclSnippet(name: string) {
78
+ const setting = this.settings[name];
79
+ if (setting?.skipAclConfigure) {
80
+ return null;
81
+ }
82
+ return setting?.aclSnippet ? setting.aclSnippet : `${SNIPPET_PREFIX}${name}`;
83
+ }
84
+
85
+ getRouteName(name: string) {
86
+ return `${ADMIN_SETTINGS_KEY}${name}`;
87
+ }
88
+
89
+ getRoutePath(name: string) {
90
+ return `${ADMIN_SETTINGS_PATH}${name.replaceAll('.', '/')}`;
91
+ }
92
+
93
+ add(name: string, options: PluginSettingOptions) {
94
+ const nameArr = name.split('.');
95
+ const topLevelName = nameArr[0];
96
+ this.settings[name] = {
97
+ ...this.settings[name],
98
+ Component: Outlet,
99
+ ...options,
100
+ name,
101
+ topLevelName: options.topLevelName || topLevelName,
102
+ };
103
+ // add children
104
+ if (nameArr.length > 1) {
105
+ set(this.settings, nameArr.join('.children.'), this.settings[name]);
106
+ }
107
+
108
+ // add route
109
+ this.app.router.add(this.getRouteName(name), {
110
+ path: this.getRoutePath(name),
111
+ Component: this.settings[name].Component,
112
+ });
113
+ }
114
+
115
+ remove(name: string) {
116
+ // delete self and children
117
+ Object.keys(this.settings).forEach((key) => {
118
+ if (key.startsWith(name)) {
119
+ delete this.settings[key];
120
+ this.app.router.remove(`${ADMIN_SETTINGS_KEY}${key}`);
121
+ }
122
+ });
123
+ }
124
+
125
+ hasAuth(name: string) {
126
+ if (this.aclSnippets.includes(`!${this.getAclSnippet('*')}`)) return false;
127
+ return this.aclSnippets.includes(`!${this.getAclSnippet(name)}`) === false;
128
+ }
129
+
130
+ getSetting(name: string) {
131
+ return this.settings[name];
132
+ }
133
+
134
+ has(name: string) {
135
+ const hasAuth = this.hasAuth(name);
136
+ if (!hasAuth) return false;
137
+ return !!this.getSetting(name);
138
+ }
139
+
140
+ get(name: string, filterAuth = true): PluginSettingsPageType {
141
+ const isAllow = this.hasAuth(name);
142
+ const pluginSetting = this.getSetting(name);
143
+ if ((filterAuth && !isAllow) || !pluginSetting) return null;
144
+ const children = Object.keys(pluginSetting.children || {})
145
+ .sort((a, b) => a.localeCompare(b)) // sort by name
146
+ .map((key) => this.get(pluginSetting.children[key].name, filterAuth))
147
+ .filter(Boolean)
148
+ .sort((a, b) => (a.sort || 0) - (b.sort || 0));
149
+ const { title, icon, aclSnippet, ...others } = pluginSetting;
150
+ return {
151
+ isTopLevel: name === pluginSetting.topLevelName,
152
+ ...others,
153
+ aclSnippet: this.getAclSnippet(name),
154
+ title,
155
+ isAllow,
156
+ label: title,
157
+ icon: this.app.flowEngine.context.renderIon?.(icon),
158
+ path: this.getRoutePath(name),
159
+ key: name,
160
+ children: children.length ? children : undefined,
161
+ };
162
+ }
163
+
164
+ getList(filterAuth = true): PluginSettingsPageType[] {
165
+ const cacheKey = JSON.stringify(filterAuth);
166
+ if (this.cachedList[cacheKey]) return this.cachedList[cacheKey];
167
+
168
+ return (this.cachedList[cacheKey] = Array.from(
169
+ new Set(Object.values(this.settings).map((item) => item.topLevelName)),
170
+ )
171
+ .sort((a, b) => a.localeCompare(b)) // sort by name
172
+ .map((name) => this.get(name, filterAuth))
173
+ .filter(Boolean)
174
+ .sort((a, b) => (a.sort || 0) - (b.sort || 0)));
175
+ }
176
+
177
+ getAclSnippets() {
178
+ return Object.keys(this.settings)
179
+ .map((name) => this.getAclSnippet(name))
180
+ .filter(Boolean);
181
+ }
182
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { get, set } from 'lodash';
11
+ import React, { ComponentType, createContext, useContext } from 'react';
12
+ import { matchRoutes, useParams } from 'react-router';
13
+ import {
14
+ type BrowserRouterProps,
15
+ createBrowserRouter,
16
+ createHashRouter,
17
+ createMemoryRouter,
18
+ type HashRouterProps,
19
+ type MemoryRouterProps,
20
+ Outlet,
21
+ type RouteObject,
22
+ RouterProvider,
23
+ useRouteError,
24
+ } from 'react-router-dom';
25
+ import { Application } from './Application';
26
+ import { BlankComponent, RouterContextCleaner } from './components';
27
+ import { RouterBridge } from './components/RouterBridge';
28
+
29
+ export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
30
+ type?: 'browser';
31
+ }
32
+ export interface HashRouterOptions extends Omit<HashRouterProps, 'children'> {
33
+ type?: 'hash';
34
+ }
35
+ export interface MemoryRouterOptions extends Omit<MemoryRouterProps, 'children'> {
36
+ type?: 'memory';
37
+ }
38
+ export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & {
39
+ renderComponent?: RenderComponentType;
40
+ routes?: Record<string, RouteType>;
41
+ };
42
+ export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
43
+ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
44
+ Component?: ComponentTypeAndString;
45
+ skipAuthCheck?: boolean;
46
+ }
47
+ export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
48
+
49
+ export class RouterManager {
50
+ protected routes: Record<string, RouteType> = {};
51
+ protected options: RouterOptions;
52
+ public app: Application;
53
+ public router;
54
+ get basename() {
55
+ return this.router.basename;
56
+ }
57
+ get state() {
58
+ return this.router.state;
59
+ }
60
+ get navigate() {
61
+ return this.router.navigate;
62
+ }
63
+
64
+ constructor(options: RouterOptions = {}, app: Application) {
65
+ this.options = options;
66
+ this.app = app;
67
+ this.routes = options.routes || {};
68
+ }
69
+
70
+ /**
71
+ * @internal
72
+ */
73
+ getRoutesTree(): RouteObject[] {
74
+ type RouteTypeWithChildren = RouteType & { children?: RouteTypeWithChildren };
75
+ const routes: Record<string, RouteTypeWithChildren> = {};
76
+
77
+ /**
78
+ * { 'a': { name: '1' }, 'a.b': { name: '2' }, 'a.c': { name: '3' } };
79
+ * =>
80
+ * { a: { name: '1', children: { b: { name: '2' }, c: {name: '3'} } } }
81
+ */
82
+ for (const [name, route] of Object.entries(this.routes)) {
83
+ set(routes, name.split('.').join('.children.'), { ...get(routes, name.split('.').join('.children.')), ...route });
84
+ }
85
+
86
+ /**
87
+ * get RouteObject tree
88
+ *
89
+ * @example
90
+ * { a: { name: '1', children: { b: { name: '2' }, c: { children: { d: { name: '3' } } } } } }
91
+ * =>
92
+ * { name: '1', children: [{ name: '2' }, { name: '3' }] }
93
+ */
94
+ const buildRoutesTree = (routes: RouteTypeWithChildren): RouteObject[] => {
95
+ return Object.values(routes).reduce<RouteObject[]>((acc, item) => {
96
+ if (Object.keys(item).length === 1 && item.children) {
97
+ acc.push(...buildRoutesTree(item.children));
98
+ } else {
99
+ const { Component, element, children, ...reset } = item;
100
+ let ele = element;
101
+ if (Component) {
102
+ if (typeof Component === 'string') {
103
+ ele = this.app.renderComponent(Component);
104
+ } else {
105
+ ele = React.createElement(Component);
106
+ }
107
+ }
108
+ const res = {
109
+ ...reset,
110
+ element: ele,
111
+ children: children ? buildRoutesTree(children) : undefined,
112
+ } as RouteObject;
113
+ acc.push(res);
114
+ }
115
+ return acc;
116
+ }, []);
117
+ };
118
+
119
+ return buildRoutesTree(routes);
120
+ }
121
+
122
+ getRoutes() {
123
+ return this.routes;
124
+ }
125
+
126
+ setType(type: RouterOptions['type']) {
127
+ this.options.type = type;
128
+ }
129
+
130
+ getBasename() {
131
+ return this.options.basename;
132
+ }
133
+
134
+ setBasename(basename: string) {
135
+ this.options.basename = basename;
136
+ }
137
+
138
+ matchRoutes(pathname: string) {
139
+ const routes = Object.values(this.routes);
140
+ // @ts-ignore
141
+ return matchRoutes<RouteType>(routes, pathname, this.basename);
142
+ }
143
+
144
+ isSkippedAuthCheckRoute(pathname: string) {
145
+ const matchedRoutes = this.matchRoutes(pathname);
146
+ return matchedRoutes.some((match) => {
147
+ return match?.route?.skipAuthCheck === true;
148
+ });
149
+ }
150
+ /**
151
+ * @internal
152
+ */
153
+ getRouterComponent(children?: React.ReactNode) {
154
+ const { type = 'browser', ...opts } = this.options;
155
+
156
+ const routerCreators = {
157
+ hash: createHashRouter,
158
+ browser: createBrowserRouter,
159
+ memory: createMemoryRouter,
160
+ };
161
+
162
+ const routes = this.getRoutesTree();
163
+
164
+ const BaseLayoutContext = createContext<ComponentType>(null);
165
+
166
+ const Provider = () => {
167
+ const BaseLayout = useContext(BaseLayoutContext);
168
+ return (
169
+ <>
170
+ <RouterBridge app={this.app} />
171
+ <BaseLayout>
172
+ <Outlet />
173
+ {children}
174
+ </BaseLayout>
175
+ </>
176
+ );
177
+ };
178
+
179
+ // bubble up error to application error boundary
180
+ const ErrorElement = () => {
181
+ const error = useRouteError();
182
+ throw error;
183
+ };
184
+
185
+ this.router = routerCreators[type](
186
+ [
187
+ {
188
+ element: <Provider />,
189
+ errorElement: <ErrorElement />,
190
+ children: routes,
191
+ },
192
+ ],
193
+ opts,
194
+ );
195
+
196
+ const RenderRouter: React.FC<{ BaseLayout?: ComponentType }> = ({ BaseLayout = BlankComponent }) => {
197
+ return (
198
+ <BaseLayoutContext.Provider value={BaseLayout}>
199
+ <RouterContextCleaner>
200
+ <RouterProvider
201
+ future={{
202
+ v7_startTransition: true,
203
+ }}
204
+ router={this.router}
205
+ />
206
+ </RouterContextCleaner>
207
+ </BaseLayoutContext.Provider>
208
+ );
209
+ };
210
+
211
+ return RenderRouter;
212
+ }
213
+
214
+ add(name: string, route: RouteType) {
215
+ this.routes[name] = {
216
+ id: name,
217
+ ...route,
218
+ handle: {
219
+ path: route.path,
220
+ },
221
+ };
222
+ }
223
+
224
+ get(name: string) {
225
+ return this.routes[name];
226
+ }
227
+
228
+ has(name: string) {
229
+ return !!this.get(name);
230
+ }
231
+
232
+ remove(name: string) {
233
+ delete this.routes[name];
234
+ }
235
+ }
236
+
237
+ export function createRouterManager(options?: RouterOptions, app?: Application) {
238
+ return new RouterManager(options, app);
239
+ }