@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36

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 (76) hide show
  1. package/es/BaseApplication.d.ts +7 -1
  2. package/es/PluginManager.d.ts +2 -0
  3. package/es/components/PoweredBy.d.ts +18 -0
  4. package/es/components/SwitchLanguage.d.ts +11 -0
  5. package/es/components/form/DialogFormLayout.d.ts +75 -0
  6. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  7. package/es/components/form/PasswordInput.d.ts +40 -0
  8. package/es/components/form/RemoteSelect.d.ts +79 -0
  9. package/es/components/form/index.d.ts +3 -0
  10. package/es/components/form/table/styles.d.ts +10 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  13. package/es/flow/models/base/GridModel.d.ts +2 -0
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  15. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  16. package/es/flow-compat/passwordUtils.d.ts +1 -1
  17. package/es/hooks/index.d.ts +2 -0
  18. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  19. package/es/index.mjs +117 -105
  20. package/es/json-logic/globalOperators.d.ts +11 -0
  21. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  22. package/es/utils/appVersionHTML.d.ts +10 -0
  23. package/es/utils/globalDeps.d.ts +7 -0
  24. package/es/utils/index.d.ts +1 -0
  25. package/es/utils/remotePlugins.d.ts +4 -1
  26. package/lib/index.js +120 -108
  27. package/package.json +7 -6
  28. package/src/BaseApplication.tsx +11 -3
  29. package/src/PluginManager.ts +2 -0
  30. package/src/PluginSettingsManager.ts +2 -1
  31. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  32. package/src/__tests__/PoweredBy.test.tsx +130 -0
  33. package/src/__tests__/app.test.tsx +39 -0
  34. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  35. package/src/__tests__/remotePlugins.test.ts +203 -0
  36. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  37. package/src/components/PoweredBy.tsx +71 -0
  38. package/src/components/README.md +314 -0
  39. package/src/components/README.zh-CN.md +312 -0
  40. package/src/components/SwitchLanguage.tsx +48 -0
  41. package/src/components/form/DialogFormLayout.tsx +111 -0
  42. package/src/components/form/DrawerFormLayout.tsx +13 -32
  43. package/src/components/form/PasswordInput.tsx +211 -0
  44. package/src/components/form/RemoteSelect.tsx +137 -0
  45. package/src/components/form/index.tsx +3 -0
  46. package/src/components/form/table/Table.tsx +2 -1
  47. package/src/components/form/table/styles.ts +19 -0
  48. package/src/components/index.ts +2 -0
  49. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  50. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  51. package/src/flow/actions/dataScope.tsx +3 -0
  52. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  54. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  55. package/src/flow/components/BlockItemCard.tsx +2 -2
  56. package/src/flow/models/base/ActionModel.tsx +8 -7
  57. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  58. package/src/flow/models/base/GridModel.tsx +93 -36
  59. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  60. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  63. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  64. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  65. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  66. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  68. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  69. package/src/hooks/index.ts +2 -0
  70. package/src/hooks/useCurrentAppInfo.ts +36 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  73. package/src/utils/appVersionHTML.ts +28 -0
  74. package/src/utils/globalDeps.ts +47 -31
  75. package/src/utils/index.tsx +2 -0
  76. package/src/utils/remotePlugins.ts +119 -13
@@ -7,11 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createCollectionContextMeta } from '@nocobase/flow-engine';
11
- import React, { createContext, type FC, useEffect, useRef, useState } from 'react';
12
- import { Navigate, Outlet, useLocation } from 'react-router-dom';
10
+ import { createCollectionContextMeta, useFlowEngine } from '@nocobase/flow-engine';
11
+ import React, { createContext, type FC, useContext, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
13
+ import { useACLRoleContext } from '../acl';
13
14
  import type { Application } from '../Application';
14
- import { getCurrentV2RedirectPath, getDefaultV2AdminRedirectPath, redirectToV2Signin } from '../authRedirect';
15
+ import { getCurrentV2RedirectPath, getDefaultV2AdminRedirectPath } from '../authRedirect';
15
16
  import { AppNotFound } from '../components';
16
17
  import { PluginFlowEngine } from '../flow';
17
18
  import { AdminLayoutMenuItemModel, AdminLayoutModel } from '../flow/admin-shell/admin-layout';
@@ -20,13 +21,18 @@ import { Plugin } from '../Plugin';
20
21
  import { AdminSettingsLayoutModel } from '../settings-center';
21
22
  import { LocalePlugin } from './plugins/LocalePlugin';
22
23
 
23
- type CurrentUserState = {
24
+ export type CurrentUserState = {
24
25
  data?: {
25
26
  data?: any;
26
27
  };
27
28
  loading: boolean;
28
29
  };
29
30
 
31
+ export type CurrentRoleOption = {
32
+ name: string;
33
+ title: string;
34
+ };
35
+
30
36
  const AUTH_ROUTE_PREFIXES = ['/signin', '/signup', '/forgot-password', '/reset-password'];
31
37
 
32
38
  function removeBasename(pathname: string, basename?: string) {
@@ -50,9 +56,42 @@ function isAdminRuntimeRoute(pathname: string, basename?: string) {
50
56
  return normalizedPathname === '/admin' || normalizedPathname.startsWith('/admin/');
51
57
  }
52
58
 
53
- const CurrentUserContext = createContext<CurrentUserState | null>(null);
59
+ export const CurrentUserContext = createContext<CurrentUserState | null>(null);
54
60
  CurrentUserContext.displayName = 'CurrentUserContext';
55
61
 
62
+ export function useCurrentUserContext() {
63
+ return useContext(CurrentUserContext);
64
+ }
65
+
66
+ /**
67
+ * 返回当前用户在 v2 应用上下文中可选的角色列表,等价于 v1 `useCurrentRoles`:
68
+ * 从 FlowEngine 全局上下文 `engine.context.user.roles` 派生(CurrentUserProvider 在
69
+ * `/auth:check` 成功后通过 `defineProperty('user', { value })` 写入),按需追加匿名角色,
70
+ * 并去掉合并角色 `__union__`。v2 中角色 title 可能含有 `{{t('...')}}` 模板,因此用
71
+ * flowEngine.context.t 解析。
72
+ *
73
+ * 不读 React `CurrentUserContext`:FlowEngine 的 dialog/drawer/popover 内容通过 `ctx.viewer`
74
+ * 渲染到独立的 ElementsHolder,部分场景会脱离原 Provider 树;FlowEngine 全局上下文是同一份
75
+ * 数据但不受 React 树位置影响。
76
+ */
77
+ export function useCurrentRoles(): CurrentRoleOption[] {
78
+ const { allowAnonymous } = useACLRoleContext();
79
+ const engine = useFlowEngine();
80
+ const rolesRaw = engine?.context?.user?.roles as Array<{ name: string; title?: string }> | undefined;
81
+
82
+ return useMemo(() => {
83
+ const compile = (value: string | undefined): string =>
84
+ value == null ? '' : engine?.context?.t ? engine.context.t(value) : value;
85
+ const roles: CurrentRoleOption[] = (rolesRaw || [])
86
+ .filter((role) => role?.name !== '__union__')
87
+ .map((role) => ({ name: role.name, title: compile(role.title) }));
88
+ if (allowAnonymous) {
89
+ roles.push({ name: 'anonymous', title: 'Anonymous' });
90
+ }
91
+ return roles;
92
+ }, [allowAnonymous, engine, rolesRaw]);
93
+ }
94
+
56
95
  const DataSourceBootstrapProvider: FC = ({ children }) => {
57
96
  const app = useApp();
58
97
  const location = useLocation();
@@ -115,6 +154,7 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
115
154
  const CurrentUserProvider: FC = ({ children }) => {
116
155
  const app = useApp();
117
156
  const location = useLocation();
157
+ const navigate = useNavigate();
118
158
  const [state, setState] = useState<CurrentUserState>({ loading: true });
119
159
  const pathnameRef = useRef(location.pathname);
120
160
  pathnameRef.current = location.pathname;
@@ -143,8 +183,23 @@ const CurrentUserProvider: FC = ({ children }) => {
143
183
  });
144
184
 
145
185
  const user = res?.data?.data;
186
+ // 服务端通过 `{ code: 302, redirect }` 通知客户端先去某个中间页(例如 2FA 验证页)。
187
+ // 这类响应没有 user.id,但也不能视为未登录——否则会和处理 302 的全局响应拦截器
188
+ // (例如 plugin-two-factor-authentication 注册的那一个)竞态,而 `window.location.replace`
189
+ // 会覆盖更早发出的 `window.location.href`,把用户错误地弹回登录页。让响应拦截器接管跳转。
190
+ if (user?.code === 302) {
191
+ if (mounted) {
192
+ setState({ loading: false });
193
+ }
194
+ return;
195
+ }
146
196
  if (user?.id == null) {
147
- redirectToV2Signin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
197
+ // 用 react-router navigate (虚拟跳转)而不是 location.replace, 这样如果有其他响应拦截器
198
+ // 已经发起了 window.location.href 整页跳转(例如 2FA 插件接收到服务端 302 重定向),
199
+ // 真实跳转可以胜出 navigate, 不会被这里的 signin 重定向覆盖。
200
+ navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
201
+ replace: true,
202
+ });
148
203
  return;
149
204
  }
150
205
 
@@ -169,7 +224,9 @@ const CurrentUserProvider: FC = ({ children }) => {
169
224
  } catch (error: any) {
170
225
  const isAuthError = error?.response?.status === 401 || error?.status === 401;
171
226
  if (isAuthError) {
172
- redirectToV2Signin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
227
+ navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
228
+ replace: true,
229
+ });
173
230
  return;
174
231
  }
175
232
  if (mounted) {
@@ -184,7 +241,7 @@ const CurrentUserProvider: FC = ({ children }) => {
184
241
  return () => {
185
242
  mounted = false;
186
243
  };
187
- }, [app]);
244
+ }, [app, navigate]);
188
245
 
189
246
  if (state.loading) {
190
247
  return app.renderComponent('AppSpin');
@@ -196,15 +253,12 @@ const CurrentUserProvider: FC = ({ children }) => {
196
253
  const RootRedirect: FC = () => {
197
254
  const app = useApp();
198
255
  const hasToken = !!app?.apiClient?.auth?.token;
199
-
200
- useEffect(() => {
201
- if (!hasToken) {
202
- redirectToV2Signin(app, getDefaultV2AdminRedirectPath(app), { replace: true });
203
- }
204
- }, [app, hasToken]);
256
+ const targetPath = getDefaultV2AdminRedirectPath(app);
205
257
 
206
258
  if (!hasToken) {
207
- return app.renderComponent('AppSpin');
259
+ // 用 react-router <Navigate /> 而非 location.replace, 避免覆盖同时段其它响应拦截器
260
+ // 触发的 window.location.href (例如 2FA 接收到服务端 302 时设置的整页跳转)。
261
+ return <Navigate replace to={`/signin?redirect=${encodeURIComponent(targetPath)}`} />;
208
262
  }
209
263
 
210
264
  return <Navigate replace to="/admin" />;
@@ -0,0 +1,28 @@
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
+ const htmlEscapeMap: Record<string, string> = {
11
+ '&': '&amp;',
12
+ '<': '&lt;',
13
+ '>': '&gt;',
14
+ '"': '&quot;',
15
+ "'": '&#39;',
16
+ };
17
+
18
+ export function escapeHTML(value: string) {
19
+ return value.replace(/[&<>"']/g, (matched) => htmlEscapeMap[matched]);
20
+ }
21
+
22
+ export function getAppVersionHTML(version: unknown) {
23
+ if (version === null || typeof version === 'undefined' || version === '') {
24
+ return '';
25
+ }
26
+
27
+ return `<span class="nb-app-version">v${escapeHTML(String(version))}</span>`;
28
+ }
@@ -37,53 +37,69 @@ import * as dndKitCore from '@dnd-kit/core';
37
37
  import * as dndKitSortable from '@dnd-kit/sortable';
38
38
  import type { RequireJS } from './requirejs';
39
39
 
40
+ declare global {
41
+ interface Window {
42
+ __nocobase_app_dev__?: boolean;
43
+ __nocobase_app_dev_deps__?: Record<string, unknown>;
44
+ __nocobase_app_dev_plugins__?: Record<string, unknown>;
45
+ }
46
+ }
47
+
48
+ function defineGlobalDep(requirejs: RequireJS, name: string, value: unknown) {
49
+ requirejs.define(name, () => value);
50
+ if (window.__nocobase_app_dev__) {
51
+ window.__nocobase_app_dev_deps__ = window.__nocobase_app_dev_deps__ || {};
52
+ window.__nocobase_app_dev_deps__[name] = value;
53
+ }
54
+ }
55
+
40
56
  /**
41
57
  * @internal
42
58
  */
43
59
  export function defineGlobalDeps(requirejs: RequireJS) {
44
60
  // react
45
- requirejs.define('react', () => React);
46
- requirejs.define('react-dom', () => ReactDOM);
47
- requirejs.define('react/jsx-runtime', () => jsxRuntime);
61
+ defineGlobalDep(requirejs, 'react', React);
62
+ defineGlobalDep(requirejs, 'react-dom', ReactDOM);
63
+ defineGlobalDep(requirejs, 'react/jsx-runtime', jsxRuntime);
48
64
 
49
65
  // react-router
50
- requirejs.define('react-router', () => ReactRouter);
51
- requirejs.define('react-router-dom', () => ReactRouterDom);
66
+ defineGlobalDep(requirejs, 'react-router', ReactRouter);
67
+ defineGlobalDep(requirejs, 'react-router-dom', ReactRouterDom);
52
68
 
53
69
  // antd
54
- requirejs.define('antd', () => antd);
55
- requirejs.define('@ant-design/icons', () => antdIcons);
56
- requirejs.define('@ant-design/cssinjs', () => antdCssinjs);
57
- requirejs.define('antd-style', () => antdStyle);
70
+ defineGlobalDep(requirejs, 'antd', antd);
71
+ defineGlobalDep(requirejs, '@ant-design/icons', antdIcons);
72
+ defineGlobalDep(requirejs, '@ant-design/cssinjs', antdCssinjs);
73
+ defineGlobalDep(requirejs, 'antd-style', antdStyle);
58
74
 
59
75
  // i18next
60
- requirejs.define('i18next', () => i18next);
61
- requirejs.define('react-i18next', () => reactI18next);
76
+ defineGlobalDep(requirejs, 'i18next', i18next);
77
+ defineGlobalDep(requirejs, 'react-i18next', reactI18next);
62
78
 
63
79
  // formily
64
- requirejs.define('@formily/antd-v5', () => formilyAntdV5);
65
- requirejs.define('@formily/core', () => formilyCore);
66
- requirejs.define('@formily/react', () => formilyReact);
67
- requirejs.define('@formily/reactive', () => formilyReactive);
68
- requirejs.define('@formily/shared', () => formilyShared);
80
+ defineGlobalDep(requirejs, '@formily/antd-v5', formilyAntdV5);
81
+ defineGlobalDep(requirejs, '@formily/core', formilyCore);
82
+ defineGlobalDep(requirejs, '@formily/react', formilyReact);
83
+ defineGlobalDep(requirejs, '@formily/reactive', formilyReactive);
84
+ defineGlobalDep(requirejs, '@formily/shared', formilyShared);
69
85
 
70
86
  // nocobase
71
- requirejs.define('@nocobase/utils', () => nocobaseClientUtils);
72
- requirejs.define('@nocobase/utils/client', () => nocobaseClientUtils);
73
- requirejs.define('@nocobase/client-v2', () => nocobaseClientV2);
74
- requirejs.define('@nocobase/client-v2/client-v2', () => nocobaseClientV2);
75
- requirejs.define('@nocobase/flow-engine', () => nocobaseFlowEngine);
76
- requirejs.define('@nocobase/evaluators', () => nocobaseEvaluators);
77
- requirejs.define('@nocobase/evaluators/client', () => nocobaseEvaluators);
87
+ defineGlobalDep(requirejs, '@nocobase/utils', nocobaseClientUtils);
88
+ defineGlobalDep(requirejs, '@nocobase/utils/client', nocobaseClientUtils);
89
+ defineGlobalDep(requirejs, '@nocobase/client-v2', nocobaseClientV2);
90
+ defineGlobalDep(requirejs, '@nocobase/client-v2/client-v2', nocobaseClientV2);
91
+ defineGlobalDep(requirejs, '@nocobase/flow-engine', nocobaseFlowEngine);
92
+ defineGlobalDep(requirejs, '@nocobase/evaluators', nocobaseEvaluators);
93
+ defineGlobalDep(requirejs, '@nocobase/evaluators/client', nocobaseEvaluators);
78
94
 
79
- requirejs.define('@dnd-kit/core', () => dndKitCore);
80
- requirejs.define('@dnd-kit/sortable', () => dndKitSortable);
95
+ defineGlobalDep(requirejs, '@dnd-kit/core', dndKitCore);
96
+ defineGlobalDep(requirejs, '@dnd-kit/sortable', dndKitSortable);
81
97
 
82
98
  // utils
83
- requirejs.define('ahooks', () => ahooks);
84
- requirejs.define('axios', () => axios);
85
- requirejs.define('dayjs', () => dayjs);
86
- requirejs.define('lodash', () => lodash);
87
- requirejs.define('@emotion/css', () => emotionCss);
88
- requirejs.define('file-saver', () => FileSaver);
99
+ defineGlobalDep(requirejs, 'ahooks', ahooks);
100
+ defineGlobalDep(requirejs, 'axios', axios);
101
+ defineGlobalDep(requirejs, 'dayjs', dayjs);
102
+ defineGlobalDep(requirejs, 'lodash', lodash);
103
+ defineGlobalDep(requirejs, '@emotion/css', emotionCss);
104
+ defineGlobalDep(requirejs, 'file-saver', FileSaver);
89
105
  }
@@ -10,6 +10,8 @@
10
10
  import React, { ComponentType, FC } from 'react';
11
11
  import { BlankComponent } from '../components';
12
12
 
13
+ export * from './appVersionHTML';
14
+
13
15
  export function normalizeContainer(container: Element | ShadowRoot | string): Element | null {
14
16
  if (!container) {
15
17
  console.warn(`Failed to mount app: mount target should not be null or undefined.`);
@@ -12,16 +12,38 @@ import type { PluginClass } from '../PluginManager';
12
12
  import type { PluginData } from '../PluginManager';
13
13
  import type { RequireJS } from './requirejs';
14
14
 
15
+ type RemotePluginModule = PluginClass | ({ default?: PluginClass } & Record<string, unknown>);
16
+
15
17
  function getClientV2ModuleId(packageName: string) {
16
18
  return `${packageName}/client-v2`;
17
19
  }
18
20
 
21
+ function getPluginClass(pluginModule: RemotePluginModule): PluginClass {
22
+ const defaultPlugin = 'default' in pluginModule ? pluginModule.default : undefined;
23
+ return defaultPlugin || (pluginModule as PluginClass);
24
+ }
25
+
26
+ function defineAppDevPluginModule(moduleId: string, pluginModule: RemotePluginModule) {
27
+ window.__nocobase_app_dev_plugins__ = window.__nocobase_app_dev_plugins__ || {};
28
+ window.__nocobase_app_dev_plugins__[moduleId] = pluginModule;
29
+ }
30
+
19
31
  /**
20
32
  * @internal
21
33
  */
22
- export function defineDevPlugins(plugins: Record<string, PluginClass>) {
23
- Object.entries(plugins).forEach(([packageName, plugin]) => {
24
- window.define(getClientV2ModuleId(packageName), () => plugin);
34
+ export function defineDevPlugins(plugins: Record<string, RemotePluginModule>) {
35
+ Object.entries(plugins).forEach(([packageName, pluginModule]) => {
36
+ const moduleId = getClientV2ModuleId(packageName);
37
+ window.define(moduleId, () => pluginModule);
38
+ defineAppDevPluginModule(moduleId, pluginModule);
39
+ });
40
+ }
41
+
42
+ function defineDevPluginModules(plugins: Record<string, RemotePluginModule>) {
43
+ Object.entries(plugins).forEach(([packageName, pluginModule]) => {
44
+ const moduleId = getClientV2ModuleId(packageName);
45
+ window.define(moduleId, () => pluginModule);
46
+ defineAppDevPluginModule(moduleId, pluginModule);
25
47
  });
26
48
  }
27
49
 
@@ -43,6 +65,12 @@ export function configRequirejs(requirejs: any, pluginData: PluginData[]) {
43
65
  */
44
66
  export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins: [string, PluginClass][]) => void) {
45
67
  return (...pluginModules: (PluginClass & { default?: PluginClass })[]) => {
68
+ pluginModules.forEach((item, index) => {
69
+ if (item) {
70
+ defineAppDevPluginModule(getClientV2ModuleId(pluginData[index].packageName), item);
71
+ }
72
+ });
73
+
46
74
  const res: [string, PluginClass][] = pluginModules
47
75
  .map<[string, PluginClass]>((item, index) => [pluginData[index].name, item?.default || item])
48
76
  .filter((item) => item[1]);
@@ -75,6 +103,77 @@ export function getRemotePlugins(requirejs: any, pluginData: PluginData[] = []):
75
103
  });
76
104
  }
77
105
 
106
+ async function getEsmDevPlugins(pluginData: PluginData[] = []): Promise<Array<[string, PluginClass]>> {
107
+ const plugins: Array<[string, PluginClass]> = [];
108
+ for (const plugin of sortPluginsByAppDevDependencies(pluginData)) {
109
+ const pluginModule: RemotePluginModule = await import(/* webpackIgnore: true */ plugin.url);
110
+ const pluginClass = getPluginClass(pluginModule);
111
+ if (pluginClass) {
112
+ plugins.push([plugin.name, pluginClass]);
113
+ defineDevPluginModules({ [plugin.packageName]: pluginModule });
114
+ }
115
+ }
116
+ return plugins;
117
+ }
118
+
119
+ function sortPluginsByAppDevDependencies(pluginData: PluginData[] = []) {
120
+ const pluginMap = new Map(pluginData.map((plugin) => [plugin.packageName, plugin]));
121
+ const sorted: PluginData[] = [];
122
+ const visiting = new Set<string>();
123
+ const visited = new Set<string>();
124
+
125
+ const visit = (plugin: PluginData) => {
126
+ if (visited.has(plugin.packageName)) {
127
+ return;
128
+ }
129
+ if (visiting.has(plugin.packageName)) {
130
+ return;
131
+ }
132
+ visiting.add(plugin.packageName);
133
+ for (const dep of plugin.appDevDependencies || []) {
134
+ const depPlugin = pluginMap.get(dep);
135
+ if (depPlugin) {
136
+ visit(depPlugin);
137
+ }
138
+ }
139
+ visiting.delete(plugin.packageName);
140
+ visited.add(plugin.packageName);
141
+ sorted.push(plugin);
142
+ };
143
+
144
+ pluginData.forEach(visit);
145
+ return sorted;
146
+ }
147
+
148
+ async function getMixedRemotePluginsInOrder(
149
+ requirejs: RequireJS,
150
+ pluginData: PluginData[] = [],
151
+ ): Promise<Array<[string, PluginClass]>> {
152
+ const plugins: Array<[string, PluginClass]> = [];
153
+ let requirejsPlugins: PluginData[] = [];
154
+ const flushRequirejsPlugins = async () => {
155
+ if (requirejsPlugins.length === 0) {
156
+ return;
157
+ }
158
+ const remotePluginList = await getRemotePlugins(requirejs, requirejsPlugins);
159
+ plugins.push(...remotePluginList);
160
+ requirejsPlugins = [];
161
+ };
162
+
163
+ for (const plugin of sortPluginsByAppDevDependencies(pluginData)) {
164
+ if (plugin.devMode === 'esm') {
165
+ await flushRequirejsPlugins();
166
+ const esmPluginList = await getEsmDevPlugins([plugin]);
167
+ plugins.push(...esmPluginList);
168
+ continue;
169
+ }
170
+ requirejsPlugins.push(plugin);
171
+ }
172
+
173
+ await flushRequirejsPlugins();
174
+ return plugins;
175
+ }
176
+
78
177
  interface GetPluginsOption {
79
178
  requirejs: RequireJS;
80
179
  pluginData: PluginData[];
@@ -91,27 +190,34 @@ export async function getPlugins(options: GetPluginsOption): Promise<Array<[stri
91
190
  const res: Array<[string, PluginClass]> = [];
92
191
 
93
192
  const resolveDevPlugins: Record<string, PluginClass> = {};
193
+ const resolveDevPluginModules: Record<string, RemotePluginModule> = {};
94
194
  if (devDynamicImport) {
95
195
  for await (const plugin of pluginData) {
96
- const pluginModule = await devDynamicImport(plugin.packageName);
196
+ const pluginModule: RemotePluginModule | null = await devDynamicImport(plugin.packageName);
97
197
  if (pluginModule) {
98
- res.push([plugin.name, pluginModule.default]);
99
- resolveDevPlugins[plugin.packageName] = pluginModule.default;
198
+ const pluginClass = getPluginClass(pluginModule);
199
+ res.push([plugin.name, pluginClass]);
200
+ resolveDevPlugins[plugin.packageName] = pluginClass;
201
+ resolveDevPluginModules[plugin.packageName] = pluginModule;
100
202
  }
101
203
  }
102
- defineDevPlugins(resolveDevPlugins);
204
+ defineDevPlugins(resolveDevPluginModules);
103
205
  }
104
206
 
105
207
  const remotePlugins = pluginData.filter((item) => !resolveDevPlugins[item.packageName]);
208
+ const esmDevPlugins = remotePlugins.filter((item) => item.devMode === 'esm');
209
+ const requirejsPlugins = remotePlugins.filter((item) => item.devMode !== 'esm');
106
210
 
107
- if (remotePlugins.length === 0) {
108
- return res;
109
- }
110
-
111
- if (res.length === 0) {
112
- const remotePluginList = await getRemotePlugins(requirejs, remotePlugins);
211
+ if (esmDevPlugins.length === 0) {
212
+ if (requirejsPlugins.length === 0) {
213
+ return res;
214
+ }
215
+ const remotePluginList = await getRemotePlugins(requirejs, requirejsPlugins);
113
216
  res.push(...remotePluginList);
217
+ return res;
114
218
  }
115
219
 
220
+ const mixedPluginList = await getMixedRemotePluginsInOrder(requirejs, remotePlugins);
221
+ res.push(...mixedPluginList);
116
222
  return res;
117
223
  }