@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,220 @@
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 { define, observable } from '@formily/reactive';
11
+ import { getSubAppName } from '@nocobase/sdk';
12
+ import { Application } from './Application';
13
+
14
+ export type WebSocketClientOptions = {
15
+ reconnectInterval?: number;
16
+ reconnectAttempts?: number;
17
+ pingInterval?: number;
18
+ url?: string;
19
+ basename?: string;
20
+ protocols?: string | string[];
21
+ onServerDown?: any;
22
+ };
23
+
24
+ export class WebSocketClient {
25
+ protected _ws: WebSocket;
26
+ protected _reconnectTimes = 0;
27
+ protected events = [];
28
+ protected options: WebSocketClientOptions;
29
+ app: Application;
30
+ enabled: boolean;
31
+ connected = false;
32
+ serverDown = false;
33
+ lastMessage = {};
34
+
35
+ constructor(options: WebSocketClientOptions | boolean) {
36
+ if (!options) {
37
+ this.enabled = false;
38
+ return;
39
+ }
40
+ this.options = options === true ? {} : options;
41
+ this.enabled = true;
42
+ define(this, {
43
+ serverDown: observable.ref,
44
+ connected: observable.ref,
45
+ lastMessage: observable.ref,
46
+ });
47
+ }
48
+
49
+ // TODO for plugin-multi-app
50
+ getSubAppName = (app: Application) => {
51
+ const publicPath = app.getPublicPath();
52
+ const pattern = `^${publicPath}${'_app'}/([^/]*)/`;
53
+ const match = location.pathname.match(new RegExp(pattern));
54
+ return match ? match[1] : null;
55
+ };
56
+
57
+ getURL() {
58
+ if (!this.app) {
59
+ return;
60
+ }
61
+ const apiBaseURL = this.app.getApiUrl();
62
+ if (!apiBaseURL) {
63
+ return;
64
+ }
65
+
66
+ let queryString = '';
67
+ if (this.getSubAppName(this.app)) {
68
+ queryString = `?_app=${this.getSubAppName(this.app)}`;
69
+ } else {
70
+ const subApp = getSubAppName(this.app.getPublicPath());
71
+ queryString = subApp ? `?__appName=${subApp}` : '';
72
+ }
73
+ const wsPath = this.options.basename || '/ws';
74
+ if (this.options.url) {
75
+ const url = new URL(this.options.url);
76
+ if (url.hostname === 'localhost') {
77
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
78
+ return `${protocol}://${location.hostname}:${url.port}${wsPath}${queryString}`;
79
+ }
80
+ return `${this.options.url}${queryString}`;
81
+ }
82
+ try {
83
+ const url = new URL(apiBaseURL);
84
+ return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}${wsPath}${queryString}`;
85
+ } catch (error) {
86
+ return `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}${wsPath}${queryString}`;
87
+ }
88
+ }
89
+
90
+ get reconnectAttempts() {
91
+ return this.options?.reconnectAttempts || 30;
92
+ }
93
+
94
+ get reconnectInterval() {
95
+ return this.options?.reconnectInterval || 1000;
96
+ }
97
+
98
+ get pingInterval() {
99
+ return this.options?.pingInterval || 30000;
100
+ }
101
+
102
+ get readyState() {
103
+ if (!this._ws) {
104
+ return -1;
105
+ }
106
+ return this._ws.readyState;
107
+ }
108
+
109
+ createWebSocket() {
110
+ return new WebSocket(this.getURL(), this.options.protocols);
111
+ }
112
+
113
+ connect() {
114
+ if (!this.enabled) {
115
+ return;
116
+ }
117
+ if (this._reconnectTimes === 0) {
118
+ console.log('[nocobase-ws]: connecting...');
119
+ }
120
+ if (this._reconnectTimes >= this.reconnectAttempts) {
121
+ return;
122
+ }
123
+ if (this.readyState === WebSocket.OPEN) {
124
+ return;
125
+ }
126
+ this._reconnectTimes++;
127
+ const ws = this.createWebSocket();
128
+ let pingIntervalTimer: any;
129
+
130
+ ws.onopen = () => {
131
+ console.log('[nocobase-ws]: connected.');
132
+ this.serverDown = false;
133
+ if (this._ws) {
134
+ this.removeAllListeners();
135
+ }
136
+ this._reconnectTimes = 0;
137
+ this._ws = ws;
138
+ for (const { type, listener, options } of this.events) {
139
+ this._ws.addEventListener(type, listener, options);
140
+ }
141
+ pingIntervalTimer = setInterval(() => this.send('ping'), this.pingInterval);
142
+ this.connected = true;
143
+ this.emit('open', {});
144
+ };
145
+ ws.onerror = async () => {
146
+ // setTimeout(() => this.connect(), this.reconnectInterval);
147
+ console.log('onerror', this.readyState, this._reconnectTimes);
148
+ };
149
+ ws.onclose = async (event) => {
150
+ setTimeout(() => this.connect(), this.reconnectInterval);
151
+ console.log('onclose', this.readyState, this._reconnectTimes, this.serverDown);
152
+ this.connected = false;
153
+ clearInterval(pingIntervalTimer);
154
+ // if (this._reconnectTimes >= Math.min(this.reconnectAttempts, 5)) {
155
+ // this.serverDown = true;
156
+ // this.emit('serverDown', event);
157
+ // }
158
+ };
159
+ }
160
+
161
+ reconnect() {
162
+ this._reconnectTimes = 0;
163
+ this.connect();
164
+ }
165
+
166
+ close() {
167
+ if (!this._ws) {
168
+ return;
169
+ }
170
+ this._reconnectTimes = this.reconnectAttempts;
171
+ return this._ws.close();
172
+ }
173
+
174
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
175
+ if (!this._ws) {
176
+ return;
177
+ }
178
+ return this._ws.send(data);
179
+ }
180
+
181
+ on(type: string, listener: any, options?: boolean | AddEventListenerOptions) {
182
+ this.events.push({ type, listener, options });
183
+ if (!this._ws) {
184
+ return;
185
+ }
186
+ this._ws.addEventListener(type, listener, options);
187
+ }
188
+
189
+ emit(type: string, args: any) {
190
+ for (const event of this.events) {
191
+ if (event.type === type) {
192
+ event.listener(args);
193
+ }
194
+ }
195
+ }
196
+
197
+ off(type: string, listener: any, options?: boolean | EventListenerOptions) {
198
+ let index = 0;
199
+ for (const event of this.events) {
200
+ if (event.type === type && event.listener === listener) {
201
+ this.events.splice(index, 1);
202
+ break;
203
+ }
204
+ index++;
205
+ }
206
+ if (!this._ws) {
207
+ return;
208
+ }
209
+ this._ws.removeEventListener(type, listener, options);
210
+ }
211
+
212
+ removeAllListeners() {
213
+ if (!this._ws) {
214
+ return;
215
+ }
216
+ for (const { type, listener, options } of this.events) {
217
+ this._ws.removeEventListener(type, listener, options);
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,141 @@
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 { Application, createMockClient, Plugin } from '@nocobase/client-v2';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { act } from 'react-dom/test-utils';
14
+
15
+ const renderApp = async (app: Application) => {
16
+ const Root = app.getRootComponent();
17
+ render(<Root />);
18
+ expect(screen.getByText('Loading')).toBeInTheDocument();
19
+ await waitFor(() => expect(screen.queryByText('Loading')).not.toBeInTheDocument());
20
+ return Root;
21
+ };
22
+
23
+ describe('app', () => {
24
+ it('should mount the app and display "Not Found"', async () => {
25
+ const app = createMockClient();
26
+ const element = document.createElement('div');
27
+ act(() => app.mount(element));
28
+ expect(element.textContent).toBe('Loading');
29
+ await waitFor(() => expect(element.textContent).not.toBe('Loading'));
30
+ expect(element.textContent).toBe('Not Found');
31
+ });
32
+
33
+ it('should render default "Not Found" view', async () => {
34
+ const app = createMockClient();
35
+ await renderApp(app);
36
+ expect(screen.getByText('Not Found')).toBeInTheDocument();
37
+ });
38
+
39
+ it('should render custom "Not Found" component', async () => {
40
+ class PluginHelloClient extends Plugin {}
41
+ const app = createMockClient({
42
+ plugins: [PluginHelloClient],
43
+ components: { AppNotFound: () => <div>Not Found2</div> },
44
+ });
45
+ await renderApp(app);
46
+ expect(screen.getByText('Not Found2')).toBeInTheDocument();
47
+ });
48
+
49
+ it('should support app provider functionality', async () => {
50
+ class PluginHelloClient extends Plugin {
51
+ async load() {
52
+ this.app.use(() => <div>Hello Provider</div>);
53
+ }
54
+ }
55
+ const app = createMockClient({ plugins: [PluginHelloClient] });
56
+ await renderApp(app);
57
+ expect(screen.getByText('Hello Provider')).toBeInTheDocument();
58
+ });
59
+
60
+ it('should support router functionality', async () => {
61
+ class PluginHelloClient extends Plugin {
62
+ async load() {
63
+ this.router.add('root', { path: '/', Component: () => <div>Hello Route</div> });
64
+ }
65
+ }
66
+ const app = createMockClient({ plugins: [PluginHelloClient] });
67
+ await renderApp(app);
68
+ expect(screen.getByText('Hello Route')).toBeInTheDocument();
69
+ });
70
+
71
+ it('should show maintaining state', async () => {
72
+ class PluginHelloClient extends Plugin {}
73
+ const app = createMockClient({ plugins: [PluginHelloClient] });
74
+ app.maintained = false;
75
+ app.maintaining = true;
76
+ await renderApp(app);
77
+ expect(screen.getByText('Maintaining')).toBeInTheDocument();
78
+ });
79
+
80
+ it('should show maintained dialog state', async () => {
81
+ class PluginHelloClient extends Plugin {}
82
+ const app = createMockClient({ plugins: [PluginHelloClient] });
83
+ app.maintained = true;
84
+ app.maintaining = true;
85
+ await renderApp(app);
86
+ expect(screen.getByText('Maintaining Dialog')).toBeInTheDocument();
87
+ });
88
+
89
+ it('should handle long loading state gracefully', async () => {
90
+ class PluginHelloClient extends Plugin {
91
+ async load() {
92
+ await new Promise((resolve) => setTimeout(resolve, 100));
93
+ }
94
+ }
95
+ const app = createMockClient({ plugins: [PluginHelloClient] });
96
+ await renderApp(app);
97
+ });
98
+
99
+ it('should handle WebSocket maintaining and running states', async () => {
100
+ const originalLocation = window.location;
101
+ const reloadMock = vi.fn();
102
+ Object.defineProperty(window, 'location', { value: { reload: reloadMock } });
103
+
104
+ class PluginHelloClient extends Plugin {
105
+ async load() {
106
+ this.router.add('root', { path: '/', Component: () => <div>Hello</div> });
107
+ this.app.ws.emit('message', {
108
+ type: 'maintaining',
109
+ payload: { code: 'APP_ERROR', message: 'maintaining error message' },
110
+ });
111
+ }
112
+ }
113
+
114
+ const app = createMockClient({
115
+ plugins: [PluginHelloClient],
116
+ ws: { url: 'ws://localhost:3000/ws' },
117
+ });
118
+
119
+ await renderApp(app);
120
+ expect(screen.getByText('Maintaining')).toBeInTheDocument();
121
+
122
+ app.ws.emit('message', {
123
+ type: 'maintaining',
124
+ payload: { code: 'APP_RUNNING', message: 'app running message' },
125
+ });
126
+ await waitFor(() => expect(screen.queryByText('Maintaining')).not.toBeInTheDocument());
127
+ expect(screen.getByText('Hello')).toBeInTheDocument();
128
+ expect(reloadMock).toHaveBeenCalled();
129
+ });
130
+
131
+ it('should handle plugin load error gracefully', async () => {
132
+ class PluginHelloClient extends Plugin {
133
+ async load() {
134
+ throw new Error('plugin load error');
135
+ }
136
+ }
137
+ const app = createMockClient({ plugins: [PluginHelloClient] });
138
+ await renderApp(app);
139
+ expect(screen.getByText('plugin load error')).toBeInTheDocument();
140
+ });
141
+ });
@@ -0,0 +1,12 @@
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 React, { FC, ReactNode } from 'react';
11
+
12
+ export const BlankComponent: FC<{ children?: ReactNode }> = ({ children }) => <>{children}</>;
@@ -0,0 +1,20 @@
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 React, { useMemo } from 'react';
11
+ import { useApp } from '../hooks';
12
+
13
+ export const MainComponent = React.memo(({ children }) => {
14
+ const app = useApp();
15
+ const Router = useMemo(() => app.router.getRouterComponent(children), [app]);
16
+ const Providers = useMemo(() => app.getComposeProviders(), [app]);
17
+ return <Router BaseLayout={Providers} />;
18
+ });
19
+
20
+ MainComponent.displayName = 'MainComponent';
@@ -0,0 +1,38 @@
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 { observable } from '@formily/reactive';
11
+ import { useEffect, useMemo } from 'react';
12
+ import { useLocation, useMatch, useMatches, useParams } from 'react-router-dom';
13
+ import { Application } from '../Application';
14
+
15
+ export function useRouterSync(app: Application) {
16
+ const params = useParams();
17
+ const location = useLocation();
18
+ const matches = useMatches();
19
+ const engine = app.flowEngine;
20
+ useEffect(() => {
21
+ const last = matches[matches.length - 1];
22
+ if (!last) return;
23
+ engine.context['_observableCache']['route'] = {
24
+ name: last.id,
25
+ pathname: last.pathname,
26
+ path: last.handle?.['path'] || null,
27
+ params,
28
+ };
29
+ }, [engine.context, params, matches]);
30
+ useEffect(() => {
31
+ engine.context['_observableCache']['location'] = location;
32
+ }, [engine.context, location]);
33
+ }
34
+
35
+ export function RouterBridge({ app }) {
36
+ useRouterSync(app);
37
+ return null;
38
+ }
@@ -0,0 +1,26 @@
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 React, { FC, ReactNode } from 'react';
11
+ import { UNSAFE_LocationContext, UNSAFE_RouteContext } from 'react-router-dom';
12
+
13
+ export const RouterContextCleaner: FC<{ children?: ReactNode }> = React.memo((props) => {
14
+ return (
15
+ <UNSAFE_RouteContext.Provider
16
+ value={{
17
+ outlet: null,
18
+ matches: [],
19
+ isDataRoute: false,
20
+ }}
21
+ >
22
+ <UNSAFE_LocationContext.Provider value={null}>{props.children}</UNSAFE_LocationContext.Provider>
23
+ </UNSAFE_RouteContext.Provider>
24
+ );
25
+ });
26
+ RouterContextCleaner.displayName = 'RouterContextCleaner';
@@ -0,0 +1,11 @@
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
+ export * from './BlankComponent';
11
+ export * from './RouterContextCleaner';
package/src/context.ts ADDED
@@ -0,0 +1,14 @@
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 { createContext } from 'react';
11
+ import type { Application } from './Application';
12
+
13
+ export const ApplicationContext = createContext<Application>(null);
14
+ ApplicationContext.displayName = 'ApplicationContext';
@@ -0,0 +1,12 @@
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
+ export * from './useApp';
11
+ export * from './usePlugin';
12
+ export * from './useRouter';
@@ -0,0 +1,16 @@
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 { useFlowEngineContext } from '@nocobase/flow-engine';
11
+ import type { Application } from '../Application';
12
+
13
+ export const useApp = () => {
14
+ const ctx = useFlowEngineContext();
15
+ return ctx.app as Application;
16
+ };
@@ -0,0 +1,17 @@
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
+ import { Plugin } from '../Plugin';
10
+ import { useApp } from './useApp';
11
+
12
+ export function usePlugin<T extends typeof Plugin = any>(plugin: T): InstanceType<T>;
13
+ export function usePlugin<T extends {}>(name: string): T;
14
+ export function usePlugin(name: any) {
15
+ const app = useApp();
16
+ return app.pm.get(name);
17
+ }
@@ -0,0 +1,15 @@
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 { useApp } from './useApp';
11
+
12
+ export const useRouter = () => {
13
+ const app = useApp();
14
+ return app.router;
15
+ };
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
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
+ export * from './Application';
11
+ export * from './components';
12
+ export * from './context';
13
+ export * from './MockApplication';
14
+ export * from './Plugin';
15
+ export * from './WebSocketClient';
@@ -0,0 +1,48 @@
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 React, { ComponentType, FC } from 'react';
11
+ import { BlankComponent } from '../components';
12
+
13
+ export function normalizeContainer(container: Element | ShadowRoot | string): Element | null {
14
+ if (!container) {
15
+ console.warn(`Failed to mount app: mount target should not be null or undefined.`);
16
+ return null;
17
+ }
18
+
19
+ if (typeof container === 'string') {
20
+ const res = document.querySelector(container);
21
+ if (!res) {
22
+ console.warn(`Failed to mount app: mount target selector "${container}" returned null.`);
23
+ }
24
+ return res;
25
+ }
26
+ if (window.ShadowRoot && container instanceof window.ShadowRoot && container.mode === 'closed') {
27
+ console.warn(`mounting on a ShadowRoot with \`{mode: "closed"}\` may lead to unpredictable bugs`);
28
+ }
29
+ return container as any;
30
+ }
31
+
32
+ export const compose = (...components: [ComponentType, any][]) => {
33
+ const Component = components.reduce<ComponentType>((Parent, child) => {
34
+ const [Child, childProps] = child;
35
+ const ComposeComponent: FC = ({ children }) => (
36
+ <Parent>
37
+ <Child {...childProps}>{children}</Child>
38
+ </Parent>
39
+ );
40
+ ComposeComponent.displayName = `compose(${Child.displayName || Child.name})`;
41
+ return ComposeComponent;
42
+ }, BlankComponent);
43
+
44
+ return (LastChild?: ComponentType) =>
45
+ ((props?: any) => {
46
+ return <Component>{LastChild && <LastChild {...props} />}</Component>;
47
+ }) as FC;
48
+ };