@nocobase/client-v2 2.1.0-beta.15 → 2.1.0-beta.16

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 (43) hide show
  1. package/es/Application.d.ts +124 -0
  2. package/es/MockApplication.d.ts +16 -0
  3. package/es/Plugin.d.ts +33 -0
  4. package/es/PluginManager.d.ts +46 -0
  5. package/es/PluginSettingsManager.d.ts +68 -0
  6. package/es/RouterManager.d.ts +69 -0
  7. package/es/WebSocketClient.d.ts +45 -0
  8. package/es/components/BlankComponent.d.ts +12 -0
  9. package/es/components/MainComponent.d.ts +10 -0
  10. package/es/components/RouterBridge.d.ts +13 -0
  11. package/es/components/RouterContextCleaner.d.ts +12 -0
  12. package/es/components/index.d.ts +10 -0
  13. package/es/context.d.ts +11 -0
  14. package/es/hooks/index.d.ts +11 -0
  15. package/es/hooks/useApp.d.ts +10 -0
  16. package/es/hooks/usePlugin.d.ts +11 -0
  17. package/es/hooks/useRouter.d.ts +9 -0
  18. package/es/index.d.ts +14 -0
  19. package/es/utils/globalDeps.d.ts +13 -0
  20. package/es/utils/index.d.ts +11 -0
  21. package/es/utils/remotePlugins.d.ts +44 -0
  22. package/es/utils/requirejs.d.ts +18 -0
  23. package/es/utils/types.d.ts +330 -0
  24. package/lib/Application.js +14 -4
  25. package/lib/PluginManager.js +1 -1
  26. package/lib/PluginSettingsManager.d.ts +1 -0
  27. package/lib/PluginSettingsManager.js +2 -1
  28. package/lib/RouterManager.d.ts +8 -0
  29. package/lib/RouterManager.js +35 -2
  30. package/lib/utils/globalDeps.d.ts +13 -0
  31. package/lib/utils/globalDeps.js +82 -0
  32. package/lib/utils/remotePlugins.js +2 -2
  33. package/package.json +5 -5
  34. package/src/Application.tsx +13 -4
  35. package/src/PluginManager.ts +1 -1
  36. package/src/PluginSettingsManager.ts +2 -0
  37. package/src/RouterManager.tsx +48 -2
  38. package/src/__tests__/app.test.tsx +55 -0
  39. package/src/__tests__/globalDeps.test.ts +26 -0
  40. package/src/__tests__/plugin.test.ts +61 -0
  41. package/src/__tests__/remotePlugins.test.ts +72 -0
  42. package/src/utils/globalDeps.ts +61 -0
  43. package/src/utils/remotePlugins.ts +2 -2
@@ -18,7 +18,7 @@ import {
18
18
  } from '@nocobase/flow-engine';
19
19
  import { APIClient, type APIClientOptions, getSubAppName } from '@nocobase/sdk';
20
20
  import { createInstance, type i18n as i18next } from 'i18next';
21
- import _ from 'lodash';
21
+ import { get, merge, set } from 'lodash-es';
22
22
  import React, { ComponentType, FC, ReactElement, ReactNode } from 'react';
23
23
  import { createRoot } from 'react-dom/client';
24
24
  import { I18nextProvider } from 'react-i18next';
@@ -30,6 +30,7 @@ import { type ComponentTypeAndString, RouterManager, type RouterOptions } from '
30
30
  import { WebSocketClient, type WebSocketClientOptions } from './WebSocketClient';
31
31
  import { BlankComponent } from './components';
32
32
  import { compose, normalizeContainer } from './utils';
33
+ import { defineGlobalDeps } from './utils/globalDeps';
33
34
  import type { RequireJS } from './utils/requirejs';
34
35
  import { getRequireJs } from './utils/requirejs';
35
36
 
@@ -139,10 +140,17 @@ export class Application {
139
140
  error: observable.ref,
140
141
  });
141
142
  this.devDynamicImport = options.devDynamicImport;
142
- this.components = _.merge(this.components, options.components);
143
+ this.components = merge(this.components, options.components);
143
144
  this.apiClient = new APIClient(options.apiClient);
144
145
  this.i18n = options.i18n || createInstance();
145
146
  this.router = new RouterManager(options.router, this);
147
+ if (typeof options.router?.basename === 'undefined') {
148
+ const publicPath = this.getPublicPath();
149
+ const basename = publicPath === '/' ? undefined : publicPath.replace(/\/$/, '');
150
+ if (basename) {
151
+ this.router.setBasename(basename);
152
+ }
153
+ }
146
154
  this.pluginManager = new PluginManager(options.plugins, options.loadRemotePlugins, this);
147
155
  this.flowEngine = new FlowEngine();
148
156
  this.flowEngine.registerModels({ ApplicationModel });
@@ -225,6 +233,7 @@ export class Application {
225
233
  return;
226
234
  }
227
235
  window['requirejs'] = this.requirejs = getRequireJs();
236
+ defineGlobalDeps(this.requirejs);
228
237
  window.define = this.requirejs.define;
229
238
  }
230
239
 
@@ -421,7 +430,7 @@ export class Application {
421
430
 
422
431
  // Component is a string, try to get it from this.components
423
432
  if (typeof Component === 'string') {
424
- const res = _.get(this.components, Component) as ComponentType<T>;
433
+ const res = get(this.components, Component) as ComponentType<T>;
425
434
  if (!res) {
426
435
  showError(`Component ${Component} not found`);
427
436
  return;
@@ -443,7 +452,7 @@ export class Application {
443
452
  console.error('Component must have a displayName or pass name as second argument');
444
453
  return;
445
454
  }
446
- _.set(this.components, componentName, component);
455
+ set(this.components, componentName, component);
447
456
  }
448
457
 
449
458
  addComponents(components: Record<string, ComponentType>) {
@@ -54,7 +54,7 @@ export class PluginManager {
54
54
  }
55
55
 
56
56
  private async initRemotePlugins() {
57
- const res = await this.app.apiClient.request({ url: 'pm:listEnabled' });
57
+ const res = await this.app.apiClient.request({ url: 'pm:listEnabledV2' });
58
58
  const pluginList: PluginData[] = res?.data?.data || [];
59
59
  const plugins = await getPlugins({
60
60
  requirejs: this.app.requirejs,
@@ -24,6 +24,7 @@ export interface PluginSettingOptions {
24
24
  * @default Outlet
25
25
  */
26
26
  Component?: RouteType['Component'];
27
+ componentLoader?: RouteType['componentLoader'];
27
28
  icon?: string;
28
29
  /**
29
30
  * sort, the smaller the number, the higher the priority
@@ -109,6 +110,7 @@ export class PluginSettingsManager {
109
110
  this.app.router.add(this.getRouteName(name), {
110
111
  path: this.getRoutePath(name),
111
112
  Component: this.settings[name].Component,
113
+ componentLoader: this.settings[name].componentLoader,
112
114
  });
113
115
  }
114
116
 
@@ -40,8 +40,13 @@ export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRo
40
40
  routes?: Record<string, RouteType>;
41
41
  };
42
42
  export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
43
+ export type ComponentLoaderResult =
44
+ | { default?: ComponentTypeAndString; Component?: ComponentTypeAndString }
45
+ | ComponentTypeAndString;
46
+ export type ComponentLoader = () => Promise<ComponentLoaderResult>;
43
47
  export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
44
48
  Component?: ComponentTypeAndString;
49
+ componentLoader?: ComponentLoader;
45
50
  skipAuthCheck?: boolean;
46
51
  }
47
52
  export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
@@ -67,6 +72,45 @@ export class RouterManager {
67
72
  this.routes = options.routes || {};
68
73
  }
69
74
 
75
+ protected resolveLoadedComponent(moduleOrComponent: ComponentLoaderResult): ComponentTypeAndString | undefined {
76
+ if (!moduleOrComponent) {
77
+ return undefined;
78
+ }
79
+ if (typeof moduleOrComponent === 'function' || typeof moduleOrComponent === 'string') {
80
+ return moduleOrComponent;
81
+ }
82
+ return moduleOrComponent.default || moduleOrComponent.Component;
83
+ }
84
+
85
+ protected createRouteLazyComponent(componentLoader: ComponentLoader): ComponentType {
86
+ const LazyComponent = React.lazy(() =>
87
+ componentLoader().then((moduleOrComponent) => {
88
+ const loadedComponent = this.resolveLoadedComponent(moduleOrComponent);
89
+ if (!loadedComponent) {
90
+ throw new Error('componentLoader must resolve to a React component or component module.');
91
+ }
92
+ if (typeof loadedComponent === 'string') {
93
+ const StringRouteComponent: ComponentType<any> = (props: Record<string, any>) =>
94
+ this.app.renderComponent(loadedComponent, props);
95
+ return {
96
+ default: StringRouteComponent,
97
+ };
98
+ }
99
+ return {
100
+ default: loadedComponent as ComponentType<any>,
101
+ };
102
+ }),
103
+ );
104
+
105
+ return function RouteLazyComponentWrapper(props: Record<string, any>) {
106
+ return (
107
+ <React.Suspense fallback={null}>
108
+ <LazyComponent {...props} />
109
+ </React.Suspense>
110
+ );
111
+ };
112
+ }
113
+
70
114
  /**
71
115
  * @internal
72
116
  */
@@ -96,9 +140,11 @@ export class RouterManager {
96
140
  if (Object.keys(item).length === 1 && item.children) {
97
141
  acc.push(...buildRoutesTree(item.children));
98
142
  } else {
99
- const { Component, element, children, ...reset } = item;
143
+ const { Component, componentLoader, element, children, ...reset } = item;
100
144
  let ele = element;
101
- if (Component) {
145
+ if (componentLoader) {
146
+ ele = React.createElement(this.createRouteLazyComponent(componentLoader));
147
+ } else if (Component) {
102
148
  if (typeof Component === 'string') {
103
149
  ele = this.app.renderComponent(Component);
104
150
  } else {
@@ -68,6 +68,61 @@ describe('app', () => {
68
68
  expect(screen.getByText('Hello Route')).toBeInTheDocument();
69
69
  });
70
70
 
71
+ it('should support router componentLoader lazy functionality', async () => {
72
+ class PluginHelloClient extends Plugin {
73
+ async load() {
74
+ this.router.add('root', {
75
+ path: '/',
76
+ componentLoader: async () => ({
77
+ default: () => <div>Hello Lazy Route</div>,
78
+ }),
79
+ });
80
+ }
81
+ }
82
+ const app = createMockClient({ plugins: [PluginHelloClient] });
83
+ await renderApp(app);
84
+ expect(await screen.findByText('Hello Lazy Route')).toBeInTheDocument();
85
+ });
86
+
87
+ it('should support publicPath basename for plugin routes', async () => {
88
+ class PluginHelloClient extends Plugin {
89
+ async load() {
90
+ this.router.add('demo.route', {
91
+ path: '/demo/app-info',
92
+ componentLoader: async () => ({
93
+ default: () => <div>Hello Basename Route</div>,
94
+ }),
95
+ });
96
+ }
97
+ }
98
+ const app = createMockClient({
99
+ publicPath: '/v2/',
100
+ plugins: [PluginHelloClient],
101
+ router: { type: 'memory', initialEntries: ['/v2/demo/app-info'] },
102
+ });
103
+ await renderApp(app);
104
+ expect(await screen.findByText('Hello Basename Route')).toBeInTheDocument();
105
+ });
106
+
107
+ it('should support plugin settings componentLoader lazy functionality', async () => {
108
+ class PluginHelloClient extends Plugin {
109
+ async load() {
110
+ this.pluginSettingsManager.add('demo', {
111
+ title: 'Demo',
112
+ componentLoader: async () => ({
113
+ default: () => <div>Hello Lazy Settings</div>,
114
+ }),
115
+ });
116
+ }
117
+ }
118
+ const app = createMockClient({
119
+ plugins: [PluginHelloClient],
120
+ router: { type: 'memory', initialEntries: ['/admin/settings/demo'] },
121
+ });
122
+ await renderApp(app);
123
+ expect(await screen.findByText('Hello Lazy Settings')).toBeInTheDocument();
124
+ });
125
+
71
126
  it('should show maintaining state', async () => {
72
127
  class PluginHelloClient extends Plugin {}
73
128
  const app = createMockClient({ plugins: [PluginHelloClient] });
@@ -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 { defineGlobalDeps } from '../utils/globalDeps';
11
+
12
+ describe('client-v2 defineGlobalDeps', () => {
13
+ it('should register shared AMD dependencies for remote plugins', () => {
14
+ const define = vi.fn();
15
+
16
+ defineGlobalDeps({
17
+ define,
18
+ } as any);
19
+
20
+ expect(define).toHaveBeenCalledWith('react', expect.any(Function));
21
+ expect(define).toHaveBeenCalledWith('react-router-dom', expect.any(Function));
22
+ expect(define).toHaveBeenCalledWith('@formily/react', expect.any(Function));
23
+ expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
24
+ expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
25
+ });
26
+ });
@@ -0,0 +1,61 @@
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 { createMockClient, Plugin } from '@nocobase/client-v2';
11
+
12
+ describe('PluginManager', () => {
13
+ it('should request remote plugins from pm:listEnabledV2', async () => {
14
+ const app = createMockClient({
15
+ loadRemotePlugins: true,
16
+ });
17
+ app.apiMock.onGet('pm:listEnabledV2').reply(200, { data: [] });
18
+
19
+ await app.load();
20
+
21
+ expect(app.apiMock.history.get).toHaveLength(1);
22
+ expect(app.apiMock.history.get[0]?.url).toBe('pm:listEnabledV2');
23
+ });
24
+
25
+ it('should not request remote plugins from pm:listEnabled', async () => {
26
+ const app = createMockClient({
27
+ loadRemotePlugins: true,
28
+ });
29
+ app.apiMock.onGet('pm:listEnabledV2').reply(200, { data: [] });
30
+
31
+ await app.load();
32
+
33
+ expect(app.apiMock.history.get.some((request) => request.url === 'pm:listEnabled')).toBe(false);
34
+ });
35
+
36
+ it('should define client-v2 module ids for dev plugins', async () => {
37
+ class DemoPlugin extends Plugin {}
38
+
39
+ const mockDefine: any = vi.fn();
40
+ window.define = mockDefine;
41
+
42
+ const app = createMockClient({
43
+ loadRemotePlugins: true,
44
+ devDynamicImport: vi.fn().mockResolvedValue({ default: DemoPlugin }) as any,
45
+ });
46
+ app.apiMock.onGet('pm:listEnabledV2').reply(200, {
47
+ data: [
48
+ {
49
+ name: '@nocobase/demo',
50
+ packageName: '@nocobase/demo',
51
+ url: 'https://demo.com/dist/client-v2/index.js',
52
+ },
53
+ ],
54
+ });
55
+
56
+ await app.load();
57
+
58
+ expect(mockDefine).toHaveBeenCalledWith('@nocobase/demo/client-v2', expect.any(Function));
59
+ expect(mockDefine).not.toHaveBeenCalledWith('@nocobase/demo/client', expect.any(Function));
60
+ });
61
+ });
@@ -0,0 +1,72 @@
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 { Plugin } from '../Plugin';
11
+ import { defineDevPlugins, definePluginClient, getPlugins } from '../utils/remotePlugins';
12
+
13
+ describe('client-v2 remotePlugins', () => {
14
+ afterEach(() => {
15
+ window.define = undefined;
16
+ });
17
+
18
+ it('should define dev plugins with /client-v2 module ids', () => {
19
+ class DemoPlugin extends Plugin {}
20
+
21
+ const mockDefine: any = vi.fn();
22
+ window.define = mockDefine;
23
+
24
+ defineDevPlugins({
25
+ '@nocobase/demo': DemoPlugin,
26
+ });
27
+
28
+ expect(mockDefine).toHaveBeenCalledWith('@nocobase/demo/client-v2', expect.any(Function));
29
+ });
30
+
31
+ it('should define remote plugin proxies with /client-v2 module ids', () => {
32
+ const mockDefine: any = vi.fn();
33
+ window.define = mockDefine;
34
+
35
+ definePluginClient('@nocobase/demo');
36
+
37
+ expect(mockDefine).toHaveBeenCalledWith(
38
+ '@nocobase/demo/client-v2',
39
+ ['exports', '@nocobase/demo'],
40
+ expect.any(Function),
41
+ );
42
+ });
43
+
44
+ it('should not define /client aliases when loading v2 plugins', async () => {
45
+ class DemoPlugin extends Plugin {}
46
+
47
+ const requirejs: any = {
48
+ requirejs: vi.fn(),
49
+ };
50
+ requirejs.requirejs.config = vi.fn();
51
+ requirejs.requirejs.requirejs = vi.fn();
52
+
53
+ const mockDefine: any = vi.fn();
54
+ window.define = mockDefine;
55
+
56
+ const plugins = await getPlugins({
57
+ requirejs,
58
+ pluginData: [
59
+ {
60
+ name: '@nocobase/demo',
61
+ packageName: '@nocobase/demo',
62
+ url: 'https://demo.com/dist/client-v2/index.js',
63
+ },
64
+ ] as any,
65
+ devDynamicImport: vi.fn().mockResolvedValue({ default: DemoPlugin }) as any,
66
+ });
67
+
68
+ expect(plugins).toEqual([['@nocobase/demo', DemoPlugin]]);
69
+ expect(mockDefine).toHaveBeenCalledWith('@nocobase/demo/client-v2', expect.any(Function));
70
+ expect(mockDefine).not.toHaveBeenCalledWith('@nocobase/demo/client', expect.any(Function));
71
+ });
72
+ });
@@ -0,0 +1,61 @@
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 * as antdCssinjs from '@ant-design/cssinjs';
11
+ import * as antdIcons from '@ant-design/icons';
12
+ import * as formilyCore from '@formily/core';
13
+ import * as formilyReact from '@formily/react';
14
+ import * as formilyReactive from '@formily/reactive';
15
+ import * as formilyShared from '@formily/shared';
16
+ import * as nocobaseFlowEngine from '@nocobase/flow-engine';
17
+ import * as antd from 'antd';
18
+ import * as i18next from 'i18next';
19
+ import React from 'react';
20
+ import ReactDOM from 'react-dom';
21
+ import * as reactI18next from 'react-i18next';
22
+ import * as ReactRouter from 'react-router';
23
+ import * as ReactRouterDom from 'react-router-dom';
24
+ import jsxRuntime from 'react/jsx-runtime';
25
+ import * as nocobaseClientV2 from '../index';
26
+
27
+ import type { RequireJS } from './requirejs';
28
+
29
+ /**
30
+ * @internal
31
+ */
32
+ export function defineGlobalDeps(requirejs: RequireJS) {
33
+ // react
34
+ requirejs.define('react', () => React);
35
+ requirejs.define('react-dom', () => ReactDOM);
36
+ requirejs.define('react/jsx-runtime', () => jsxRuntime);
37
+
38
+ // react-router
39
+ requirejs.define('react-router', () => ReactRouter);
40
+ requirejs.define('react-router-dom', () => ReactRouterDom);
41
+
42
+ // antd
43
+ requirejs.define('antd', () => antd);
44
+ requirejs.define('@ant-design/icons', () => antdIcons);
45
+ requirejs.define('@ant-design/cssinjs', () => antdCssinjs);
46
+
47
+ // i18next
48
+ requirejs.define('i18next', () => i18next);
49
+ requirejs.define('react-i18next', () => reactI18next);
50
+
51
+ // formily
52
+ requirejs.define('@formily/core', () => formilyCore);
53
+ requirejs.define('@formily/react', () => formilyReact);
54
+ requirejs.define('@formily/reactive', () => formilyReactive);
55
+ requirejs.define('@formily/shared', () => formilyShared);
56
+
57
+ // nocobase
58
+ requirejs.define('@nocobase/client-v2', () => nocobaseClientV2);
59
+ requirejs.define('@nocobase/client-v2/client-v2', () => nocobaseClientV2);
60
+ requirejs.define('@nocobase/flow-engine', () => nocobaseFlowEngine);
61
+ }
@@ -17,7 +17,7 @@ import type { RequireJS } from './requirejs';
17
17
  */
18
18
  export function defineDevPlugins(plugins: Record<string, typeof Plugin>) {
19
19
  Object.entries(plugins).forEach(([packageName, plugin]) => {
20
- window.define(`${packageName}/client`, () => plugin);
20
+ window.define(`${packageName}/client-v2`, () => plugin);
21
21
  });
22
22
  }
23
23
 
@@ -25,7 +25,7 @@ export function defineDevPlugins(plugins: Record<string, typeof Plugin>) {
25
25
  * @internal
26
26
  */
27
27
  export function definePluginClient(packageName: string) {
28
- window.define(`${packageName}/client`, ['exports', packageName], function (_exports: any, _pluginExports: any) {
28
+ window.define(`${packageName}/client-v2`, ['exports', packageName], function (_exports: any, _pluginExports: any) {
29
29
  Object.defineProperty(_exports, '__esModule', {
30
30
  value: true,
31
31
  });